Native surfaces migration — evolve the extension onto VS Code's native review surfaces (9-task plan, spec v0.2.1) #72

Merged
benstull merged 18 commits from session-0065 into main 2026-07-02 23:09:38 +00:00
Owner

Executes the approved migration plan plans/2026-07-01-native-surfaces-migration.md (from graduated Solution Design coauthoring-native-surfaces.md v0.2.1, session 0064) end to end — session 0065, executing-plans.

What ships

  • Task 1 — CoeditingRegistry (INV-10): explicit opt-in gate; ✦ Coedit this Document with Claude / Stop editing with Claude; persisted set; cowriting.isCoediting/baselineMode context keys.
  • Task 2 — Baseline router (INV-7): git-HEAD baseline for tracked files (advances on commit only, storage-free) / snapshot otherwise; GitBaselineAdapter on the built-in Git extension; INV-18 machine-landing advance retired (approved); pinDiffBaselinemarkReviewed (snapshot-only, D14).
  • Task 3 — Native diff surface: cowriting-baseline: content provider + SourceControl QuickDiff (gutter bars), Review Changes native diff, ✦ Coediting · N changes status bar, pure countLineHunks.
  • Task 4 — INV-10 gating everywhere: threads/attribution/proposals/CodeLens/decorations all gate on the registry; commenting ranges re-assigned on gate change (spike seam); exit hides, re-enter restores (PUC-7).
  • Task 5 — EditFlow extraction: runEditAndPropose + seams out of the webview controller ahead of its sunset.
  • Task 6 — Comments-first ask (D19/D10/D8): Ask Claude opens a focused native comment; every human comment on a coedited doc runs a turn → machine reply (INV-8 onBehalfOf) → ✦ Make this edit offer → pending proposals (INV-5); input webview deleted.
  • Task 7 — Built-in preview annotations (D3/D21): pure markdown-it plugin (sentinel discipline reused, #33/#47-hardened), authorship + change + proposal marks, cowriting.annotations toggle, mermaid rendering via markdown.previewScripts (Q4 confirmed) including intra-diagram mermaid diff parity (INV-29) with body-matched fence queue.
  • Task 8 — Review-panel webview sunset (D17/§6.10): trackChangesPreview.ts + client assets deleted; ⌘⌥R → Review Changes; Open Cowriting Review Preview wrapper; ✓ Keep / ✗ Reject (+ Keep all) lens copy (PUC-2); README rewritten to the native-surface map (DOC-2); coverage moved in the same commit.
  • Task 9 — Full-loop E2E + smoke: one named E2E walking PUC-7→8→2→1; npm run smoke:native (real-SDK, operator-run).

Verification

  • typecheck + 301 unit + 90+5 host E2E green at HEAD; esbuild clean. Every task landed green; per-task subagent reviews + a final whole-branch review (verdict: ready to merge) with one fix wave (preview-host INV-10 gate, thread contextValue tokens, establish-on-open baseline).

Follow-ups filed

  • #70 — INV-5 gap: reject-after-interior-edit silently skips the revert (pre-existing F12, bug P2)
  • #71 — surface polish batch from the final review (task P3)
  • #69 — D15 preview-initiated comments (deferred at plan approval)

Owed operator gestures (plan definition of done)

  • npm run smoke:native (rung 3, real SDK) — one green run reported to the session transcript
  • Mermaid-preview GUI smoke (previewScripts path isn't E2E-queryable)

No §9 web pipeline applies (VS Code extension — spec §7.2).

🤖 Generated with Claude Code

Executes the approved migration plan `plans/2026-07-01-native-surfaces-migration.md` (from graduated Solution Design `coauthoring-native-surfaces.md` v0.2.1, session 0064) end to end — session 0065, executing-plans. ## What ships - **Task 1 — CoeditingRegistry (INV-10):** explicit opt-in gate; `✦ Coedit this Document with Claude` / `Stop editing with Claude`; persisted set; `cowriting.isCoediting`/`baselineMode` context keys. - **Task 2 — Baseline router (INV-7):** git-HEAD baseline for tracked files (advances on commit only, storage-free) / snapshot otherwise; `GitBaselineAdapter` on the built-in Git extension; **INV-18 machine-landing advance retired** (approved); `pinDiffBaseline` → `markReviewed` (snapshot-only, D14). - **Task 3 — Native diff surface:** `cowriting-baseline:` content provider + SourceControl QuickDiff (gutter bars), `Review Changes` native diff, `✦ Coediting · N changes` status bar, pure `countLineHunks`. - **Task 4 — INV-10 gating everywhere:** threads/attribution/proposals/CodeLens/decorations all gate on the registry; commenting ranges re-assigned on gate change (spike seam); exit hides, re-enter restores (PUC-7). - **Task 5 — EditFlow extraction:** `runEditAndPropose` + seams out of the webview controller ahead of its sunset. - **Task 6 — Comments-first ask (D19/D10/D8):** `Ask Claude` opens a focused native comment; every human comment on a coedited doc runs a turn → machine reply (INV-8 onBehalfOf) → `✦ Make this edit` offer → pending proposals (INV-5); **input webview deleted**. - **Task 7 — Built-in preview annotations (D3/D21):** pure markdown-it plugin (sentinel discipline reused, #33/#47-hardened), authorship + change + proposal marks, `cowriting.annotations` toggle, mermaid rendering via `markdown.previewScripts` (Q4 confirmed) **including intra-diagram mermaid diff parity (INV-29)** with body-matched fence queue. - **Task 8 — Review-panel webview sunset (D17/§6.10):** `trackChangesPreview.ts` + client assets deleted; ⌘⌥R → `Review Changes`; `Open Cowriting Review Preview` wrapper; ✓ Keep / ✗ Reject (+ Keep all) lens copy (PUC-2); README rewritten to the native-surface map (DOC-2); coverage moved in the same commit. - **Task 9 — Full-loop E2E + smoke:** one named E2E walking PUC-7→8→2→1; `npm run smoke:native` (real-SDK, operator-run). ## Verification - typecheck + **301 unit** + **90+5 host E2E** green at HEAD; esbuild clean. Every task landed green; per-task subagent reviews + a final whole-branch review (verdict: ready to merge) with one fix wave (preview-host INV-10 gate, thread contextValue tokens, establish-on-open baseline). ## Follow-ups filed - #70 — INV-5 gap: reject-after-interior-edit silently skips the revert (pre-existing F12, bug P2) - #71 — surface polish batch from the final review (task P3) - #69 — D15 preview-initiated comments (deferred at plan approval) ## Owed operator gestures (plan definition of done) - `npm run smoke:native` (rung 3, real SDK) — one green run reported to the session transcript - Mermaid-preview GUI smoke (previewScripts path isn't E2E-queryable) No §9 web pipeline applies (VS Code extension — spec §7.2). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
benstull added 18 commits 2026-07-02 23:09:27 +00:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements comprehensive unit test coverage for CoeditingRegistry.syncContext,
which derives context keys (cowriting.isCoediting, cowriting.baselineMode) via
vscode.commands.executeCommand — previously untested. Added 8 new tests:

- isCoediting: true for active editor with coediting doc
- isCoediting: false for undefined editor or non-entered doc
- baselineMode: uses hook result when set; defaults to "" otherwise
- exit idempotency: double exit fires onDidChange only once

All assertions inspect the mocked executeCommand call arguments directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 2 of the native-surfaces migration plan. GitBaselineAdapter resolves a
document's HEAD blob via the built-in vscode.git extension (hardened with a
.git/logs/HEAD watch that nudges repo.status(), since the git extension's own
watcher doesn't reliably notice out-of-band commits on non-workspace-folder
repos in the E2E host). DiffViewController is reworked into a baseline router:
head mode (git-tracked, never persisted, re-read on commit) vs snapshot mode
(captured on CoeditingRegistry entry, re-pinned by "Mark Changes as Reviewed"
— renamed from cowriting.pinDiffBaseline, D14). The shipped machine-landing
baseline advance (#48/INV-18) is retired: a landed Claude edit now stays a
visible change-since-baseline until commit or review (INV-7/D21). Legacy
on-disk reasons ("opened"/"machine-landing") migrate to "entered" via
BaselineStore.normalizeReason on load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 4 gated every surface on CoeditingRegistry but only wired
ThreadController/EditorProposalController to registry.onDidChange for
restore-on-enter — ProposalController.renderAll and
AttributionController.loadAll were never called on the enter transition, so
a document's FIRST enter with pre-existing sidecar proposals/attribution
showed threads but not proposal decorations/CodeLens or committed
author-coloring until a later edit happened to fire them (a gap Task 4's
own report flagged as known and unfixed).

Both controllers now subscribe to registry.onDidChange in their
constructors and call renderAll/loadAll on entry only (both already
self-gate on isCoediting; exit stays hide-only, mirroring
ThreadController). Construction order in extension.ts (attribution before
proposals before EditorProposalController) keeps the same-tick read order
correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves runEditAndPropose, the askClaude gate/prompt/progress wrapper, the
EditTarget type, the editTurn/askEditInstruction/turnSeq state, and the
cowriting.editDocument command registration out of TrackChangesPreviewController
into a new EditFlow (src/editFlow.ts), reachable from the review webview
(delegates) and, from Task 6 on, the thread controller. extension.ts now
constructs EditFlow before the preview controller and routes
acceptAllProposals/rejectAllProposals directly to ProposalController, dropping
the preview-controller indirection. E2E suites that drove the old
trackChangesPreviewController.{setEditTurnForTest,runEditAndPropose,askEditInstruction}
seams now drive the same seams on editFlow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 7 of the native-surfaces migration plan. Pure previewAnnotations.ts
(annotateSource + cowritingMarkdownItPlugin) reuses trackChangesModel.ts's
sentinel discipline (#33/#47), generalized to a 3-way ins-claude/ins-human/del
tag (exported injectSentinels/sentinelsToSpans, colorByAuthor unaffected,
84/84 existing tests pass unmodified). Host hook in extension.ts wires
registry/diffView/attribution/proposals/config into the plugin, exposed via
CowritingApi.extendMarkdownIt + a previewAnnotationHost test seam.
cowriting.annotations setting + toggleAnnotations command/menu. Q4 mermaid
(previewScripts + options.highlight fence override) implemented per the
proven bierner.markdown-mermaid pattern; intra-diagram diff augmentation
scoped out (unverifiable via the structural E2E harness) — see
task-7-report.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deletes the last bespoke UI surface (TrackChangesPreviewController + its
sealed webview client assets); every entry point re-points at native VS
Code chrome per spec §5: the #41 right-click entries become
cowriting.openReviewPreview (enter coediting if needed -> "Open Preview to
the Side"), Ctrl+Alt+R/Cmd+Alt+R moves to cowriting.reviewChanges (native
diff), and the F12 CodeLens per-proposal titles read "Keep"/"Reject" with
a top-of-file "Keep all (N)"/"Reject all" pair once >=2 proposals are
pending. EditFlow drops its own askClaude/askEditInstruction (the
webview's only caller) and its now-unused constructor params.

Coverage that lived only in the webview's test seams (renderHtmlFor,
receiveMessage, isOpen, ...) moves to direct calls against the surviving
controllers/pure renderReview (test/e2e/suite/helpers.ts gains a shared
renderHtmlFor probe); a genuine gap (pending-proposal + unchanged-block
rendering) is backfilled in test/previewAnnotations.test.ts. README's "how
it works" is rewritten as the native-surface map; the superseded F6/F7/F9/
F10/F11 sections are kept as a marked historical record rather than
deleted outright.

292 unit + 91 E2E green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 7 scoped out reusing the F7.1 (#22) intra-diagram mermaid diff in the
built-in Markdown preview's mermaid fence rendering; Task 8 then deleted the
webview that still had it, leaving mermaidDiff.ts/mermaidFlowchartDiff.ts/
mermaidSequenceDiff.ts production-dead. Wires diffMermaid into
cowritingMarkdownItPlugin's options.highlight override via a per-render
mermaid-fence queue (built in the core rule where state.env is visible,
consumed by highlight in fence-encounter order — highlight itself has no env
param) so a mermaid fence changed since the F6/F7 baseline is re-emitted with
classDef/class/linkStyle styling directives + legend before mermaid.js ever
sees it, reusing diffBlocks' baseline/current block pairing (same pairing
renderReview used). Unchanged diagrams, disabled annotations, no baseline, and
pinned-zero-diff all pass through verbatim.

Ports the substance of the 6 mermaid-diff assertions Task 8 deleted with the
webview's E2E suite as pure unit tests in previewAnnotations.test.ts instead
(DOM-free, source-level).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review of 705de31's mermaid-diff wiring: buildMermaidQueue's overrides
were consumed positionally per rendered mermaid fence, so a fence the queue
didn't know about (a pending proposal's optimistic apply inserting a whole
new mermaid block, or annotateSource's committed-deletion reinsert) shifted
every later override by one and substituted the wrong diagram's augmented
source instead of the documented verbatim fallback. Queue entries now carry
{curBody, source} and are matched to fences by body (first unconsumed match,
scanning forward) so a non-matching fence renders verbatim without consuming
anything and a later matching fence still gets its augmentation. Also resets
the per-render consumed state in the renderer.render wrapper, since VS Code
can call md.parse()/renderer.render() separately and replay cached tokens
without a fresh parse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 9 of the native-surfaces migration plan. fullLoop.test.ts walks the
whole inner loop in one doc, stub-turned: enter (snapshot baseline) -> two
comment asks -> reply -> offer -> pending proposals (F12 optimistic-apply,
decorated + isApplied) -> human tweak inside a pending proposal's range
(INV-11: breaks the raw fingerprint anchor, but finalizeInPlace/"Keep" is
built to survive that) -> keep one (attribution split via spansFor: claude's
words + the human tweak, char-honest) -> reject the other (buffer text
returns EXACTLY to its pre-proposal text, INV-5) -> markReviewed -> change
count 0.

scripts/smoke-native-loop.mjs + test/e2e/smoke/nativeLoop.ts are the rung-3
manual gate: launches the real EDH against sandbox/, posts a real comment via
ThreadController.createThreadOnSelection with BOTH turns left unstubbed (the
real @cline/sdk, INV-8), waits for the reply/offer, makes + accepts the edit,
and prints the created proposal ids. Lives outside test/e2e/suite so it is
never swept into `npm run test:e2e`; `npm run smoke:native` builds +
compiles then runs it. Operator-run only (real credentials) -- not executed
here, verified by typecheck + a syntax/structural pass only.

300 unit + 86 workspace/5 no-workspace host E2E green (1 pending, pre-existing
#54 undo-suite skip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
benstull merged commit 935fcc35ee into main 2026-07-02 23:09:38 +00:00
Sign in to join this conversation.