Skip to content

fix(annotate): folder sessions export each comment once - #1696

Merged
backnotprop merged 3 commits into
mainfrom
fix/folder-annotate-duplicate-export
Oct 5, 2026
Merged

backnotprop merged 3 commits into
mainfrom
fix/folder-annotate-duplicate-export

Conversation

@backnotprop

@backnotprop backnotprop commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Target: 0.28.1

Bug

In a folder annotate session, every comment on the open document was exported twice: once under # Folder Feedback and again under # Linked Document Feedback … documents referenced in the plan. It goes back to at least 0.27.25 and affects every host, because the export is built in the browser.

Root cause

useLinkedDoc.getDocAnnotations() includes the active document's live state (the panel and the counts need it). Both export paths (annotationsOutput and getCurrentFeedbackPayload → buildCompleteAnnotateFeedback) exported the live annotations under the session heading, then exported getDocAnnotations() again under the linked heading. A folder session always has a document open, so it always duplicated. The same mistake hit any session submitted while a linked document was open. In plan review that also dropped the plan's own comments, because the stashed plan has no sourceFilePath.

Two more defects sat in the same path:

  • Weaker copy. The second copy came from exportLinkedDocAnnotations, a separate and weaker renderer. It sorted by blockId.localeCompare, so block-10 came before block-2. It had no [In diff content] label, no quick-label heading, tip or Label Summary, and no reply threading.
  • Self-opened root dropped. When a file session opened its own path from the root (a "Home" link), getDocAnnotations() overwrote that copy's cache entry with the stashed root. Its comments then vanished from the export while a linked doc was open. Main printed "User reviewed the document and has no feedback." for that case, shown below.

Fix

One split for every export path.

  • useLinkedDoc.getFeedbackDocuments() is additive. It returns { root, documents }: root is the stashed root while a linked document is open (otherwise null), and documents holds every other document once, by path.
  • A cache entry under the root's own path is a separate copy opened from the root. It is exported under its path, never dropped, and the counts already include it.
  • getDocAnnotations() is unchanged.

One section resolver. packages/editor/feedbackDocuments.ts → resolveFeedbackSections() is used by both annotationsOutput and the submitted payload.

  • The root, plus SSE external annotations (which belong to the session), goes under the session heading.
  • Every other document goes under its own path.
  • In a folder session that section is titled # Folder Document Feedback, with "The following feedback is on files in the reviewed folder, grouped by file." The plan and file headings are unchanged.

One renderer. renderAnnotationEntries in parser.ts is now what exportAnnotations uses, and exportLinkedDocAnnotations uses it too, one heading level down. sortAnnotationsInDocumentOrder sorts by block position; without blocks it compares ids numerically. labelSummaryBlock is shared as well.

No leading blank line. The payload no longer starts with \n when only secondary sections are present.

Plain plan review, linked-doc output: what changes

Plain comments, deletions and globals are byte-identical to main. This is pinned by a test that passes against main's parser.ts. Linked documents only gain fidelity they were missing:

  • Entries are in document order (block 2 before block 10).
  • Version-diff comments are labelled [In diff content] instead of a line label (they never had a real one).
  • Quick labels print as [Label] Feedback on: … plus the tip, and the document gets a ### Label Summary. Before, they printed as a plain > Label quote.
  • Replies nest under their parent's **Replies:** instead of being numbered as separate entries.
  • Quick labels no longer list skill references, which matches the root export.
  • If no primary section precedes it, the payload no longer begins with a blank line.

Counts: feedbackAnnotationCount is unchanged and now matches what is emitted. Each section's "N pieces" line counts what it prints, the same way the root export counts.

Feedback archive: the record's annotations array (the client's allAnnotations) was never duplicated. The record's feedback text was, and now isn't (see below).

Tests

  • packages/ui/utils/parser.linkedDocParity.test.ts (new): a 12-block document with comments on block 10 and block 2, a reply, a diff comment, and a quick label with a tip.
    • Checks document order with and without blocks, nested replies, [In diff content], the quick-label heading, tip and ### Label Summary.
    • Checks that a linked document's entries equal the root renderer's entries one level down.
    • Pins the plain comment/deletion/global format; that test passes against main's parser.
    • The five fidelity tests fail against main's parser.
  • packages/editor/feedbackDocuments.test.ts:
    • Folder session with comments on the open doc only, another doc only, and both.
    • The same 12-block fidelity case for the open folder doc.
    • The payload starts with # Folder Document Feedback (no leading newline).
    • Plain plan with a linked doc, submitted from the plan (unchanged) and with the linked doc open.
    • External annotations stay under the session heading.
  • useLinkedDoc.crossFile.test.tsx (already on the DOM_TESTS list):
    • The split holds each document once, before opening, while one is open, and after back().
    • The reviewer's self-link repro: openLoaded(ROOT_PATH) from the root, add s1, open another doc, go back. getFeedbackDocuments() now carries s1 and docAnnotationCount is 1.
  • bun test (6039 pass), bun run typecheck, and the DOM suites from test.yml (1465 pass; the Windows-step server tests in that file are excluded, since they are not DOM suites) are all green.

Headless verification

Built with bun run --cwd apps/review build && bun run build:hook, for main (b6702ed) and for this branch. Each run used bun apps/hook/server/index.ts annotate <dir>/ --json with a temp PLANNOTATOR_DATA_DIR and headless Chromium (--use-mock-keychain --password-store=basic).

Folder session. A seed session opened a.md (12 paragraphs) to record version history. Paragraph 4 was then edited. In the second session the reviewer:

  • toggled the version diff and commented on the changed block;
  • commented on paragraph 11 and then paragraph 3 (out of order on purpose);
  • added a 👍 Looks good quick label on paragraph 6;
  • added a global comment;
  • clicked Send Feedback.

Replies have no human-UI producer (only agents create them, through WebMCP or the external-annotations API), so the export tests cover them rather than the browser run.

Before (main): every comment appears twice. The second copy is in string order (line 21 before line 5) and has lost its diff label and quick-label heading.

# Folder Feedback

I've reviewed this folder and have 5 pieces of feedback:

## 1. [In diff content] Feedback on: "- Paragraph number 4 text for review.
+ Paragraph number 4 text, now REVISED with new detail."
> E2E-DIFF-COMMENT

## 2. General feedback about the folder
> E2E-GLOBAL-COMMENT

## 3. (line 5) Feedback on: "Paragraph numb"
> E2E-ON-PARAGRAPH-3

## 4. (line 11) [👍 Looks good] Feedback on: "Paragraph numb"

## 5. (line 21) Feedback on: "Paragraph numb"
> E2E-ON-PARAGRAPH-11

---

## Label Summary

- **👍 Looks good**: 1


# Linked Document Feedback

The following feedback is on documents referenced in the plan.

## /tmp/fold-e2e/docs2/a.md

I've reviewed this document and have 5 pieces of feedback:

### 1. General feedback about the document
> E2E-GLOBAL-COMMENT

### 2. (line 21) Feedback on: "Paragraph numb"
> E2E-ON-PARAGRAPH-11

### 3. (line 5) Feedback on: "Paragraph numb"
> E2E-ON-PARAGRAPH-3

### 4. (line 11) Feedback on: "Paragraph numb"
> 👍 Looks good

### 5. Feedback on: "- Paragraph number 4 text for review.
+ Paragraph number 4 text, now REVISED with new detail."
> E2E-DIFF-COMMENT

---

Archive record: 5 annotations; the paragraph-11 comment and the diff comment each appear in feedback 2×.

After (this branch): each comment appears once, in document order, at full fidelity, and the payload opens on its heading.

# Folder Document Feedback

The following feedback is on files in the reviewed folder, grouped by file.

## /tmp/fold-e2e/docs2/a.md

I've reviewed this document and have 5 pieces of feedback:

### 1. [In diff content] Feedback on: "- Paragraph number 4 text for review.
+ Paragraph number 4 text, now REVISED with new detail."
> E2E-DIFF-COMMENT

### 2. General feedback about the document
> E2E-GLOBAL-COMMENT

### 3. (line 5) Feedback on: "Paragraph numb"
> E2E-ON-PARAGRAPH-3

### 4. (line 11) [👍 Looks good] Feedback on: "Paragraph numb"

### 5. (line 21) Feedback on: "Paragraph numb"
> E2E-ON-PARAGRAPH-11

### Label Summary

- **👍 Looks good**: 1

---

Archive record: 5 annotations; the paragraph-11 comment and the diff comment each appear 1×.

Self-link (file session). index.md links to itself ([Home](index.md)) and to other.md. The reviewer clicked Home, commented on the opened copy, clicked Other, and submitted.

Before (main):

User reviewed the document and has no feedback.

After:

# Linked Document Feedback

The following feedback is on documents referenced in the plan.

## /tmp/fold-e2e/self/index.md

I've reviewed this document and have 1 piece of feedback:

### 1. (line 3) Feedback on: "Intro paragraph"
> E2E-ON-SELF-OPENED-COPY

---

Not changed / follow-up

In the self-copy state, getDocAnnotations() (the panel's All files view) still shows the stashed root under that path instead of the copy, though the counts include the copy. The export no longer depends on it. Fixing the panel is a separate change.

In a folder annotate session (and any session submitted while a linked
document is open) the open document's comments were exported twice: once
from the host's live state under the session heading, and again from
useLinkedDoc.getDocAnnotations(), which also carries the active document.
Plan review submitted with a linked doc open also dropped the plan's own
comments, since the stashed plan has no source path.

useLinkedDoc gains getFeedbackDocuments() (root once, every other document
once) and packages/editor/feedbackDocuments.ts resolves the export sections
once for both export paths (annotationsOutput and the submitted payload).
Folder sessions title the per-file section 'Folder Document Feedback'
instead of 'documents referenced in the plan'.
… a self-opened root copy

Review follow-ups for the folder duplicate-export fix:

- exportLinkedDocAnnotations now renders each document through the same
  entry renderer as exportAnnotations (renderAnnotationEntries): document
  order (block-2 before block-10), [In diff content], quick labels with
  their tip and a Label Summary, and threaded replies. Plain comment,
  deletion and global output is unchanged.
- getFeedbackDocuments() no longer deletes a cache entry under the root's
  own path: it is a copy opened from the root itself (an HTML Home link)
  with its own comments, which the counts include, so it is exported
  under its path.
- The payload no longer opens on a blank line when only secondary
  sections are present.
@backnotprop
backnotprop force-pushed the fix/folder-annotate-duplicate-export branch from b45b3fc to 4923927 Compare October 5, 2026 02:06
@backnotprop
backnotprop merged commit 273226d into main Oct 5, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant