Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/features/workspaces/components/WorkspaceContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -60,7 +60,7 @@ interface WorkspaceContentProps {
type WorkspaceItemActionDialogsState = ReturnType<typeof useWorkspaceItemActionDialogState>;

export default function WorkspaceContent({
instanceId,
viewInstanceId,
items,
activeItem,
workspace,
Expand All @@ -75,7 +75,7 @@ export default function WorkspaceContent({
<>
<WorkspaceItemView
item={activeItem}
viewInstanceId={instanceId ?? activeItem.id}
viewInstanceId={viewInstanceId}
workspaceId={workspaceId}
onMoveItem={actionDialogs.openMoveDialog}
onRenameItem={actionDialogs.setRenamingItem}
Expand Down Expand Up @@ -109,7 +109,7 @@ function WorkspaceBrowseContent({
workspace,
onCreateItem,
onOpenItem,
}: WorkspaceContentProps & {
}: Omit<WorkspaceContentProps, "viewInstanceId"> & {
actionDialogs: WorkspaceItemActionDialogsState;
}) {
const { capabilities } = useWorkspaceMutationAccess();
Expand Down Expand Up @@ -490,15 +490,19 @@ function WorkspaceItemView({

if (item.type === "document") {
return (
<DocumentEditorSurface item={item} toolbarSlotId={viewInstanceId} workspaceId={workspaceId} />
<DocumentEditorSurface
item={item}
viewInstanceId={viewInstanceId}
workspaceId={workspaceId}
/>
);
}

if (item.type === "file") {
return (
<WorkspaceFileViewer
item={item}
toolbarSlotId={viewInstanceId}
viewInstanceId={viewInstanceId}
workspaceId={workspaceId}
onMoveItem={onMoveItem}
onRenameItem={onRenameItem}
Expand Down
8 changes: 4 additions & 4 deletions src/features/workspaces/components/WorkspaceFileViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { cn } from "#/lib/utils";

interface WorkspaceFileViewerComponentProps {
item: WorkspaceItem;
toolbarSlotId?: string;
viewInstanceId: string;
workspaceId: string;
}

Expand All @@ -28,7 +28,7 @@ const workspaceFileViewers: Record<

interface WorkspaceFileViewerProps {
item: WorkspaceItem;
toolbarSlotId: string;
viewInstanceId: string;
workspaceId: string;
onDeleteItem: (item: WorkspaceItem) => void;
onMoveItem: (item: WorkspaceItem) => void;
Expand All @@ -37,7 +37,7 @@ interface WorkspaceFileViewerProps {

export default function WorkspaceFileViewer({
item,
toolbarSlotId,
viewInstanceId,
workspaceId,
onDeleteItem,
onMoveItem,
Expand All @@ -48,7 +48,7 @@ export default function WorkspaceFileViewer({
const viewCapabilities = useWorkspaceViewCapabilities();
const viewerContent = Viewer ? (
<Suspense fallback={<WorkspaceFileViewerSkeleton />}>
<Viewer item={item} toolbarSlotId={toolbarSlotId} workspaceId={workspaceId} />
<Viewer item={item} viewInstanceId={viewInstanceId} workspaceId={workspaceId} />
</Suspense>
) : (
<div className="flex h-full items-center justify-center bg-background">
Expand Down
12 changes: 6 additions & 6 deletions src/features/workspaces/components/WorkspaceImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -33,7 +33,7 @@ export default function WorkspaceImageViewer({
key={fileUrl}
fileUrl={fileUrl}
item={item}
toolbarSlotId={toolbarSlotId}
viewInstanceId={viewInstanceId}
workspaceId={workspaceId}
/>
);
Expand All @@ -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<HTMLImageElement>(null);
Expand Down Expand Up @@ -75,7 +75,7 @@ function WorkspaceImageViewerContent({
: undefined,
fileName: item.name,
fileUrl,
slotId: toolbarSlotId ?? item.id,
slotId: viewInstanceId,
});

const handleImageLoad = useCallback(() => {
Expand Down
17 changes: 14 additions & 3 deletions src/features/workspaces/components/WorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -292,7 +303,7 @@ export function WorkspaceShell({

return (
<WorkspaceMutationAccessProvider membershipRole={workspace.membershipRole}>
<WorkspaceLocationProvider itemsById={itemsById} reveal={revealWorkspaceLocation}>
<WorkspaceLocationProvider itemsById={itemsById} navigate={navigateToWorkspaceLocation}>
{hasHeavyViewerRuntimeItems ? (
<WorkspacePdfEngineProvider>{workspaceInteractionContent}</WorkspacePdfEngineProvider>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ export default function WorkspaceMobileBreadcrumbOverflow({

return (
<DropdownMenuItem key={item.id} onClick={() => onNavigateToItem(item)}>
<span className="inline-flex size-4 items-center justify-center text-muted-foreground">
<span className="inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground">
<Icon className={iconClassName} aria-hidden="true" />
</span>
<span className="min-w-0 truncate">{item.name}</span>
<span className="min-w-0 flex-1 truncate">{item.name}</span>
<span className="ml-auto shrink-0 text-muted-foreground text-xs">{label}</span>
</DropdownMenuItem>
);
Expand Down
2 changes: 2 additions & 0 deletions src/features/workspaces/components/WorkspacePaneRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function WorkspacePaneRenderer({

return (
<WorkspaceContent
viewInstanceId={pane.id}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Citations opened while the workspace is in split presentation will not reveal the page in the target pane: this line identifies the PDF viewer with pane.id, but the navigation callback stores a workspace tab ID in the reveal request. The request consequently matches no mounted WorkspacePdfViewer. The navigation/reveal flow would need to resolve and return the actual rendered pane instance (or otherwise map the tab to that pane) for split/maximized presentations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/WorkspacePaneRenderer.tsx, line 18:

<comment>Citations opened while the workspace is in split presentation will not reveal the page in the target pane: this line identifies the PDF viewer with `pane.id`, but the navigation callback stores a workspace tab ID in the reveal request. The request consequently matches no mounted `WorkspacePdfViewer`. The navigation/reveal flow would need to resolve and return the actual rendered pane instance (or otherwise map the tab to that pane) for split/maximized presentations.</comment>

<file context>
@@ -15,6 +15,7 @@ export default function WorkspacePaneRenderer({
 
 			return (
 				<WorkspaceContent
+					viewInstanceId={pane.id}
 					workspace={workspace}
 					items={scopedItems}
</file context>

workspace={workspace}
items={scopedItems}
activeItem={item}
Expand All @@ -26,6 +27,7 @@ export default function WorkspacePaneRenderer({
case "root":
return (
<WorkspaceContent
viewInstanceId={pane.id}
workspace={workspace}
items={scopedItems}
activeItem={undefined}
Expand Down
60 changes: 56 additions & 4 deletions src/features/workspaces/components/WorkspacePdfViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ScrollPluginPackage,
ScrollStrategy,
useScroll,
useScrollCapability,
} from "@embedpdf/plugin-scroll/react";
import {
SelectionPluginPackage,
Expand All @@ -44,7 +45,10 @@ import {
WorkspaceCaptureViewerFrame,
} from "#/features/workspaces/components/WorkspaceCaptureChrome";
import { useFileItemToolbar } from "#/features/workspaces/components/WorkspaceItemToolbarSlot";
import { useWorkspacePaneHotkey } from "#/features/workspaces/components/WorkspacePaneRuntime";
import {
useWorkspacePaneHotkey,
useWorkspacePaneRuntime,
} from "#/features/workspaces/components/WorkspacePaneRuntime";
import { WorkspacePdfAskSelectionMenu } from "#/features/workspaces/components/WorkspacePdfAskSelectionMenu";
import {
WorkspacePdfCaptureInteractionMode,
Expand All @@ -55,6 +59,7 @@ import { WorkspacePdfPageControl } from "#/features/workspaces/components/Worksp
import { useWorkspaceViewCapabilities } from "#/features/workspaces/components/workspace-view-policy";
import { createCaptureAttachmentFile } from "#/features/workspaces/components/workspace-region-capture";
import { stageCaptureAttachmentToComposerWithFeedback } from "#/features/workspaces/composer/workspace-composer-actions";
import { useWorkspacePdfPageRevealRequest } from "#/features/workspaces/locations/workspace-location-context";
import type { WorkspaceItem } from "#/features/workspaces/model/types";
import { getWorkspaceFileContentUrl } from "#/features/workspaces/model/workspace-file";
import {
Expand Down Expand Up @@ -102,11 +107,11 @@ type WorkspacePdfInteractions = {

export default function WorkspacePdfViewer({
item,
toolbarSlotId,
viewInstanceId,
workspaceId,
}: {
item: WorkspaceItem;
toolbarSlotId?: string;
viewInstanceId: string;
workspaceId: string;
}) {
const fileUrl = getWorkspaceFileContentUrl(workspaceId, item.id);
Expand All @@ -131,7 +136,7 @@ export default function WorkspacePdfViewer({
: undefined,
fileName: item.name,
fileUrl,
slotId: toolbarSlotId ?? item.id,
slotId: viewInstanceId,
});

return (
Expand All @@ -155,6 +160,7 @@ export default function WorkspacePdfViewer({
itemId={item.id}
onCaptureModeExit={exitCapture}
onCaptureModeToggle={toggleCapture}
viewInstanceId={viewInstanceId}
workspaceId={workspaceId}
/>
) : (
Expand All @@ -177,6 +183,7 @@ function WorkspacePdfDocumentLoader({
itemId,
onCaptureModeExit,
onCaptureModeToggle,
viewInstanceId,
workspaceId,
}: {
activeDocumentId: string | null;
Expand All @@ -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;
Comment on lines +197 to +209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether `@embedpdf/plugin-scroll` exposes a synchronous "current layout/ready" query
# that can be checked immediately (avoiding reliance on a fresh onLayoutReady firing).
fd -HI 'plugin-scroll' node_modules/@embedpdf 2>/dev/null
rg -n 'isInitial|onLayoutReady|getState|totalPages' node_modules/@embedpdf/plugin-scroll/dist 2>/dev/null | head -50

Repository: ThinkEx-OSS/thinkex

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files of interest:"
git ls-files | rg '(^|/)WorkspacePdfViewer\.tsx$|WorkspaceStandardTabPanes|package\.json|yarn\.lock|pnpm-lock\.yaml|package-lock\.json'

echo
echo "Dependency versions:"
if [ -f package.json ]; then jq '.dependencies, .devDependencies' package.json | rg -n 'embedpdf|react-pdf|pdf|plugin-scroll' -C 2; fi

echo
echo "Inspect WorkspacePdfViewer outline and relevant sections:"
ast-grep outline src/features/workspaces/components/WorkspacePdfViewer.tsx 2>/dev/null || true
wc -l src/features/workspaces/components/WorkspacePdfViewer.tsx
sed -n '1,340p' src/features/workspaces/components/WorkspacePdfViewer.tsx

echo
echo "Inspect WorkspaceStandardTabPanes:"
fd -HI 'WorkspaceStandardTabPanes' . -x sh -c 'echo "FILE: $1"; wc -l "$1"; sed -n "1,220p" "$1"' sh {}

echo
echo "Search revealWorkspaceLocation and related types:"
rg -n "revealWorkspaceLocation|WorkspacePdfPage|useWorkspacePdfPageRevealRequest|onLayoutReady|viewInstanceId|handleScroll" src package.json

Repository: ThinkEx-OSS/thinkex

Length of output: 20236


🌐 Web query:

embedpdf plugin-scroll onLayoutReady isInitial documentation

💡 Result:

In the EmbedPDF viewer, the onLayoutReady event is part of the Scroll plugin and is used to perform actions—such as programmatic scrolling—once a document's layout has been fully initialized and is ready for interaction [1][2][3]. The isInitial property is a boolean flag included in the payload of the onLayoutReady event [1][4]. It serves to distinguish between different layout stages: - isInitial is true only on the first layout immediately following the initial document load [1][5]. - isInitial is false on subsequent layouts, such as when switching between tabs or re-rendering the layout [1][5]. Because documents load asynchronously, developers should use the onLayoutReady event rather than attempting to call navigation methods like scrollToPage immediately after initialization [2][6][3]. Checking that isInitial is true ensures that your logic (e.g., jumping to a specific page on load) only executes when the document is first ready [2][4][5]. This pattern was introduced as part of a update that removed the global initialPage configuration option in favor of more flexible, programmatic control [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect workspace navigation / revealWorkspaceLocation:"
wc -l src/features/workspaces/navigation/useWorkspaceNavigation.ts
sed -n '180,290p' src/features/workspaces/navigation/useWorkspaceNavigation.ts

echo
echo "Inspect workspace location context:"
wc -l src/features/workspaces/locations/workspace-location-context.tsx
sed -n '1,120p' src/features/workspaces/locations/workspace-location-context.tsx

echo
echo "Inspect workspace content around pdf viewer path:"
sed -n '380,530p' src/features/workspaces/components/WorkspaceContent.tsx

echo
echo "Search pdfPage references:"
rg -n "pdf-page|pdfPage|pdf-page|locationPage|pageNumber|revealWorkspaceLocation|matchingTab|activeItem" src -C 2

Repository: ThinkEx-OSS/thinkex

Length of output: 50375


🌐 Web query:

embedpdf plugin-scroll source onLayoutReady handler isInitial

💡 Result:

In the EmbedPDF viewer, the onLayoutReady event handler within the scroll plugin is used to handle scrolling after a document has loaded [1][2]. Because document loading is asynchronous, this event ensures that the document structure is calculated and the viewer is ready for interaction before attempting to navigate to a specific position [1][3]. The onLayoutReady event provides a payload containing documentId and isInitial properties [4][5]. The isInitial boolean is a key feature used to differentiate between the first layout following a document's initial load (where isInitial is true) and subsequent layout events, such as those triggered by switching tabs or document re-renders (where isInitial is false) [6][7]. This mechanism was introduced as part of a move to remove the global initialPage configuration option [6][7]. By checking if isInitial is true, developers can safely trigger actions—like scrolling to a specific page—only once during the initial load cycle [1][2]. Example usage in React: const unsubscribe = scrollCapability.onLayoutReady((event) => { if (event.documentId === documentId && event.isInitial) { scrollCapability.forDocument(documentId).scrollToPage({ pageNumber: initialPage, behavior: 'instant', }); } });

Citations:


🌐 Web query:

github embedpdf embed-pdf-viewer plugin-scroll source onLayoutReady isInitial

💡 Result:

The terms onLayoutReady and isInitial refer to the API provided by the @embedpdf/plugin-scroll package within the embedpdf/embed-pdf-viewer repository [1][2][3]. The onLayoutReady event is used to detect when a document's layout is fully calculated and ready for interaction, which is necessary because PDF documents load asynchronously [1][3]. The isInitial flag is a boolean included in the event payload that allows developers to distinguish between the first layout after an initial document load (true) and subsequent layout events, such as those occurring when switching between tabs (false) [3][4][5]. This mechanism was introduced to replace the deprecated initialPage configuration option, as the latter did not support multi-document scenarios effectively [4][5]. Developers typically use this to perform actions like scrolling to a specific page immediately after the viewer initializes [1][4]. Example usage (React): const unsubscribe = scrollCapability.onLayoutReady((event) => { if (event.documentId === 'my-doc' && event.isInitial) { scrollCapability.forDocument('my-doc').scrollToPage({ pageNumber: 3, behavior: 'instant' }); } });

Citations:


🌐 Web query:

embedpdf plugin-scroll @embedpdf/plugin-scroll source onLayoutReady isInitial getLayout

💡 Result:

In the @embedpdf/plugin-scroll package, the onLayoutReady event is a lifecycle event provided by the scroll capability that triggers when a document's layout has been calculated and the viewer is ready for navigation [1][2][3]. The event provides a payload containing the documentId and a boolean isInitial property [1][2]. The isInitial property is true only during the first layout calculation immediately following the initial loading of the document [1][2]. It becomes false on subsequent layout calculations, such as those that occur when switching tabs or changing viewer configurations [1][2][4]. This event is essential for programmatic navigation, such as calling scrollToPage, because it ensures the document structure is available before scroll operations are attempted [3]. While the provided information focuses on onLayoutReady, the getLayout function is not typically cited as a direct top-level public API in the standard scroll plugin documentation [1][2][3]. Interaction with layout state is generally handled via the provided scroll capability hooks (like useScroll) and events (like onLayoutReady or onScroll) which abstract the underlying layout calculations [1][3].

Citations:


Check EmbedPDF layout readiness synchronously before relying on onLayoutReady.

For reused inactive PDF tabs, revealWorkspaceLocation reactivates an already-mounted view, and this effect may subscribe to scrollCapability.onLayoutAfter after that document’s layout has already fired. Since onLayoutReady is modeled as a lifecycle event, a late subscriber can be left waiting and request will never be consumed, making citation-aware PDF navigation no-op on the standard tab-reuse path. Handle already-ready state before subscribing if a synchronous capability/query is available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/components/WorkspacePdfViewer.tsx` around lines 197 -
209, Update the PDF reveal effect in the component containing
useWorkspacePdfPageRevealRequest to synchronously check whether
scrollCapability’s layout is already ready before subscribing to its lifecycle
event. Immediately consume or process the pending request when ready, while
retaining the onLayoutAfter subscription for layouts that are not yet ready so
reused inactive tabs navigate correctly.


useEffect(() => {
if (!documentManager || documentManager.isDocumentOpen(documentId)) {
Expand Down Expand Up @@ -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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle citations after the PDF layout is already ready

When a citation targets a PDF that is already open and laid out, this effect subscribes to onLayoutReady only after the click, so the existing readiness event has already passed and no immediate call to scrollToPage occurs. The request then remains pending until another layout event happens, meaning citations for the currently viewed PDF—the common case—do not reveal their cited page; consult the current scroll state or retain layout readiness and scroll immediately when it is already available.

Useful? React with 👍 / 👎.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The consume function returned by useWorkspacePdfPageRevealRequest gets a new reference on every render of WorkspaceLocationProvider because it's defined inline without useCallback. Since the new effect depends on consume, this causes the effect to tear down and re-register the onLayoutReady listener whenever the provider re-renders — even when nothing about the reveal request has actually changed. Consider stabilizing the reference with useCallback in the provider to avoid wasteful listener churn.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/WorkspacePdfViewer.tsx, line 287:

<comment>The `consume` function returned by `useWorkspacePdfPageRevealRequest` gets a new reference on every render of `WorkspaceLocationProvider` because it's defined inline without `useCallback`. Since the new effect depends on `consume`, this causes the effect to tear down and re-register the `onLayoutReady` listener whenever the provider re-renders — even when nothing about the reveal request has actually changed. Consider stabilizing the reference with `useCallback` in the provider to avoid wasteful listener churn.</comment>

<file context>
@@ -240,6 +252,46 @@ function WorkspacePdfDocumentLoader({
+		});
+	}, [
+		activeDocumentId,
+		consume,
+		currentOpenError,
+		documentId,
</file context>

currentOpenError,
documentId,
isActive,
request,
scrollCapability,
]);

if (currentOpenError) {
return (
<WorkspacePdfLoadFailure fileName={fileName} fileUrl={fileUrl}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function WorkspaceStandardTabPanes({
onCloseItemView={canCloseItemView ? onCloseItemView : undefined}
>
<WorkspaceContent
instanceId={tab.id}
viewInstanceId={tab.id}
items={scopedItems}
activeItem={tab.viewItemId ? itemsById.get(tab.viewItemId) : undefined}
workspace={workspace}
Expand Down
Loading