From c7dec1b4b165d3f98a5219cf9b2789f0e09a0be1 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Fri, 10 Apr 2026 15:55:40 -0700 Subject: [PATCH 1/4] fix(annotate): include original file annotations when submitting from linked doc view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using `plannotator annotate` on a file with markdown links, annotations on the original file were silently dropped if the user clicked "Send Annotations" while viewing a linked document. The root cause was that `getDocAnnotations()` only read from `docCache` (previously-visited linked docs) and the current linked doc — it never read from `savedPlanState`, where the original file's annotations are stashed during navigation. This adds a `sourceFilePath` option to `useLinkedDoc` so the hook can key the stashed annotations in the returned Map. Also updates `docAnnotationCount` in `open()` so button visibility and exit warnings reflect reality immediately, not only after calling `back()`. Closes #535 For provenance purposes, this commit was AI assisted. --- packages/editor/App.tsx | 6 +++++- packages/ui/hooks/useLinkedDoc.ts | 24 ++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index ca75dd06..ca1e6304 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -134,6 +134,7 @@ const App: React.FC = () => { const [globalAttachments, setGlobalAttachments] = useState([]); const [annotateMode, setAnnotateMode] = useState(false); const [annotateSource, setAnnotateSource] = useState<'file' | 'message' | 'folder' | null>(null); + const [sourceFilePath, setSourceFilePath] = useState(); const [imageBaseDir, setImageBaseDir] = useState(undefined); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); @@ -218,7 +219,7 @@ const App: React.FC = () => { const linkedDocHook = useLinkedDoc({ markdown, annotations, selectedAnnotationId, globalAttachments, setMarkdown, setAnnotations, setSelectedAnnotationId, setGlobalAttachments, - viewerRef, sidebar, + viewerRef, sidebar, sourceFilePath, }); // Archive browser @@ -554,6 +555,9 @@ const App: React.FC = () => { } if (data.filePath) { setImageBaseDir(data.mode === 'annotate-folder' ? data.filePath : data.filePath.replace(/\/[^/]+$/, '')); + if (data.mode === 'annotate') { + setSourceFilePath(data.filePath); + } } if (data.sharingEnabled !== undefined) { setSharingEnabled(data.sharingEnabled); diff --git a/packages/ui/hooks/useLinkedDoc.ts b/packages/ui/hooks/useLinkedDoc.ts index 23f2e6b9..a77da800 100644 --- a/packages/ui/hooks/useLinkedDoc.ts +++ b/packages/ui/hooks/useLinkedDoc.ts @@ -22,6 +22,9 @@ export interface UseLinkedDocOptions { setGlobalAttachments: (att: ImageAttachment[]) => void; viewerRef: React.RefObject; sidebar: { open: (tab?: SidebarTab) => void }; + /** Absolute path of the primary document — enables getDocAnnotations() to include + * stashed original-file annotations when viewing a linked doc. */ + sourceFilePath?: string; } interface SavedPlanState { @@ -53,7 +56,7 @@ export interface UseLinkedDocReturn { dismissError: () => void; /** All linked doc annotations including the active doc's live state (keyed by filepath) */ getDocAnnotations: () => Map; - /** Reactive count of cached linked doc annotations (updates on back()) */ + /** Reactive count of annotations on non-active documents (updates on open() and back()) */ docAnnotationCount: number; } @@ -71,6 +74,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { setGlobalAttachments, viewerRef, sidebar, + sourceFilePath, } = options; const [linkedDoc, setLinkedDoc] = useState<{ filepath: string } | null>(null); @@ -120,12 +124,21 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { selectedAnnotationId, globalAttachments: [...globalAttachments], }; + setDocAnnotationCount(annotations.length + globalAttachments.length); } else if (linkedDoc) { // Already viewing a linked doc — cache its annotations before moving on docCache.current.set(linkedDoc.filepath, { annotations: [...annotations], globalAttachments: [...globalAttachments], }); + let total = 0; + for (const cached of docCache.current.values()) { + total += cached.annotations.length + cached.globalAttachments.length; + } + if (savedPlanState.current) { + total += savedPlanState.current.annotations.length + savedPlanState.current.globalAttachments.length; + } + setDocAnnotationCount(total); } // Check cache for previous annotations on this file @@ -219,6 +232,13 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { const getDocAnnotations = useCallback((): Map => { const result = new Map(docCache.current); + // Include stashed original-file annotations when viewing a linked doc + if (linkedDoc && savedPlanState.current && sourceFilePath) { + result.set(sourceFilePath, { + annotations: savedPlanState.current.annotations, + globalAttachments: savedPlanState.current.globalAttachments, + }); + } if (linkedDoc) { result.set(linkedDoc.filepath, { annotations: [...annotations], @@ -226,7 +246,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { }); } return result; - }, [linkedDoc, annotations, globalAttachments]); + }, [linkedDoc, annotations, globalAttachments, sourceFilePath]); return { isActive: linkedDoc !== null, From e5627a5fad1c561fc0f8230030d6b2319a0a8900 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Fri, 10 Apr 2026 16:15:09 -0700 Subject: [PATCH 2/4] fix(annotate): include docCache in count when re-entering linked doc navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first branch of open() set docAnnotationCount from only the original file's annotations, ignoring any linked docs already cached from prior navigation. After original → linkedA → back → linkedB, the count dropped to 0 if the original had no annotations, hiding the Send Annotations button despite linkedA's annotations sitting in docCache. Also adds defensive spread copies for savedPlanState arrays in getDocAnnotations() to match the hook's existing pattern. For provenance purposes, this commit was AI assisted. --- packages/ui/hooks/useLinkedDoc.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/ui/hooks/useLinkedDoc.ts b/packages/ui/hooks/useLinkedDoc.ts index a77da800..f5859623 100644 --- a/packages/ui/hooks/useLinkedDoc.ts +++ b/packages/ui/hooks/useLinkedDoc.ts @@ -124,7 +124,11 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { selectedAnnotationId, globalAttachments: [...globalAttachments], }; - setDocAnnotationCount(annotations.length + globalAttachments.length); + let total = annotations.length + globalAttachments.length; + for (const cached of docCache.current.values()) { + total += cached.annotations.length + cached.globalAttachments.length; + } + setDocAnnotationCount(total); } else if (linkedDoc) { // Already viewing a linked doc — cache its annotations before moving on docCache.current.set(linkedDoc.filepath, { @@ -235,8 +239,8 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { // Include stashed original-file annotations when viewing a linked doc if (linkedDoc && savedPlanState.current && sourceFilePath) { result.set(sourceFilePath, { - annotations: savedPlanState.current.annotations, - globalAttachments: savedPlanState.current.globalAttachments, + annotations: [...savedPlanState.current.annotations], + globalAttachments: [...savedPlanState.current.globalAttachments], }); } if (linkedDoc) { From 486202e3309325320ee3467b3e6eb5c057b0378d Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Fri, 10 Apr 2026 16:30:11 -0700 Subject: [PATCH 3/4] fix(annotate): exclude destination doc from docAnnotationCount to prevent double-counting When navigating to a previously-cached linked doc, the destination's cached annotations were included in docAnnotationCount AND loaded into allAnnotations, causing the exit warning to report an inflated count. Skip the destination filepath when summing the non-active total. For provenance purposes, this commit was AI assisted. --- packages/ui/hooks/useLinkedDoc.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ui/hooks/useLinkedDoc.ts b/packages/ui/hooks/useLinkedDoc.ts index f5859623..cfe9c118 100644 --- a/packages/ui/hooks/useLinkedDoc.ts +++ b/packages/ui/hooks/useLinkedDoc.ts @@ -125,7 +125,8 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { globalAttachments: [...globalAttachments], }; let total = annotations.length + globalAttachments.length; - for (const cached of docCache.current.values()) { + for (const [fp, cached] of docCache.current.entries()) { + if (fp === data.filepath!) continue; // destination becomes active — don't double-count total += cached.annotations.length + cached.globalAttachments.length; } setDocAnnotationCount(total); @@ -136,7 +137,8 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { globalAttachments: [...globalAttachments], }); let total = 0; - for (const cached of docCache.current.values()) { + for (const [fp, cached] of docCache.current.entries()) { + if (fp === data.filepath!) continue; // destination becomes active — don't double-count total += cached.annotations.length + cached.globalAttachments.length; } if (savedPlanState.current) { From bdc42f6cdee4cecf681306882b22cb86b28b4c52 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Fri, 10 Apr 2026 16:54:27 -0700 Subject: [PATCH 4/4] fix(annotate): treat backlinks to source file as back() navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a linked document contains a link back to the source file (e.g., original.md → design.md → backlink to original.md), opening it as a linked doc created two competing Map entries for the same filepath in getDocAnnotations(). The empty linked-doc entry overwrote the stashed annotations, silently dropping them. Now detects when the resolved destination matches sourceFilePath and routes through back() instead, which caches the current linked doc and restores the source file with its annotations intact. For provenance purposes, this commit was AI assisted. --- packages/ui/hooks/useLinkedDoc.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/ui/hooks/useLinkedDoc.ts b/packages/ui/hooks/useLinkedDoc.ts index cfe9c118..5ac01bc6 100644 --- a/packages/ui/hooks/useLinkedDoc.ts +++ b/packages/ui/hooks/useLinkedDoc.ts @@ -113,6 +113,18 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { return; } + // Backlink detection: if a linked doc links back to the source file (e.g., + // original.md → design.md → link back to original.md), opening it as a linked + // doc would create two competing Map entries for the same filepath in + // getDocAnnotations(), and the empty linked-doc entry would overwrite the + // stashed annotations. Instead, treat the backlink as a back() navigation — + // the current linked doc gets cached and the source file restores with its + // annotations intact. + if (sourceFilePath && data.filepath === sourceFilePath && savedPlanState.current) { + back(); + return; + } + // Clear web-highlighter marks before swapping content to prevent React DOM mismatch viewerRef.current?.clearAllHighlights();