Skip to content

Document comments: Google Docs–style comments for every file, Word and Excel in-file, assistant tools - #587

Merged
itsdestin merged 123 commits into
masterfrom
session/comments-mock-a
Sep 29, 2026
Merged

itsdestin merged 123 commits into
masterfrom
session/comments-mock-a

Conversation

@itsdestin

@itsdestin itsdestin commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Google Docs–style comments in the file viewer, on desktop, Android and the remote browser.

What users get

  • Comments on any file: select text (or an Excel cell, or code lines) → Add comment. Highlights with a hover card to reply or resolve in place; a Comments panel with Show resolved; edit and delete on every comment and reply (delete asks first, and deleting a thread's first comment deletes the thread).
  • Word and Excel comments live inside the file — real Word comments and Excel's modern threaded comments (old-style notes are left untouched and not shown), so they appear in Word, Excel and Google Docs. Every other file keeps comments in the project's .youcoded/comments/ sidecar.
  • The assistant works with comments: read, reply, resolve, reopen, add (sparingly) and move, in the native harness and in Claude Code sessions (MCP). It asks permission only for Word/Excel changes, at the same tier as Edit/Write (acceptEdits doesn't ask, plan mode always asks).
  • "Ask about this" chips in the composer and sent messages that light up the text they point at.
  • Live refresh of an open panel on desktop and remote, including Word/Excel files changed elsewhere.

Safety

  • Word/Excel writes are surgical (untouched parts byte-identical), with backup → fsync → atomic rename → re-read verify → rollback, and a refusal when the file looks open in another app (~$ / .~lock owner files).
  • Known-root gate + realpath containment on every channel and tool; symlinked targets are judged by their real file.
  • Claude Code's pending-mutation queue is authenticated by a per-session secret, ignores pre-planted request files, and applies only inside the watcher's verified project.
  • Zip-bomb, record-count and XML entity/DOCTYPE guards on both platforms; XML-illegal control characters stripped from comment text.

Parity and tests

  • Five-surface IPC parity (preload, ipc-handlers, remote-shim, remote-server, SessionService.kt) pinned by ipc-channels.test.ts.
  • Cross-platform golden fixtures: desktop and Kotlin must produce the same read-back for add/reply/resolve/reopen/move/edit/delete on Word and Excel, both directions, plus a staleness replay.
  • scripts/verify.sh --full green; Android ./gradlew test 591 tests × 3 build types, 0 failures, run in UTC (2026-09-28, after the review fixes below).

Review trail

Signed contract (27 rows) graded and accepted; per-task adversarial reviews, two final code reviews (desktop + Android), and a threaded-Excel design reviewed three rounds — all in the workspace repo under docs/active/reviews/ and docs/active/design/2026-09-24-doc-comments/.

Review fixes (2026-09-28)

Word run splits no longer drop tabs/breaks/images; xmlns:w14 declared before use; every save rejects a newly undeclared namespace prefix; saves stay compressed; Excel <legacyDrawing> placed in schema order (before <tableParts>/<extLst>); rollback works across drives; Word's shortened ~$ owner names recognised; Android caps one in-memory part at 32MB; the Android golden tests no longer depend on the machine's time zone.

Known limits (filed on the roadmap)

Phone panel doesn't refresh on its own; rolling backups never evicted; the open-in-another-app check can go stale after a Word/Excel crash; exact comment times not shown. Not verified in real Excel on this machine (checked against the spec and LibreOffice).

Companion workspace PR: itsdestin/youcoded-dev, branch session/comments-mock-a (design, contract, reviews, roadmap).

🤖 Generated with Claude Code

itsdestin and others added 30 commits September 24, 2026 01:17
Google Docs/Word-style comments: commented spans get a soft accent
highlight, a margin rail holds cards aligned to their highlights (or,
below 640px of the viewer's OWN width, small markers that open a
popover), a review bar shows the count and sends open comments to the
assistant as a batch. Comments are durable records — author, timestamp,
reply threads (including assistant replies), resolve/reopen by either
side, "Show resolved" reveals a faded history.

"Ask about this" is redesigned onto the same primitive: it attaches a
quote reference chip above the composer instead of scaffold text in the
textarea, and the same chip renders on the sent bubble. Comment state and
the composer's held references live in renderer memory only (a shared
useSyncExternalStore store + two window CustomEvents) — no IPC, no
persistence, no main-process change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two bugs found reviewing this mockup's own screenshots in the default
drawer (Session Files list open, ~230-490px pane):

1. use-container-narrow's ResizeObserver never attached: its host
   (ActiveArtifactView) has an early-return loading state that renders
   before the ref'd div exists, so the effect's one-shot null check bailed
   permanently. The review bar stayed "wide" forever, overflowing
   "Send to assistant" past the pane edge with no comment cards visible.
   Fixed with an rAF retry until the ref resolves.

2. Once narrow rendering did apply, its marker's stored vertical offset was
   measured before the narrow/wide switch's own multi-frame settle finished
   reflowing the document, landing ~600px below the highlight it belonged
   to. Fixed by observing the content column's own height (not a fixed
   frame count) and re-measuring on every reflow.

Re-verified against a workbench pointed at this worktree (not the shared
checkout) in three states: default (list open), list closed, and the
maximized panel — screenshots overwritten under
docs/active/design/2026-09-24-doc-comments/a/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…I only)

Destin, round 2: "the comment primarily be seen as highlighted text, with
the comment displaying on hover... comments mode should kinda be a
distinct mode entered by the user."

Reading mode (default when a file opens): no margin, no markers — full
width. A comment is a soft highlight; hover shows a read-only card
(author, time, note, reply count / resolved state, "Open in comments").
Click pins the same card. Selecting text shows a floating "Comment"
button; the popup it opens (or the same right-click "Add comment") saves
on Enter, newlines on Shift+Enter, cancels on Esc.

Comments mode: a header toggle (speech-bubble + count, pressed state)
switches to round 1's margin/markers rail; the review bar (Show
resolved, Send to assistant) only renders in this mode, so the two modes
read as visibly distinct, not an internal flag. "Open in comments"
jumps straight to the clicked thread, revealing it even if resolved.

Ask about this → inline pill, ported from session/comments-mock-c
(compose-ref.ts, TokenPill): a reference now rides INSIDE the composer's
sentence as an invisible marker with a real pill drawn over it by the
mirror layer, deletes as one unit (Backspace/Delete at its edge), and the
sent bubble decodes the identical marker back into the same pill. This
replaces round 1's QuoteReferenceChip row above the composer, and the
ChatMessage.references field it needed (the marker now rides in
`content` itself, so UserMessage needs no extra field to decode it).
"Send to assistant" from Comments mode now sends the open comments as
those same pills, each carrying its thread's id for the jump-back.

Verified against a workbench pointed at THIS worktree (not the shared
checkout), in the default drawer, the expanded panel, and a light theme;
screenshots under docs/active/design/2026-09-24-doc-comments/a2/.
docs/active/design/2026-09-24-doc-comments/a/ (round 1's shots) untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…overs, tidy toolbar

Selection release in the file viewer now opens the SAME right-click menu
(build-menu.ts's buildContextMenu) instead of a lookalike floating button,
anchored at the selection's end and gated on drag/tiny-selection/dismissal.
The new-comment popup and the hover card both move onto anchor-position.ts's
shared placement arithmetic (extended with an optional bounds host) so
neither can land over the viewer's header again. The in-document highlight
drops its 2px ring (read as a focus bug) for a stronger accent tint on
hover/active, with no outline. Comments-mode's toolbar collapses two nested
bars into one, gives the mode toggle a text label, and switches "Show
resolved" to a FilterChip that always carries its own label. Entering
Comments mode in the drawer's default (narrow) width now reuses the
drawer's own Expand control instead of falling back to a bottom sheet that
covered the composer, and restores it on exit. The reference pill gets a
message/file glyph and a solid-panel "on-accent" tone so it reads on any
theme's accent color. CommentCard's avatar/name mismatch ("You" shown as
"D") is fixed and de-duplicated into one Avatar component; the reply row
moves to a single TextInput + Reply + Resolve line. Dead
SelectionCommentButton deleted.

Also fixes two pre-existing (unrelated) test failures found while running
the full suite: ActiveArtifactView's new optional ArtifactContext reads
needed the Optional hook variants so tests with no provider don't throw,
and shared/types.ts had already grown 6 lines past its line-budget entry
before this session touched it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…earance, toolbar sizing, underline

Four defects from the a3 review: the Comments-mode auto-expand effect
depended only on commentsMode, so it fired before narrowPane's
ResizeObserver had settled and silently never expanded (reproduced with the
file list both open and closed) — now depends on narrowPane/drawerExpanded
too, guarded by the same ref so it still fires only once. CommentCard's
reply row stacks the TextInput full-width with Reply+Resolve right-aligned
below it, since three controls never fit the margin's 256px card regardless
of input min-width. NewCommentPopover now places into a bounds host whose
rect has the floating Edit FAB's footprint carved off the bottom, so the
popup can no longer land on top of it. "Show resolved" swapped from
FilterChip (pinned at text-sm, taller than the Comments toggle beside it)
to the same Button/size/ring recipe as CommentsModeToggle. The highlight
mark gets an accent-tinted underline so it reads as "commented", not a
plain text selection, in every theme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… or paragraphs

Quote matching searched one text node at a time, so only comments inside a
single run of plain text got a highlight; a real selection across a bold
word, link, list item or paragraph break silently got none. Match across
all text nodes ignoring whitespace and wrap one <mark> per node touched,
all sharing the comment id; hover, active state and card anchoring now
span every segment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t the foot of the comment pane

Destin, round 4: the Comments mode toggle joins the viewer header's icon
row (imperative handle + state callback, the Edit/Save pattern), and the
toolbar strip above the document is gone. 'Send to assistant' becomes an
'Ask Your Assistant' button with the ask sparkle, sticky at the bottom of
the comment margin alongside Show resolved. ProjectView, which has no
such header, keeps a one-button toggle row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rs cards

The Ask Your Assistant footer was sticky inside the scrolling margin, but
that column was only one screen tall, so after scrolling it drifted up and
sat on top of a comment card. It is now overlaid on the margin's bottom edge
from outside the scroller (clear of the scrollbar), with extra bottom room
so the last cards scroll past it. An inner min-h-full row makes the margin
and its divider run the whole document length. The floating Edit button
hides in Comments mode so it can't cover the footer; the code-file rail
gets the same footer under its own list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the comment pane

Destin, round 5: the Comments toggle moves from the header icon row into
the floating Edit cluster, left of Edit, in the same pill shape. In
Comments mode, Show resolved and Ask Your Assistant float above that row
over the comment column as pills, with no bar or panel of their own. The
cluster ignores the pointer between pills. Comments stays visible when the
file list is open (Edit keeps its own hide-with-list behaviour).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin, round 6. The whole floating cluster now shares Edit's pop: in when
the file list folds away, out when it reopens, always shown while editing.
The faded cluster is inert, since each pill re-enables pointer events.
Clicking Comments folds the list away, as Edit does, so Comments mode's
buttons are on screen as it opens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin, round 7: 'review this against existing app ui'. Against the guide:
- Comments stays beside Edit in Comments mode (Edit no longer hides)
- Show resolved / Ask Your Assistant are full-width Button primitives,
  one primary (G-1, G-4, G-28), aligned to the comment card width
- comment column is the side-pane surface (panel); cards are inset with an
  edge-dim border and no shadow (2.1, 2.4); resolved cards no longer fade
  to 70% opacity (contrast)
- timestamps 11px, not 10px (G-5)
- hover card and new-comment popup use OverlayPanel, the shared popup
  surface the right-click menu uses, so glass themes match
- 'Open in comments' is a ghost Button, not an underlined raw link (G-1)
- counts are a muted numeral after the label, not accent badges (G-19, G-8)
- card padding 12px (2.4)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… solid fill

Destin, round 8. In Comments mode the Comments + Edit pair moves left past
the 256px comment column so it floats over the document, not the
comments. The comment actions keep their own anchor at the foot of the
column, and both anchors share the one pop-in group. Show resolved uses
Button's 'raised' variant (solid panel fill + edge border, the primitive's
variant for a control on top of content) instead of transparent
'secondary'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…omments mode

Destin, round 9. The hover card drops its reply-count/'No replies yet' line
and the 'Open in comments' button. It is a non-interactive tooltip shown
only while the pointer is on the highlight, with no pinning. Clicking a
highlight, or a sent pill for it, opens Comments mode on that thread.
Entering Comments mode by any route folds the file list away, so the
floating comment actions are never hidden behind it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pane-bound actions

Destin, round 10:
- Resolve is the Resume card's CompleteToggle (hollow circle-check, filled
  when resolved, click again to reopen) at each card's top right; the old
  Resolve/Reopen buttons are gone. CompleteToggle gains optional hover copy
  (titles); the Resume wording is unchanged when omitted.
- The reply field is an InputGroup with Send inside it on the right.
- Show resolved / Ask Your Assistant render only while the full comment
  column is on screen (Comments mode, pane at least 640px so the margin is
  not the marker rail). They are no longer tied to the file list's pop-in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… by the real scrollbar

Destin, round 11:
- Comment cards no longer repeat the quoted text. The resolve toggle sits
  on the author row, top right. On code files, clicking a card's background
  jumps the editor to its line (the quote used to be that target).
- The comment column sits left of the document scrollbar, but the floating
  buttons were placed from the pane edge, so they sat one scrollbar-width
  too far right. ActiveArtifactView measures the scroller's scrollbar
  (data-comments-scroller, ResizeObserver) and SessionDrawer offsets
  Show resolved / Ask Your Assistant and Comments / Edit by it. Verified
  with scrollbars visible: the buttons match the card edges exactly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin, round 12: 'send button looks too big for the container'. The sm
field is 28px tall and a text sm button 22px, leaving ~3px above and
below. icon-sm (20px) with the composer send button's arrow leaves an even
4px on every side, matching InputGroup's right inset (measured 4/4/4).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es its highlight

Destin, round 13. The wide comment column is now a top-down list in
document order with its own scroller, rendered beside the document
instead of inside it, so scrolling a long document doesn't carry the list
away. Clicking a card's background, or its highlight, selects that thread:
the document scrolls to the highlight, the list scrolls to the card, and
both stay lit until another is picked. The narrow marker rail still sits
level with its highlights inside the document scroller. Comments/Edit no
longer add a scrollbar offset, since the list's scrollbar is inside the
column.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin, round 14: 'gets too close to edges of the outer container'.
icon-xs (16px), and the reply field's right inset goes from 4px to 6px,
leaving about 6px on every side (measured 6/6/6).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin, round 15: 'i want to see a few different ways to style/frame/place
the side panel'. A design-review selector (?commentsPane=, forwarded by
WorkbenchFrame) picks the framing: column (today), sheet (rounded bordered
panel inset from the edges), margin (no chrome, cards on the document
background), or titled (the Session Files pane's title row with 'Comments N'
and Show resolved). Floating actions and Comments/Edit now position from
the measured card list and pane edges (data-comments-list /
data-comments-pane), so they line up in every framing and with any
scrollbar. Measured: buttons match the card edges in all four.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…solved switch

Destin, round 16. The new default framing combines sheet's rounded,
bordered, inset panel, column's full 256px width and titled's title row
('Comments N'). Show resolved is the Resume browser's Show Complete switch
(same label recipe, shared Toggle) in that title row, so only Ask Your
Assistant floats. The floating actions' bottom is measured from the
pane's bottom edge (9px inset, matching the cards), and Comments/Edit
align to it. The earlier framings stay reachable via ?commentsPane= for
comparison.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin, round 17: 'remove the number next to comments'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin's questions deck (Q-4) and Excel follow-up: Word and Excel comments
work like any other file's. The markdown viewer's highlights/comment-pane
layout moves into a shared CommentableDocument used by MarkdownView,
DocxView and XlsxView. Word text highlights like markdown; a spreadsheet
comment attaches to a CELL (Excel's corner triangle, hover card, right-click
Add comment, cell reference on the card). Colleague authors ("Priya Shah")
for comments that came from the file. New workbench fixture: a real .docx
with real Word comments (fixtures/docs/make.mjs). Show Resolved label raised
to the 11px floor (G-5).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin's questions deck Q-3 (phone support now; tap opens the comment). At
phone width the single-comment sheet was drawn inside the file drawer, so the
composer, quick chips and status bar covered its reply box; it is now portaled
to <body> like Dialog. A long-press selection no longer stacks a second copy
of the right-click menu on the one ContextMenuHost already opened.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Spreadsheet fixture renamed to reports/q3-sales-by-rep.xlsx: the default
  file list may not show the site scenario's Q3-sales.xlsx (mock-shim.test).
- Comment boxes mark themselves for the editable right-click menu with
  data-edit-menu instead of a bare class the design lint rejects on
  <Textarea>; the Projects-screen fallback toggle swaps variant instead of
  hand-adding a ring; the phone sheet's max height is on the scale.
- doc-comments-store helpers used only via useDocComments are no longer
  exported (knip).
- use-container-narrow disconnects in the form observer-ref-returns-cleanup
  reads.
- Line budgets raised, reviewed: globals.css +18 (the spreadsheet cell
  comment mark) and SessionDrawer.tsx 1591 (the floating Comments/Edit
  cluster and comment-pane actions from rounds 5-17; revisit when the
  mockup-only framings are deleted in the build).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin: pull the mouse down onto a highlight's hover card and reply there
without opening Comments mode. The card now takes the pointer, shows the
thread's replies and the same reply box as Comments mode (extracted to
ReplyField). While a reply is being typed it stays open until Esc or a
click outside.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin: add the resolve button to the preview. Same circle-check, same
top-right spot as a Comments-mode card. Resolving releases the
typing-a-reply hold, since the card unmounts without a blur.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…g it

Destin: on pages where selecting text pops the menu, right-clicking
opened a second copy on top. The auto menu now reacts to the left
button only, and a right-click closes it so the right-click menu is the
only one on screen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin: the cursor landed in the wrong place after Ask about this, the
chip looked out of style, and he disliked the icon. The composer held the
full encoded reference (150+ invisible characters) under a short drawn
pill, so the caret was measured against text nobody could see. The
composer now holds a display-sized token (brackets + zero-width key +
label) that the mirror draws character for character; the full marker is
produced only on send. Caret snaps out of a chip, arrows step over it,
Backspace/Delete remove it whole. Chip styled after TagChip in the accent,
no icon or paragraph mark, same look in the sent bubble.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Destin: clicking an Ask-about chip should focus/highlight the text it
came from, and hovering should too. Hovering a chip (composer or sent
bubble) washes the source text in an open viewer; clicking scrolls to it
and flashes it, opening the file first if it is closed (pending jump,
taken once the content loads). Painted with the CSS Custom Highlight API
like the find bar, so it never fights the comment marks. Refs now carry
the quote (or cell) to find.

globals.css budget 2954 -> 2965: the two ::highlight rules and their
comment. The two ChatView tests' ArtifactContext mocks gain
useArtifactStoreOptional, which UserMessage's chips now read on click.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Chips sent before refs carried the quote had nothing to search for, so
hover/click did nothing on them. Fall back to the label's text, which is
a prefix of the source.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
itsdestin and others added 27 commits September 27, 2026 21:07
…T20)

T9c: deploy the byte-identical doc-comments-mcp.js asset per Claude Code
session (ClaudeCodeDocCommentsMcp.kt), with its own per-session random
server id + secret token, mirroring desktop's deployClaudeCodeDocCommentsMcp
exactly (adversarial review findings #1/#2). PtyBridge.start() now combines
SendUserLink's and the doc-comments server's flags into ONE --mcp-config/
--allowedTools occurrence, since Claude Code's CLI treats a repeated
single-value flag as last-value-wins (the same fix desktop's own
session-manager.ts already made).

T20: a Kotlin coroutine polling loop (DocCommentsPendingQueue) applies a
queued docx/xlsx mutation through the SAME dispatch functions the
docComments:* bridge uses, honoring desktop's exact security properties:
the applier never trusts a request's own projectRoot, every request must
carry the session's own per-deployment token (constant-time compared),
freshness-checks a pre-planted file, refuses an unknown/missing kind, and
sweeps orphaned result files.

Permission parity: HookEvent.PermissionRequest now carries Claude Code's
own live permission_mode (already forwarded by hook-relay-blocking.js but
previously unread), and DocCommentsPermission.shouldAutoApproveDocComment
mirrors desktop's permission-auto-approve.ts — a plain-text/markdown/code
target is auto-approved unconditionally, a Word/Excel target only in an
already-frictionless mode, and a symlink disguising a native format never
gets a free ride through either branch. Wired into ManagedSession.kt's
existing PermissionRequest handling.

Tests: Kotlin suites mirroring desktop's pending-mutation-queue.test.ts and
permission-auto-approve.test.ts (round-trip add/move, wrong/missing/shared
token, unknown kind, pre-planted/cold-start freshness, stale-result sweep,
Word/Excel vs plain-text, every permission mode) plus a desktop-side test
comparing the embedded DOC_COMMENTS_SERVER_JS byte-for-byte against the
Android asset, the same shape as claude-code-mcp.test.ts's existing
SendUserLink parity guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…x reverse direction + staleness self-check

- xlsx cross-platform golden parity was entirely missing before this: T12's
  desktop test and T18's Kotlin test each hand-wrote their own expectations
  against the same two real files (docling, elden), which could agree by
  construction without either implementation's actual output ever being
  cross-checked. Wired both into a shared golden JSON
  (generate-xlsx-golden.mjs extended to cover them) and added the
  corresponding Kotlin assertions (XlsxCommentsTest.kt), including the real
  B19 five-independent-threads case.
- Regenerating that golden caught a real, live drift: the committed
  q3-sales-by-rep.json/chartsheet-workbook.json still reflected the RETIRED
  legacy-Notes reader's output from before the 2026-09-27 threaded-comments
  redesign. Fixed by regenerating against the current reader (now correctly
  zero comments) -- exactly the failure class T21 exists to catch.
- New xlsx write-sequence goldens (add-docling, move-docling,
  resolve-b19-sibling, elden-sequence: add->reply->resolve->reopen->move
  cross-sheet) plus XlsxCommentsCrossPlatformParityTest.kt, mirroring the
  existing docx parity test's structure.
- staleness self-check (doc-comments-write-golden-staleness.test.ts): replays
  every committed write-golden recipe (docx + xlsx) against desktop's CURRENT
  writer and asserts the fresh output still matches the committed golden --
  fails loudly on drift instead of only the next manual regeneration.
- reverse direction (doc-comments-kotlin-write-golden.test.ts): a Kotlin
  writer produced an add->reply->resolve->move sequence once (disposable
  generator, run by hand, deleted after), committed under
  shared-fixtures/doc-comments/kotlin-write-golden/, and desktop's real
  reader is proven to read it correctly -- the "same in reverse" half T21's
  design explicitly calls for.

Verified: desktop scripts/verify.sh full suite green. Android doc-comments
Kotlin tests (163 tests across DocCommentsBridgeTest, DocCommentsDispatchTest,
DocCommentsGateTest, DocCommentsPendingQueueTest, DocCommentsPermissionTest,
DocCommentsStoreTest, DocxCommentsCrossPlatformParityTest,
DocxCommentsTest, DocxCommentsWriteTest, XlsxCommentsCrossPlatformParityTest,
XlsxCommentsTest) verified green in an isolated worktree at this branch's
last commit, since a concurrently-edited, unrelated file
(ClaudeCodeDocCommentsMcp.kt/-Test.kt, another agent's in-flight T20 work)
currently fails to compile in the shared worktree -- out of this task's
scope, left untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… for #1

Review: docs/active/reviews/2026-09-27-doc-comments-t9c-t20-review.md
(triage appended there, not committed here per instruction).

Finding #1 (rejected as filed, cheap hardening kept): the pending-mutation
queue's per-session token was never a boundary against same-uid code inside
an already-live session (that code can already rewrite the target file
directly, no token needed, and would hit an ordinary permission prompt in
default mode anyway). Added a WHY note beside the token's own generation on
both platforms explaining exactly what it does and doesn't defend against,
and owner-only permissions (0700 dir / 0600 config, Bootstrap.kt's own
~/.netrc technique on Android, chatgpt-request-diagnostics.ts's mkdir+chmod
shape on desktop) as real-but-narrow hardening against a rooted device or a
backup tool that bypasses the app's own permission model entirely.

Finding #2 (fixed): ClaudeCodeDocCommentsMcp.deploy() no longer takes a
mobileSessionId parameter — its directory is now keyed on the same freshly-
minted serverId desktop already uses, so no caller mistake can reintroduce
a fixed, collision-prone path.

Finding #3 (fixed, both platforms): every deploy directory is deleted on
session-exit (Android: PtyBridge.stop(); desktop: ipc-handlers.ts's
doc-comments-mcp-attached/session-exit wiring, extracted to
desktop/src/main/doc-comments/session-lifecycle.ts to keep ipc-handlers.ts
under its own line budget). A crash/kill that skips that delete is caught
by a once-per-process startup sweep on the next deploy on both platforms.

Tests: Kotlin tests for the new directory-naming (no fixed fallback),
permission hardening, and sweep behavior; TS tests for permission hardening,
deployDir, the sweep, and the session-lifecycle wiring's cleanup — moved out
of ipc-handlers.test.ts into its own file alongside the extracted module.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop review (2026-09-27):
- F1: forward an xlsx Move's fresh id through the pending-mutation queue
  and MoveCommentTool's result text, and the MCP script's MoveComment
  handler (both the desktop-embedded copy and the byte-identical Android
  asset) — a follow-up call on the same comment now avoids the full-
  workbook fallback scan the fix exists to avoid.
- F2: zip-size-guard's decompressBounded now reports a genuine stream
  error as 'decompress-failed', distinct from 'archive-too-large' (real
  size overflow only); docx/xlsx readers map it to their own existing
  'invalid-docx'/'invalid-xlsx' codes instead of mislabeling corruption
  as an oversize refusal.
- F3: measured, not fixed — real fixture data (elden-ring, 315 comments
  on one sheet) and a synthetic 1,000-comment stress pin both cost well
  under a second of CPU to mount, a one-time per-file-open cost, so no
  virtualization is needed; CommentsMargin's header now records the
  measured bound instead of an unverified "handful per file" guess.

Android review (2026-09-27):
- F1: desktop's IPC and remote-server ADD handlers now validate a
  docComments:add selector the same way Android already does (missing/
  malformed selector refuses with missing-field, matching Android's own
  leniency on inner fields) — all three platforms agree.
- F2: fixed the stale WHY comment on moveNativeXlsxComment, and closed
  the parity gap it revealed — Android's own pending-mutation queue now
  also forwards an xlsx move's fresh id, matching desktop's F1 fix.
- F3: DocCommentsStore.mutateSidecar now tells a write-time IOException
  (disk full, failed rename) apart from a read-time one (an unreadable
  existing sidecar) — the former reports a new SIDECAR_WRITE_FAILED
  code instead of the misleading SIDECAR_CORRUPT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nstead of a fixed CPU ceiling

The fixed 5s ceiling measured ~1.6s alone but 5.08s in a loaded full-suite run.
The ratio (measured 6.4x; linear is 5x) cancels out machine load and still
catches a per-comment cost blow-up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review-deck V-1/V-3 needed to shoot a real docComments.add failure
(generic, and the specific "file looks open elsewhere" code) and V-2
needed a comment whose quoted text the real resolver can't find. The
existing ?fail=docComments.add switch only throws a generic string
describeError can't map to a specific message, so it could never show
V-3's exact wording. Adds ?docCommentsFail=<code> (fails add with a
named MutationResult error code) and ?docCommentsDetached=1 (one extra
seed comment whose quote is deliberately not a substring of its
fixture, so the real client-side resolver marks it detached on its
own). Workbench-only, inert unless the param is set — never product
code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved:
- app/.../runtime/ManagedSession.kt: kept master's refactored single-arg
  shouldAutoApprove(toolName, toolInput, overrides) (now excludes
  ExitPlanMode too), with this branch's docCommentsApprove short-circuit
  still checked first.
- desktop/line-budgets.json: re-measured every listed file with `wc -l`
  post-merge and set each budget to its real current count; dropped
  ChatView.tsx (now under the 1500 default) per the file's own policy.
- desktop/src/renderer/components/MarkdownContent.tsx: combined both new
  imports (branch's ANCHOR_SKIP_ATTR, master's streaming markdown-blocks).
- desktop/src/renderer/components/artifact-views/ActiveArtifactView.tsx:
  kept branch's two-pane layout (viewer + CodeCommentsRail) and added
  master's ScreenMark inside the Suspense boundary.
- desktop/src/renderer/components/pages/PageCreateDialog.tsx: kept
  master's screen= prop plus branch's request.subtitle fallback.
- desktop/tests/LocalModelsSection.test.tsx: both sides independently
  fixed the same poll-race flake; kept branch's simpler always-safe
  landed-flag design (used consistently by every later call site in the
  file already) over master's opt-in holdLater/reveal() version.

Also fixed, found by the post-merge full verify run:
- Two new lint:design warnings the merge combined past the 542 ratchet
  (ReplyField.tsx's InputGroup padding override -> margin on the Button
  instead; CommentsFloatingActions.tsx's solid-accent class -> added as
  an explicit Button contract exception in .oxlintrc.design.json).
- Confirmed four other full-suite failures (CommentsMargin, ReadingHighlights,
  busy-app-render-budget, docx-comments/MarkdownContent CPU-budget tests)
  are load-driven, not logic bugs: each passes alone, and a clean re-run
  of the full suite passed all 927 files with no source changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…load-proof

Stress pins compare two sizes measured in the same run (best of 3) instead of a fixed CPU ceiling; ReadingHighlights needed its own 100-vs-1,000 bound (its cost genuinely grows faster than CommentsMargin's). The real 201 MB zip bomb and the two random-streaming markdown tests get named wall-clock budgets so verify.sh's parallel load cannot time them out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…or a fresh code-file draft

Bug 1: CommentCard's resolved branch truncated the note to one line and
dropped every reply, so a resolved thread's replies were unreachable in
"show resolved" view. Now shows the full text and every reply (muted),
still says "Resolved by ...", keeps the filled toggle to reopen, and
offers no reply box on a resolved thread. The hover card, narrow popover
and phone sheet all reuse this same component, so they're fixed too.

Bug 2: selecting text in a code file (e.g. total.py) and using "Add
comment" wrote the draft into the store but nothing appeared, because
CodeEditorView never wires up commentsMode/onOpenComments (by design --
see types.ts) and its own draft card only exists inside CodeCommentsRail,
which ActiveArtifactView renders only while commentsMode === 'comments'.
ActiveArtifactView now switches to Comments mode itself whenever a fresh
draft (focusId) appears on a code file while still in Reading mode, the
same way clicking the Comments button would.

Tests: CommentCard.test.tsx pins a resolved comment with 2 replies
rendering both reply texts. ActiveArtifactView.test.tsx pins the mode
switch for a .py-equivalent code file's fresh draft (red without the
fix, confirmed before committing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the draft actually appears

The code editor stamped the file path but not the project, so "Add comment"
filed the draft under "no project" while the Comments rail looked in the
file's project: the draft existed but nobody could see it. Found by probing
the dev instance; the earlier mode-switch fix was right but never triggered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the same mini popover

BUG — the chat input sometimes stole focus mid-comment. CommentCard's isDraft
flag was recomputed from the LIVE comment.text on every render, so the very
first keystroke (making text non-empty) flipped the JSX from <Textarea> to a
plain <p>. React unmounts on that element-type change, dropping DOM focus
with nothing focused — the next keystroke then had nothing to skip in
InputBar's global auto-focus listener, so it grabbed focus into the chat
composer and that character (and every one after) landed there instead.
isDraft is now local state, captured once per comment id and cleared only on
a real blur with real text — never by a keystroke or an unrelated store
update (a docComments:changed echo, a re-anchor pass). Regression tests in
CommentCard.test.tsx type through a harness that mirrors CommentsMargin/
CodeCommentsRail's own re-render-on-every-keystroke wiring.

CHANGE — code files now show the same small floating comment box markdown
files do. ActiveArtifactView used to force-switch into the whole Comments
panel the instant a fresh code draft appeared (CodeEditorView never wired up
commentsMode itself). New CodeCommentPopover renders NewCommentPopover
anchored through CM6's own coordsAtPos/selection instead, in Reading mode,
with no panel switch — existing line markers for persisted code comments are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…licitly, not on a keystroke debounce

Cause: a fresh comment draft's text was persisted by a 400ms keystroke
debounce. The first time it fired, persistNewComment() removed the draft's
id from pendingLocalIds (the "not yet on the server" set). Comments have no
edit-after-add IPC, so every keystroke typed after that point stayed
local-only — and the next unrelated docComments:changed refresh (a
whole-array replace from disk) overwrote the box with the truncated saved
prefix, even mid-typing. On disk this showed up as prefixes of what was
actually typed ('h', 'hiiiii ', 'cur', 'hiii as').

Fix: typing (setCommentText) only ever updates local state now; a draft is
persisted only by an explicit commit (commitDraft), fired by Enter/"Comment"/
click-away (NewCommentPopover, via clearFocus) or a real blur-with-text on
the panel's own inline draft (CommentCard, wired from CommentsMargin/
CodeCommentsRail — spreadsheet-cell and margin-created drafts had no other
commit path). pendingLocalIds now stays true for the WHOLE composition, so
a mid-typing refresh can never see (let alone overwrite) text the server
doesn't have yet, and Cancel/Delete on an uncommitted draft sends no IPC.

Also fixes a related loss: closing the last viewer of a file used to
discard ANY still-uncommitted draft outright, including one with real
typed text (previously masked by the debounce firing within ~400ms in most
cases). It now commits a non-empty draft instead, same as an explicit
commit, and only discards one that's still empty.

Tests: added/updated in use-doc-comments.test.tsx — typing never persists
mid-debounce, committing after a paused mid-typing pause sends the full
text, a docComments:changed push mid-draft leaves the draft's text alone,
Cancel sends no IPC, and closing the last viewer commits (not discards) a
non-empty draft. Verified red against the pre-fix store in a scratch copy
(8/33 failures), green after restoring the fix (33/33). Full
scripts/verify.sh passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds docComments:edit / :edit-reply / :delete / :delete-reply on the
desktop main process, the remote WS relay and the renderer IPC surfaces
(preload, remote-shim, workbench mock-shim), across all three comment
targets:

- Plain sidecar (doc-comments-store.ts): text overwrite / array-filter
  under the existing mutateFileUnderLock, no "edited" marker.
- Word (docx-comments.ts): edit replaces a <w:comment>'s own paragraph
  text, keeping author/date/id/paraId; delete removes the root's
  document.xml anchor, the root, every w15:paraIdParent-chained reply,
  and their commentsExtended/commentsIds/commentsExtensible entries;
  delete-reply removes one reply only. Same backup/atomic-write/verify/
  rollback pipeline every existing mutation already uses.
- Excel (xlsx-comments.ts): edit overwrites a threadedComment's <text>
  and rebuilds the legacy placeholder; delete reuses the existing
  thread-removal helper plus a new cleanupEmptyCommentPartsIfNeeded
  step that strips comments/vmlDrawing/threadedComment parts, their
  rels, content-type overrides and <legacyDrawing> when a delete
  leaves the sheet with zero comments of any kind (never touching a
  genuine Note).

WHY no author check and no edited marker: anyone's comment/reply can
be edited or deleted, and no "edited" indicator is stored or shown —
per doc-comments.edit-delete.questions.answers.json. Deleting a
thread's first comment deletes the whole thread on every format.

The assistant gets no new tool — this is the human-facing IPC surface
only (native/MCP doc-comments tools are untouched).

Bumps five line-budgets.json ceilings for the reviewed growth; design
doc gets a new dated §11 "Edit and delete" section describing the
build and the one deliberately-skipped follow-up (extending the T21
golden-fixture cross-platform-parity suite to these four ops).

Tests: doc-comments-store.test.ts, docx-comments.test.ts (incl. a
structural "no dangling range markers" pin), xlsx-comments.test.ts
(incl. a from-scratch last-comment-cleanup pin and a five-independent-
threads-on-one-cell delete), doc-comments-ipc-handlers.test.ts,
doc-comments-remote-relay.test.ts, mock-shim-doc-comments.test.ts,
ipc-channels.test.ts (channel parity — Kotlin arm lands separately).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-1..E-6 (docs/active/design/2026-09-24-doc-comments/doc-comments.edit-
delete.questions.answers.json): small Edit/Delete icons next to the Resolve
toggle on every comment and reply (hover-revealed on desktop, always visible
on touch via .touch-reveal/.coarse-hit), anyone's comment/reply can be
edited or deleted, deleting a thread's first comment deletes the whole
thread, delete confirms inline first, no "edited" marker, the assistant gets
no edit/delete tool.

- CommentActions.tsx (new): shared Edit/Delete icon buttons, the inline
  textarea editor (Enter saves, Escape cancels, focus-safe the same way the
  recent draft fix is), and the inline delete-confirm block, reused by
  CommentCard and HighlightHoverCard so the two surfaces can't drift.
- doc-comments-store.ts: editComment/editReply/deleteComment/deleteReply,
  optimistic update + rollback + inline <ErrorState>, calling the new
  window.claude.docComments.edit/editReply/delete/deleteReply IPC methods
  (channel names/payloads agreed with the concurrent main-process/Kotlin
  build, landed in c980784).
- CommentCard.tsx, HighlightHoverCard.tsx: wire the icons, inline editor and
  delete confirm into both the open/resolved comment branches and every
  reply row.
- CommentsMargin.tsx, CodeCommentsRail.tsx, ReadingHighlights.tsx: wire the
  new store mutations through to the cards.
- Tests: comment-edit-delete.test.tsx (UI), use-doc-comments.test.tsx
  (store mutations, in-flight-reply edge case, rollback).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors the desktop TS edit/delete build (2026-09-28, session
comments-mock-a) 1:1 in Kotlin, function-for-function:

- DocCommentsStore.kt: editComment/deleteComment/editReply/deleteReply
  on the plain JSON sidecar (no author check, no "edited" marker,
  deleting a thread's first comment deletes the whole thread).
- DocxComments.kt: editDocxComment/editDocxReply/deleteDocxComment/
  deleteDocxReply plus shared helpers (replyOrdinalFromId,
  replaceCommentParagraphText, buildExtendedMap, collectReplyEntries,
  removeElement, removeExtendedEntry, removeCommentExtensionEntries).
  Delete-thread strips the root's document.xml range/reference markers
  and every reply chained via w15:paraIdParent, refactors
  nextReplyOrdinal onto collectReplyEntries.
- XlsxComments.kt: editXlsxComment/editXlsxReply/deleteXlsxComment/
  deleteXlsxReply plus shared helpers (replyOrdinalFromId,
  setThreadedCommentText, rebuildPlaceholderForRoot, personDisplayName,
  cleanupEmptyCommentPartsIfNeeded). Adds WriteArchive.removedPartNames
  so a genuinely-removed part (a sheet's last comment) is skipped by
  serializeArchiveToFile instead of being streamed back from the
  read-only source ZipFile — the Kotlin-side equivalent of desktop's
  JSZip `zip.remove()`.
- DocCommentsDispatch.kt: editNative*/deleteNative* wrappers for both
  formats, through the same resolveNativeWriteTarget gate every other
  native mutation uses.
- DocCommentsBridge.kt: four new docComments:edit/:edit-reply/:delete/
  :delete-reply branches in handleDocCommentsMessage, same
  field-validation/gate/dispatch shape as reply/resolve/reopen/move.
- SessionService.kt: appended the four new channel labels to the
  existing combined docComments:* `when` arm (after move, before
  watch/unwatch), matching desktop's ipc-channels.ts ordering.

Tests mirror the TS suites' new describe blocks: DocCommentsStoreTest,
DocxCommentsWriteTest (incl. a structural no-dangling-markers/
rels/content-types pin), XlsxCommentsTest (incl. a from-scratch
last-comment-cleanup pin and the elden five-threads-on-one-cell case),
DocCommentsBridgeTest and DocCommentsDispatchTest.

Verification: `./gradlew test` (570/570 across debug/release/
releaseTest, 0 failures), `./gradlew assembleReleaseTest` (BUILD
SUCCESSFUL), `npx vitest run tests/ipc-channels.test.ts` (381/381,
including all docComments:* edit/delete parity checks and the
"are real Kotlin implementations" pin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause: a bare <button>'s UA stylesheet sets cursor:default, which
OVERRIDES inheritance rather than falling back to it. The card
background under Resolve/Edit/Delete already carries cursor-pointer, so
the ~2px gap-0.5 slivers between the tightly-packed icons showed pointer
(inherited) while the buttons themselves showed default (UA override) —
every button/gap crossing while sweeping the row flipped the OS cursor
glyph, reading as flicker/stutter (Destin, dev instance).

Fix: explicit cursor-pointer on every button (CommentActions.tsx's
ICON_BUTTON, SessionCardDetails.tsx's CompleteToggle) AND on every
gap-only wrapper div in the row (EditDeleteButtons' own wrapper,
CommentCard's topRightActions, HighlightHoverCard's trailing row) so
nothing in the row can disagree with anything else regardless of what
an ancestor's cursor happens to be. Covers every surface that reuses
these shared components: margin cards, the code rail, replies, and the
Reading-mode hover card.

Added regression coverage in comment-edit-delete.test.tsx pinning the
cursor-pointer class on every button and gap, plus a same-id-refresh
check that the row's DOM nodes survive a mid-hover store update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, arrow-send for new comments

Three requested follow-ups on the doc-comments Edit/Delete build, in
CommentActions.tsx, CommentCard.tsx and NewCommentPopover.tsx:

1. InlineEditField (CommentActions.tsx): Cancel/Save move inside the
   field's own border, bottom-right — one bordered box (textarea on top,
   buttons at the bottom) instead of a border on the textarea plus a
   separate row below it. Same "border on the wrapper, field goes bare"
   structure InputGroup uses, stacked instead of inline because this row
   carries two buttons, which is why it isn't built through InputGroup
   itself (its own §11.9 sub-rule limits it to one submit action).

2. CommentCard.tsx's draft state: removed the bottom "Delete" button.

3. New comments (CommentCard.tsx's draft textarea, and NewCommentPopover.tsx
   for text/code files) now use the same in-field round-arrow send control
   ReplyField uses, via InputGroup, instead of Cancel/Delete/Comment buttons
   below the box. Since the Delete button is gone, Escape and a blur while
   the draft is still empty now discard it (onDelete) so a draft can never
   get stuck open with nothing in it; a non-empty draft still posts on
   Enter, the arrow, or a blur (click-away), matching the existing
   commit-on-done rule in doc-comments-store.ts. Typing, focus and the
   existing focus-regression tests are unaffected — editingDraft still only
   flips on Enter/blur/Escape/the arrow, never a keystroke.

Added/updated tests: comment-edit-delete.test.tsx pins Cancel/Save sharing
the edit box's border; CommentCard.test.tsx adds a describe block for the
draft's send control (no Delete button, arrow disabled-while-empty, Enter
posts, Escape/blur discard only while empty); new NewCommentPopover.test.tsx
pins the same contract for the popover surface (Reading mode / code files).

Verified visually in the workbench (midnight theme, 1440x900): edit mode's
Cancel/Save now sit inside the textarea's border, and a new draft card in
the panel shows the arrow-send control with no Delete button — screenshots
in docs/active/design/2026-09-24-doc-comments/shots-fixes/ (workspace repo).
The code-file popover wasn't captured live — reaching it needs a real text
selection + right-click "Add comment", which this session's screenshot
tooling (explore.mjs) can only drive through numbered interactive
controls, not arbitrary prose; its behavior is covered by
NewCommentPopover.test.tsx and CodeCommentPopover.test.tsx instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…box get the full card width; comment boxes grow with their text

The Edit/Delete/Resolve icons were a third column beside the text, taking
~60px from every comment even while hidden (Destin: edit box squeezed, text
cut off after two lines). They now sit on the name/time line; edit, draft and
popover fields grow with content (CSS field-sizing, no per-key measuring).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ine up everywhere; plain quote line

Reply, new-comment (panel) and new-comment (floating) boxes were three
separate builds: the new-comment ones were bare textareas with no left
padding, so the caret sat against the border and the arrow landed at a
different height in each (Destin: "alignment is all very off"). One
CommentComposer now serves all three, with the field's own padding tokens,
growing with the text. The floating box's quote loses its italic and side bar
("gross thumbnail styling") for one quiet muted line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and the Editing pill

Destin's exact asks, reviewing the dev instance screenshot:

1. Edit mode's textarea already shared FIELD_TEXT/FIELD_SIZE.sm and an
   inset/edge-dim/focus-accent surface with CommentComposer — the mismatch
   was Save reading as a light secondary button next to the composer's dark
   filled send arrow. Save is now the Button primitive's `primary` variant
   (bg-accent/text-on-accent, same as the arrow); Cancel stays `ghost`.
2. Editing a comment now hides its own Delete + Resolve and the thread's
   reply box; editing a reply hides that reply's own Delete and the same
   reply box. CommentCard/HighlightHoverCard's `isEditingComment`/
   `editingReplyId` gate the ReplyField render.
3. The pencil becomes a dark pill reading "Editing" + the pencil icon,
   sitting exactly where the hidden icons/resolve toggle were. Its
   aria-label/title is "Cancel editing"; clicking it cancels.
4. Clearing the box swaps Save for a `danger` Delete button; pressing it
   (or Enter while empty) opens the SAME delete confirm the standalone
   Delete icon opens (E-4) — no second deletion path. Typing text back
   swaps it back to Save.

Refactor: CommentActions.tsx grows CommentRowActions (the editing/
not-editing swap) and EditingPill, shared by CommentCard and
HighlightHoverCard so neither hand-rolls the swap — EditDeleteButtons and
EditingPill are no longer exported (knip: only CommentRowActions is called
from outside the file now). InlineEditField takes a new onRequestDelete.

EditingPill is a raw <button> reusing Button's own primary/sm classes
verbatim rather than `<Button className="rounded-full">`: lint:design's
no-restyle rule blocks a caller overriding <Button>'s shape/spacing groups
even under this file's "layout" contract, the same reason ICON_BUTTON above
is already a raw button for its hover-reveal.

Tests: tests/comment-edit-delete.test.tsx gets new coverage for hide-while-
editing (comment and reply), the Editing pill (renders, cancels, tooltip),
Save<->Delete swap on empty/non-empty text, and Enter-while-empty opening
the confirm — for both CommentCard and HighlightHoverCard. All existing
cases (focus-safety, cursor-pointer sweep, resolve/edit/delete gating)
still pass unmodified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… box gets the full width

The hover card kept its actions (now the Editing pill) as a third column,
squeezing the edit box to half the card and pushing Cancel outside its border
when empty. Same fix the panel card already had; checked in the preview at
both the filled and emptied states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…int back to its 542 ceiling)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pill (Destin, review D-2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word: edit a comment's text, edit a reply, delete a reply, delete a whole
thread (root + replies, anchors gone). Excel: edit a thread's text, edit a
reply, delete a reply, delete a whole thread, and delete a sheet's LAST
comment (proves cleanupEmptyCommentPartsIfNeeded removes every comment
part/rel/content-type override/<legacyDrawing> — new fresh-single-comment.xlsx
fixture, since no existing fixture has a sheet with only one comment). Plain
sidecar: edit + delete, mirrored in desktop and Kotlin tests against the
shared thread.json fixture.

New write-golden cases in generate-docx-write-golden.mjs/
generate-xlsx-write-golden.mjs (edit-launch-brief, edit-reply-launch-brief,
delete-reply-launch-brief, delete-thread-launch-brief, edit-docling,
edit-reply-docling, delete-reply-docling, delete-thread-docling,
delete-last-comment-fresh), replayed by
doc-comments-write-golden-staleness.test.ts and matched by new
DocxCommentsCrossPlatformParityTest.kt/XlsxCommentsCrossPlatformParityTest.kt
cases (copied into app/src/test/resources/doc-comments/write-golden/).

Reverse direction (Kotlin writes, desktop reads) gains one docx and one xlsx
case in shared-fixtures/doc-comments/kotlin-write-golden/ (docx-edit-delete,
xlsx-edit-delete — each combining an edit and a delete against two real
pre-existing comments), read by new cases in
doc-comments-kotlin-write-golden.test.ts.

Added README.md/manifest.json to both write-golden directories indexing
every case.

No cross-platform parity bug found: desktop's and Kotlin's edit/delete
implementations already agreed byte-for-byte (ignoring createdAt and a
brand-new xlsx thread's own GUID, the same exclusions every other T21 case
already makes). Tests/fixtures only — no product code changed.

bash scripts/verify.sh: all checks pass. ./gradlew test -x bundleWebUi: 583
tests, 0 failures, 0 errors (debug/releaseTest/release unit test variants).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts:
- desktop/line-budgets.json: re-measured wc -l on every listed file
  against the merged tree and recorded the real ceilings (no file
  dropped below the 1500 default).
- desktop/src/renderer/components/MarkdownContent.test.tsx: both
  branches independently added the same 120s named test budget for
  the same two random-streaming tests on 2026-09-28 (test-suite-hygiene:
  budgets are measured, named constants). Kept master's locally-scoped
  STREAMING_SWEEP_BUDGET_MS (already covers a third, master-only test)
  and dropped our now-redundant top-level RANDOM_STREAM_BUDGET_MS.
- desktop/tests/LocalModelsSection.test.tsx: kept our branch's
  openSettings() helper, whose unconditional land()-gate already
  supersedes master's holdLater/reveal() opt-in for the same
  under-load poll race (documented inline in the helper's own comment).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roid CI

PR-review fixes, both platforms:
- Word: splitting a run to place a comment kept only the one text node being
  cut, deleting tabs, breaks and images in the run, and could crash on a
  quote spanning several children of one run. Splits now move the real child
  nodes; tab-stop definitions in <w:pPr> no longer count as text.
- Word: declare xmlns:w14 before writing w14:paraId (files from Word 2007,
  LibreOffice and Google Docs came out not well-formed).
- Every save now fails verification (and rolls back) if a changed XML part
  newly uses an undeclared namespace prefix — the lenient DOM could not see it.
- Saves use DEFLATE: JSZip's STORE default re-stored every part uncompressed.
- Excel: <legacyDrawing> goes at its schema position, before <tableParts> and
  <extLst>; appending it last made Excel "repair" the file and drop comments.
- Desktop rollback writes the original bytes back beside the file instead of
  renaming the backup from ~/.claude (EXDEV across drives left the bad file).
- Owner-file check covers Word's shortened ~$ names (8+ chars drop two).
- Android: one part in memory is capped at 32MB (was 200MB -> OOM).
- Android CI: the two Excel golden tests ran in the author's time zone only;
  they and the golden generator now pin America/Phoenix.

line-budgets: xlsx-comments.ts raised 2310 -> 2317 for the schema-order fix
(comments already condensed); docx-comments.ts lowered 2037 -> 2031.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Ask your assistant no longer wipes a half-typed message: with a draft (or
  attachments) waiting, the comments chip is added and nothing is sent.
- Android: the pending-mutation queue owns its coroutine scope, so closing the
  first of two sessions in a project no longer stops Word/Excel comments for
  the other.
- No duplicate comments after a timeout: the app claims a request (atomic
  rename to .claimed) before applying it; the MCP script withdraws its request
  on timeout, or keeps waiting if the app already claimed it. Hour-old
  leftover requests and claims are swept.
- Reply ids are the highest existing number + 1, not count + 1, so a reply
  added after a middle one was deleted never reuses a live id (desktop store,
  MCP script, Android store, workbench mock).
- Markdown highlights no longer garble or crash the viewer when the file
  changes: the content is keyed on its text, and removing highlights restores
  React's own text nodes exactly instead of normalize()-merging them.
- Stress tests take best-of-5 samples (a best-of-3 ratio hit 13x once under
  full-suite load; the code measures 6.3-7.2x).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@itsdestin
itsdestin merged commit 1e8031f into master Sep 29, 2026
4 checks passed
@itsdestin
itsdestin deleted the session/comments-mock-a branch September 29, 2026 05:18
itsdestin added a commit that referenced this pull request Sep 29, 2026
Brings the Office branch up to date with master, including the doc comments
work (#587). Office's Edit/Done and the Comments button now sit side by side
in the file drawer and the Projects file view; comments stay hidden while a
file is being edited in Office.

Conflicts:
- ActiveArtifactView.tsx: master's comments layout kept; Office's artifactId
  prop, autosaves flag and lazy import kept.
- SessionDrawer.tsx / FilesTab.tsx: both sides' state kept; Comments button
  first, then Office's Done or the text editor's Save/Cancel.
- mock-shim.ts: the comments fixture answers launch-brief.docx; other Word and
  PowerPoint fixtures come from the Office editor's samples.
- ipc-channels.test.ts: the office:* and docComments:* parity blocks kept whole.
- line-budgets.json: main.ts 2668, preload.ts 2051, SessionDrawer.tsx 1614,
  remote-shim.ts 3247 — each is the two sides' reviewed growth added together.

Also fixes a pre-existing master flake: CommentsMargin's 200-vs-1,000 comment
cost pin grew the document with the comment count, so normal cost read 8.8x
(12.02x under a full-suite run, failing its 12x bound). The document is now a
fixed 1,000 paragraphs: normal 5.1-5.3x, bound 8x; a planted per-pair DOM
slowdown fails it at 8.4x.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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