From 4c7195087583091755ced122d3ef6c4dc2f47400 Mon Sep 17 00:00:00 2001 From: Urjit Chakraborty <135136842+urjitc@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:52:27 -0400 Subject: [PATCH 1/4] feat(workspaces): reveal PDF citations at cited pages --- .../components/WorkspaceContent.tsx | 16 +++-- .../components/WorkspaceFileViewer.tsx | 8 +-- .../components/WorkspaceImageViewer.tsx | 12 ++-- .../workspaces/components/WorkspaceLayout.tsx | 14 ++++- .../components/WorkspacePaneRenderer.tsx | 2 + .../components/WorkspacePdfViewer.tsx | 60 +++++++++++++++++-- .../components/WorkspaceStandardTabPanes.tsx | 2 +- .../ai-chat/AiChatMessageResponse.test.tsx | 8 +-- .../document-editor/DocumentEditorSurface.tsx | 12 ++-- .../locations/workspace-location-context.tsx | 48 ++++++++++++--- .../navigation/useWorkspaceNavigation.ts | 14 +++-- 11 files changed, 150 insertions(+), 46 deletions(-) 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..f21fee3b 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"; @@ -125,6 +126,15 @@ export function WorkspaceShell({ 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 +302,7 @@ export function WorkspaceShell({ return ( - + {hasHeavyViewerRuntimeItems ? ( {workspaceInteractionContent} ) : ( 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}> { 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}> ); @@ -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/locations/workspace-location-context.tsx b/src/features/workspaces/locations/workspace-location-context.tsx index 04113131..d319ccbe 100644 --- a/src/features/workspaces/locations/workspace-location-context.tsx +++ b/src/features/workspaces/locations/workspace-location-context.tsx @@ -1,22 +1,31 @@ import { FileQuestion, type LucideIcon } from "lucide-react"; -import { createContext, type ReactNode, use } from "react"; +import { createContext, type ReactNode, use, useState } from "react"; import type { WorkspaceLocation } from "#/features/workspaces/locations/workspace-location"; import { getWorkspaceItemDisplay } from "#/features/workspaces/model/item-display"; import type { WorkspaceItem } from "#/features/workspaces/model/types"; +type WorkspacePdfPageLocation = Extract; + type WorkspaceLocationPresentation = { Icon: LucideIcon; iconClassName: string; label: string; }; -type WorkspaceLocationActions = { +type WorkspacePdfPageRevealRequest = { + location: WorkspacePdfPageLocation; + viewInstanceId: string; +}; + +type WorkspaceLocationContextValue = { + consumeRevealRequest: (request: WorkspacePdfPageRevealRequest) => void; getPresentation: (location: WorkspaceLocation) => WorkspaceLocationPresentation; reveal: (location: WorkspaceLocation) => boolean; + revealRequest: WorkspacePdfPageRevealRequest | null; }; -const WorkspaceLocationContext = createContext(null); +const WorkspaceLocationContext = createContext(null); /** * Connects location-aware UI to the current workspace's items and navigation. @@ -24,13 +33,18 @@ const WorkspaceLocationContext = createContext( export function WorkspaceLocationProvider({ children, itemsById, - reveal, + navigate, }: { readonly children: ReactNode; readonly itemsById: ReadonlyMap; - readonly reveal: WorkspaceLocationActions["reveal"]; + readonly navigate: (location: WorkspaceLocation) => string | undefined; }) { - const value: WorkspaceLocationActions = { + const [revealRequest, setRevealRequest] = useState(null); + const consumeRevealRequest = (request: WorkspacePdfPageRevealRequest) => { + setRevealRequest((current) => (current === request ? null : current)); + }; + const value: WorkspaceLocationContextValue = { + consumeRevealRequest, getPresentation(location) { const item = itemsById.get(location.itemId); const itemName = item?.name ?? "Source unavailable"; @@ -48,7 +62,15 @@ export function WorkspaceLocationProvider({ const { Icon, iconClassName } = getWorkspaceItemDisplay(item); return { Icon, iconClassName, label }; }, - reveal, + reveal(location) { + const viewInstanceId = navigate(location); + + setRevealRequest( + viewInstanceId && location.kind === "pdf-page" ? { location, viewInstanceId } : null, + ); + return Boolean(viewInstanceId); + }, + revealRequest, }; return {children}; @@ -65,3 +87,15 @@ export function useWorkspaceLocationActions() { return value; } + +/** + * Returns the latest PDF-page reveal request when it targets this mounted view. + */ +export function useWorkspacePdfPageRevealRequest(viewInstanceId: string) { + const { consumeRevealRequest, revealRequest } = useWorkspaceLocationActions(); + + return { + consume: consumeRevealRequest, + request: revealRequest?.viewInstanceId === viewInstanceId ? revealRequest : null, + }; +} diff --git a/src/features/workspaces/navigation/useWorkspaceNavigation.ts b/src/features/workspaces/navigation/useWorkspaceNavigation.ts index 697c8aa7..c71a3135 100644 --- a/src/features/workspaces/navigation/useWorkspaceNavigation.ts +++ b/src/features/workspaces/navigation/useWorkspaceNavigation.ts @@ -217,24 +217,26 @@ export function useWorkspaceNavigation({ const revealWorkspaceLocation = (location: WorkspaceLocation) => { const item = itemsById.get(location.itemId); if (!item) { - return false; + return undefined; } if (activeTab?.viewItemId === item.id) { - return true; + return activeTab.id; } const matchingTab = session?.tabs.find((tab) => tab.viewItemId === item.id); if (matchingTab) { activateWorkspaceTab(matchingTab); - } else if (activeTab && !activeTab.viewItemId) { + return matchingTab.id; + } + + if (activeTab && !activeTab.viewItemId) { const tab = replaceActiveTabView({ item }); navigateToTab(tab); - } else { - openItemInNewTab({ item }); + return tab.id; } - return true; + return openItemInNewTab({ item }).id; }; const openWorkspaceRoot = () => { if (!activeTab?.viewItemId) { From cedc88767014d8d92d38ae56e5150250e7fc5005 Mon Sep 17 00:00:00 2001 From: Urjit Chakraborty <135136842+urjitc@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:18:01 -0400 Subject: [PATCH 2/4] fix(workspaces): preserve trailing metadata in compact rows Keep PDF page locators and compact-row metadata visible when primary labels are long. Preserve full citation labels for hover and assistive technology. --- .../components/WorkspaceMobileBreadcrumbOverflow.tsx | 4 ++-- .../components/ai-chat/AiChatMessageResponse.test.tsx | 3 ++- .../workspaces/components/ai-chat/WorkspaceCitation.tsx | 9 ++++++--- .../workspaces/components/workspace-card-meta-row.tsx | 4 ++-- .../workspaces/locations/workspace-location-context.tsx | 9 +++++---- 5 files changed, 17 insertions(+), 12 deletions(-) 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/ai-chat/AiChatMessageResponse.test.tsx b/src/features/workspaces/components/ai-chat/AiChatMessageResponse.test.tsx index 2d526a38..9a0d9439 100644 --- a/src/features/workspaces/components/ai-chat/AiChatMessageResponse.test.tsx +++ b/src/features/workspaces/components/ai-chat/AiChatMessageResponse.test.tsx @@ -49,7 +49,8 @@ describe("AI chat message response citations", () => { expect(html).toContain("Source unavailable"); + expect(html).toContain(">· p. 12"); expect(html).not.toContain(" { if (!reveal(location)) { @@ -24,7 +26,8 @@ export function WorkspaceCitation({ location }: { readonly location: WorkspaceLo strokeWidth={1.75} aria-hidden="true" /> - {label} + {label} + {locatorLabel ? · {locatorLabel} : null} ); } 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 ? (