diff --git a/src/features/workspaces/components/WorkspaceContent.tsx b/src/features/workspaces/components/WorkspaceContent.tsx index fb13a3bc..b85bb646 100644 --- a/src/features/workspaces/components/WorkspaceContent.tsx +++ b/src/features/workspaces/components/WorkspaceContent.tsx @@ -49,7 +49,7 @@ import { useNativeFileDropTarget } from "#/lib/use-native-file-drop-target"; import { cn } from "#/lib/utils"; interface WorkspaceContentProps { - instanceId?: string; + viewInstanceId: string; items: WorkspaceItem[]; activeItem?: WorkspaceItem; workspace: WorkspaceSummary; @@ -60,7 +60,7 @@ interface WorkspaceContentProps { type WorkspaceItemActionDialogsState = ReturnType; export default function WorkspaceContent({ - instanceId, + viewInstanceId, items, activeItem, workspace, @@ -75,7 +75,7 @@ export default function WorkspaceContent({ <> & { actionDialogs: WorkspaceItemActionDialogsState; }) { const { capabilities } = useWorkspaceMutationAccess(); @@ -490,7 +490,11 @@ function WorkspaceItemView({ if (item.type === "document") { return ( - + ); } @@ -498,7 +502,7 @@ function WorkspaceItemView({ return ( void; onMoveItem: (item: WorkspaceItem) => void; @@ -37,7 +37,7 @@ interface WorkspaceFileViewerProps { export default function WorkspaceFileViewer({ item, - toolbarSlotId, + viewInstanceId, workspaceId, onDeleteItem, onMoveItem, @@ -48,7 +48,7 @@ export default function WorkspaceFileViewer({ const viewCapabilities = useWorkspaceViewCapabilities(); const viewerContent = Viewer ? ( }> - + ) : (
diff --git a/src/features/workspaces/components/WorkspaceImageViewer.tsx b/src/features/workspaces/components/WorkspaceImageViewer.tsx index 73331e13..9b0b7ce0 100644 --- a/src/features/workspaces/components/WorkspaceImageViewer.tsx +++ b/src/features/workspaces/components/WorkspaceImageViewer.tsx @@ -17,13 +17,13 @@ import { cn } from "#/lib/utils"; interface WorkspaceImageViewerProps { item: WorkspaceItem; - toolbarSlotId?: string; + viewInstanceId: string; workspaceId: string; } export default function WorkspaceImageViewer({ item, - toolbarSlotId, + viewInstanceId, workspaceId, }: WorkspaceImageViewerProps) { const fileUrl = getWorkspaceFileContentUrl(workspaceId, item.id); @@ -33,7 +33,7 @@ export default function WorkspaceImageViewer({ key={fileUrl} fileUrl={fileUrl} item={item} - toolbarSlotId={toolbarSlotId} + viewInstanceId={viewInstanceId} workspaceId={workspaceId} /> ); @@ -42,12 +42,12 @@ export default function WorkspaceImageViewer({ function WorkspaceImageViewerContent({ fileUrl, item, - toolbarSlotId, + viewInstanceId, workspaceId, }: { fileUrl: string; item: WorkspaceItem; - toolbarSlotId?: string; + viewInstanceId: string; workspaceId: string; }) { const imageRef = useRef(null); @@ -75,7 +75,7 @@ function WorkspaceImageViewerContent({ : undefined, fileName: item.name, fileUrl, - slotId: toolbarSlotId ?? item.id, + slotId: viewInstanceId, }); const handleImageLoad = useCallback(() => { diff --git a/src/features/workspaces/components/WorkspaceLayout.tsx b/src/features/workspaces/components/WorkspaceLayout.tsx index 4384d72e..766f38dc 100644 --- a/src/features/workspaces/components/WorkspaceLayout.tsx +++ b/src/features/workspaces/components/WorkspaceLayout.tsx @@ -27,11 +27,12 @@ import type { WorkspaceItemType, WorkspaceSummary, } from "#/features/workspaces/contracts"; +import type { WorkspaceLocation } from "#/features/workspaces/locations/workspace-location"; +import { WorkspaceLocationProvider } from "#/features/workspaces/locations/workspace-location-context"; import type { WorkspaceItem } from "#/features/workspaces/model/types"; import { isWorkspaceItemView } from "#/features/workspaces/model/view"; import { workspaceItemRequiresHeavyViewerRuntime } from "#/features/workspaces/model/workspace-file"; import { getWorkspaceMobileChatSurfaceMode } from "#/features/workspaces/model/workspace-ui"; -import { WorkspaceLocationProvider } from "#/features/workspaces/locations/workspace-location-context"; import { useWorkspaceNavigation } from "#/features/workspaces/navigation/useWorkspaceNavigation"; import { useWorkspaceRealtime } from "#/features/workspaces/realtime/use-workspace-presence"; import { useWorkspacePersistedStoresHydrated } from "#/features/workspaces/state/persisted-store-hydration"; @@ -79,6 +80,7 @@ export function WorkspaceShell({ const selectedQuotes = useWorkspaceAiComposerDraftQuotes(workspace.id); const setChatSurfaceMode = useWorkspaceUiStore((state) => state.setChatSurfaceMode); const toggleChatPanel = useWorkspaceUiStore((state) => state.toggleChatPanel); + const normalizedUiSession = useWorkspaceUiSession(workspace.id); const realtime = useWorkspaceRealtime({ workspaceId: workspace.id, lastSeenRevision: revision, @@ -116,15 +118,24 @@ export function WorkspaceShell({ } = useWorkspaceNavigation({ workspace, items, + presentation: normalizedUiSession.presentation, activeTabIdFromUrl, activeViewFromUrl, }); - const normalizedUiSession = useWorkspaceUiSession(workspace.id); const { capabilities: viewCapabilities, viewportMode } = useWorkspaceViewPolicy(); const selectedItemIds = useWorkspaceSelectionItemIds(workspace.id); const { chatSurfaceMode, presentation } = normalizedUiSession; const mobileChatSurfaceMode = getWorkspaceMobileChatSurfaceMode(chatSurfaceMode); const hasHeavyViewerRuntimeItems = scopedItems.some(workspaceItemRequiresHeavyViewerRuntime); + const navigateToWorkspaceLocation = (location: WorkspaceLocation) => { + const viewInstanceId = revealWorkspaceLocation(location); + + if (viewInstanceId && chatSurfaceMode === "fullscreen") { + setChatSurfaceMode(workspace.id, "hidden"); + } + + return viewInstanceId; + }; const createWorkspaceItem = (input: { type: WorkspaceItemType; parentId: string | null }) => { if (!getWorkspaceMemberCapabilities(workspace.membershipRole).canMutateContent) { return; @@ -292,7 +303,7 @@ export function WorkspaceShell({ return ( - + {hasHeavyViewerRuntimeItems ? ( {workspaceInteractionContent} ) : ( diff --git a/src/features/workspaces/components/WorkspaceMobileBreadcrumbOverflow.tsx b/src/features/workspaces/components/WorkspaceMobileBreadcrumbOverflow.tsx index 0e59d89e..3604d6c2 100644 --- a/src/features/workspaces/components/WorkspaceMobileBreadcrumbOverflow.tsx +++ b/src/features/workspaces/components/WorkspaceMobileBreadcrumbOverflow.tsx @@ -47,10 +47,10 @@ export default function WorkspaceMobileBreadcrumbOverflow({ return ( onNavigateToItem(item)}> - + - {item.name} + {item.name} {label} ); diff --git a/src/features/workspaces/components/WorkspacePaneRenderer.tsx b/src/features/workspaces/components/WorkspacePaneRenderer.tsx index f4bb144d..1ba68c8a 100644 --- a/src/features/workspaces/components/WorkspacePaneRenderer.tsx +++ b/src/features/workspaces/components/WorkspacePaneRenderer.tsx @@ -15,6 +15,7 @@ export default function WorkspacePaneRenderer({ return ( ) : ( @@ -177,6 +183,7 @@ function WorkspacePdfDocumentLoader({ itemId, onCaptureModeExit, onCaptureModeToggle, + viewInstanceId, workspaceId, }: { activeDocumentId: string | null; @@ -187,14 +194,19 @@ function WorkspacePdfDocumentLoader({ itemId: string; onCaptureModeExit: () => void; onCaptureModeToggle: () => void; + viewInstanceId: string; workspaceId: string; }) { const { provides: documentManager } = useDocumentManagerCapability(); + const { provides: scrollCapability } = useScrollCapability(); + const paneRuntime = useWorkspacePaneRuntime(); + const { consume, request } = useWorkspacePdfPageRevealRequest(viewInstanceId); const [openError, setOpenError] = useState<{ documentId: string; message: string; } | null>(null); const currentOpenError = openError?.documentId === documentId ? openError.message : null; + const isActive = paneRuntime?.isActive ?? true; useEffect(() => { if (!documentManager || documentManager.isDocumentOpen(documentId)) { @@ -240,6 +252,46 @@ function WorkspacePdfDocumentLoader({ }; }, [documentId, documentManager, fileName, fileUrl]); + useEffect(() => { + if (!request) { + return; + } + + if (currentOpenError || request.location.itemId !== documentId) { + consume(request); + return; + } + + if (!isActive || activeDocumentId !== documentId || !scrollCapability) { + return; + } + + let handled = false; + + return scrollCapability.onLayoutReady((event) => { + if (handled || event.documentId !== documentId) { + return; + } + + handled = true; + if (request.location.pageNumber <= event.totalPages) { + scrollCapability.forDocument(documentId).scrollToPage({ + behavior: "instant", + pageNumber: request.location.pageNumber, + }); + } + consume(request); + }); + }, [ + activeDocumentId, + consume, + currentOpenError, + documentId, + isActive, + request, + scrollCapability, + ]); + if (currentOpenError) { return ( diff --git a/src/features/workspaces/components/WorkspaceStandardTabPanes.tsx b/src/features/workspaces/components/WorkspaceStandardTabPanes.tsx index 9846a3ad..27792edf 100644 --- a/src/features/workspaces/components/WorkspaceStandardTabPanes.tsx +++ b/src/features/workspaces/components/WorkspaceStandardTabPanes.tsx @@ -45,7 +45,7 @@ export default function WorkspaceStandardTabPanes({ onCloseItemView={canCloseItemView ? onCloseItemView : undefined} > { it("renders a validated citation as an app-owned source button", () => { const ref = "wr_AAAAAAAA" as WorkspaceReference; const html = renderToStaticMarkup( - false}> + undefined}> { expect(html).toContain("Source unavailable"); + expect(html).toContain(">· p. 12"); expect(html).not.toContain(" { const html = renderToStaticMarkup( true} + navigate={() => "tab-1"} > { it("does not expose an incomplete streamed citation tag", () => { const html = renderToStaticMarkup( - false}> + undefined}> {'Claim { it("renders non-empty citation markup as inert text", () => { const ref = "wr_AAAAAAAA" as WorkspaceReference; const html = renderToStaticMarkup( - false}> + undefined}> { + // This browser-only module installs KaTeX's global copy listener. + void import("katex/contrib/copy-tex"); + }, []); + const mergedComponents = { ...streamdownComponents, ...components, diff --git a/src/features/workspaces/components/ai-chat/AiChatModelPicker.tsx b/src/features/workspaces/components/ai-chat/AiChatModelPicker.tsx index a099dde1..bd03f0d3 100644 --- a/src/features/workspaces/components/ai-chat/AiChatModelPicker.tsx +++ b/src/features/workspaces/components/ai-chat/AiChatModelPicker.tsx @@ -11,11 +11,9 @@ import { type WorkspaceAiChatModelLevel, } from "#/features/workspaces/ai/models"; import { ProviderLogo } from "#/features/workspaces/components/ai-chat/ProviderLogo"; +import { WorkspaceToolbarTextButton } from "#/features/workspaces/components/WorkspaceToolbar"; import { cn } from "#/lib/utils"; -const TRIGGER_CLASSNAME = - "flex h-8.5 items-center gap-1.5 rounded-md px-2 text-sm font-normal text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-expanded:text-foreground"; - interface AiChatModelPickerProps { modelId: WorkspaceAiChatModelId; onModelChange?: (modelId: WorkspaceAiChatModelId) => void; @@ -55,7 +53,11 @@ export default function AiChatModelPicker({ modelId, onModelChange }: AiChatMode } }} > - + + } + > {selectedModel.name} diff --git a/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx b/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx index 03b5a87f..3ea7acc0 100644 --- a/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx +++ b/src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx @@ -203,7 +203,6 @@ export default function AiChatPromptInput({ aria-label={dictation.isActive ? "Stop dictation" : "Start dictation"} aria-pressed={dictation.isActive} className={cn( - "rounded-full", dictation.isActive && "bg-destructive/10 text-destructive hover:bg-destructive/20 hover:text-destructive", )} diff --git a/src/features/workspaces/components/ai-chat/AiChatToolActivityRow.tsx b/src/features/workspaces/components/ai-chat/AiChatToolActivityRow.tsx index aa387da9..f9f72e44 100644 --- a/src/features/workspaces/components/ai-chat/AiChatToolActivityRow.tsx +++ b/src/features/workspaces/components/ai-chat/AiChatToolActivityRow.tsx @@ -153,14 +153,12 @@ function ActivitySummary({ role={isRunning ? "status" : undefined} aria-live={isRunning ? "polite" : undefined} title={activity.summary} - className="group/tool-row inline-flex min-w-0 max-w-full items-center gap-1.5 py-0.5 text-sm text-muted-foreground" + className="group/tool-row inline-flex min-w-0 max-w-full items-center gap-1.5 py-0.5 text-muted-foreground text-xs" > - + - + {activity.summary} diff --git a/src/features/workspaces/components/ai-chat/WorkspaceCitation.tsx b/src/features/workspaces/components/ai-chat/WorkspaceCitation.tsx index 56cae6fa..e8a65f57 100644 --- a/src/features/workspaces/components/ai-chat/WorkspaceCitation.tsx +++ b/src/features/workspaces/components/ai-chat/WorkspaceCitation.tsx @@ -6,12 +6,14 @@ import { cn } from "#/lib/utils"; export function WorkspaceCitation({ location }: { readonly location: WorkspaceLocation }) { const { getPresentation, reveal } = useWorkspaceLocationActions(); - const { Icon, iconClassName, label } = getPresentation(location); + const { Icon, iconClassName, label, locatorLabel } = getPresentation(location); + const accessibleLabel = locatorLabel ? `${label} · ${locatorLabel}` : label; return ( ); } diff --git a/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx b/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx index c47bb649..dfb55d6b 100644 --- a/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx +++ b/src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx @@ -24,11 +24,11 @@ import { getAuthSessionQueryOptions } from "#/lib/session-query"; export function DocumentEditorSurface({ item, - toolbarSlotId, + viewInstanceId, workspaceId, }: { item: WorkspaceItem; - toolbarSlotId?: string; + viewInstanceId: string; workspaceId: string; }) { const { data: session } = useQuery(getAuthSessionQueryOptions()); @@ -49,7 +49,7 @@ export function DocumentEditorSurface({ ); @@ -58,12 +58,12 @@ export function DocumentEditorSurface({ function DocumentEditorInstance({ collaborationSession, item, - toolbarSlotId, + viewInstanceId, workspaceId, }: { collaborationSession: DocumentCollaborationSession; item: WorkspaceItem; - toolbarSlotId?: string; + viewInstanceId: string; workspaceId: string; }) { const { capabilities } = useWorkspaceMutationAccess(); @@ -98,7 +98,7 @@ function DocumentEditorInstance({ }, }); - useDocumentEditorToolbar(toolbarSlotId ?? item.id, capabilities.canMutateContent ? editor : null); + useDocumentEditorToolbar(viewInstanceId, capabilities.canMutateContent ? editor : null); return (
diff --git a/src/features/workspaces/components/workspace-card-meta-row.tsx b/src/features/workspaces/components/workspace-card-meta-row.tsx index ab4e7e67..a1b97782 100644 --- a/src/features/workspaces/components/workspace-card-meta-row.tsx +++ b/src/features/workspaces/components/workspace-card-meta-row.tsx @@ -15,12 +15,12 @@ export function WorkspaceCardMetaRow({ leading, trailing }: WorkspaceCardMetaRow return (
- {hasLeading ?
{leading}
: null} + {hasLeading ?
{leading}
: null} {hasLeading && hasTrailing ? (