Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 KiB
status
| status |
|---|
| graduated |
Solution Design: Coediting Markdown with a Machine — a Native VS Code Experience
| Author(s) | Ben Stull (with Claude) |
| Reviewers / approvers | Ben Stull |
| Status | graduated |
| Version | v0.2.1 |
| Source artifacts | North-star architecture spec for human↔machine markdown coediting. Reference: VS Code's native review surfaces (SCM/QuickDiff, diff editor, Markdown preview, Comments). Inputs: the shipped plugin (F1–F12), the rung-2 feasibility spike (vscode-cowriting-prototype), coauthoring-feasibility-spike.md. Supersedes: none. |
Change log
| Date | Version | Change | By |
|---|---|---|---|
| 2026-06-27 | v0.1.0 | Initial draft — brainstorming session 0062 | Ben Stull + Claude |
| 2026-07-01 | v0.2.0 | Session 0063 recommendation folded in: review surface is in-buffer pending changes + per-hunk CodeLens (supersedes the multi-diff Keep/Undo surface — D18, resolves Q2/Q7, amends INV-5/INV-12); Q1 resolved — evolve the shipped extension in place (D17); comments-first ask surface (D19); D11 updated to shipped reality (D21); rung-2 spike findings (E1/E3/E5) recorded | Ben Stull + Claude |
| 2026-07-01 | v0.2.1 | Rung-2 spike complete (session 0064): E2 PASS, E4 PASS-with-fallback — comment-loop API seams folded into §6.4 (commenting-range refresh on gate change, input focus via workbench.action.addComment, commentThread menu when-key, offer affordances) and baseline-router notes (nested-repo discovery); §7.1 rung 2 closed |
Ben Stull + Claude |
1. Business Context
The business lens — solution-agnostic throughout. No mechanism is proposed until §2.
1.1 Executive Summary
A growing share of real writing — specs, docs, RFCs, design notes, prose — happens in markdown, and writers increasingly want a machine to help author it. But coediting a markdown document with a machine is awkward today. When the machine edits the file it hands back a rewrite, not a reviewable change: the writer is left staring at a wall of new text with no natural answer to "what did you actually change?" The writer can't easily keep some of the machine's edits and drop others; after a few rounds nobody can tell which words are the human's and which the machine's; and there's no clean rendered view of the work-in-progress with the changes made legible. In practice "writing with a machine" collapses into copy-paste between a chat window and the file — throwing away the diff, granular control, and provenance. This design delivers human↔machine markdown coediting inside the editor, through the surfaces a writer already uses to review changes, so coediting feels like a native capability of the editor rather than a chat window bolted onto the side. The value: writers can let a machine help with real documents while staying in control of what lands and confident about what changed.
1.2 Background
Markdown is the lingua franca of technical and semi-technical writing — it is where specifications, documentation, RFCs, and a great deal of prose are actually drafted and revised. At the same time, capable machine coauthors have arrived, and writers want to use them on these documents, not just on code.
But the modalities available for "writing with a machine" were built for other shapes of work, and they serve document coediting poorly:
- Chat copy-paste. Paste the document (or a passage) into a conversation, get a rewrite back, paste it into the file. The diff is gone, authorship is gone, granular accept/reject is gone, and the file and the conversation drift apart.
- Whole-file rewrite. The machine overwrites the file. The writer must diff it by hand to see what happened and has only all-or-nothing control.
- Inline code completions. Built for short, accept-as-you-type code suggestions in the editor — not for reviewing a machine's substantive revisions to a prose document, where the writer wants to see, weigh, and selectively keep changes.
None of these lets the writer do the thing that matters for a document: review the machine's proposed revision the way they'd review any change — as a diff they can read, accept or reject piece by piece, attribute, and preview rendered.
Crucially, the editor already has best-in-class machinery for exactly this — diff viewing, source-control review, multi-change accept/reject, threaded comments, and a rendered Markdown preview — but none of it is wired up for coediting with a machine. The opportunity is to deliver machine coediting through those surfaces.
1.3 Business Actors / Roles
| Role | Responsible for (in the business) |
|---|---|
| Writer (human) | Authors and owns a markdown document; decides what changes land in it. |
| Machine coauthor | A capable machine writing assistant that the writer asks to help revise the document; it proposes changes on the writer's behalf and never lands them unilaterally. |
1.4 Problem Statement
A writer who wants a machine to help revise a markdown document has no good way to do it while staying in control. They cannot see exactly what the machine changed, cannot keep some changes and reject others, cannot tell who wrote which words, and cannot read a clean rendered result — at least not without leaving their editor and diffing, cherry-picking, and reconciling by hand. The machine's revision arrives as an opaque rewrite rather than a reviewable proposal, so the writer must choose between blind trust and tedious manual reconciliation.
1.5 Pain Points
| # | Pain | Who feels it | Cost / frequency today |
|---|---|---|---|
| PP-1 | The machine's revision arrives as a rewrite, not a reviewable change — "what did you change?" has no easy answer | Writer (human) | Every machine revision; manual diffing each time |
| PP-2 | Accepting the machine's help is all-or-nothing — no way to keep some edits and reject others in place | Writer (human) | Every revision with mixed-quality changes |
| PP-3 | Authorship blurs — after a few rounds nobody can tell which words are the human's and which the machine's | Writer (human) | Cumulative; erodes trust and review confidence |
| PP-4 | No clean rendered view of the in-progress document with the changes made legible | Writer (human) | Every time the writer wants to judge the result as prose |
| PP-5 | The machine works opaquely — ask, wait, a blob appears; no sense of progress or ability to stop it | Writer (human) | Every machine turn |
1.6 Targeted Business Outcomes
| # | Outcome | Success metric | Baseline → Target | Guardrail (must not regress) | How / when measured |
|---|---|---|---|---|---|
| BO-1 | Writers adopt machine coauthoring for real documents because it's reviewable and in-editor | A writer completes a full revise → review → keep/reject → read loop without leaving the editor or copy-pasting | chat copy-paste / blind rewrite → in-editor reviewable coediting | The writer always stays in control of what lands | Dogfooding walkthrough |
| BO-2 | Coediting feels like a built-in editor capability, not a separate tool | The diff, accept/reject, and preview use surfaces the writer already knows | bespoke/foreign UI → native surfaces | Full capability (see-change, selective accept, authorship, preview, progress) preserved | Dogfooding walkthrough |
1.7 Scope (business)
- In scope: enabling a writer to coedit a markdown document with a machine while staying in control — seeing what changed, accepting/rejecting changes individually, knowing authorship, reading the rendered result, and following the machine's progress — entirely within their editor.
- Out of scope: marketplace release; multi-file / whole-project machine orchestration; a server or cloud backend. (v0.1.0 also deferred the packaging decision; v0.2.0 resolves it — the shipped extension evolves in place, D17/§13-Q1.)
- Non-goals: replacing the writer's judgment (the machine proposes; the human disposes); non-markdown document formats in v1; a real-time multi-human collaboration server.
1.8 Assumptions · Constraints · Dependencies
- Assumptions: the writer works in an editor with native diff, source-control, multi-change review, Comments, and Markdown-preview surfaces (VS Code is the target); a capable machine coauthor is available to run locally.
- Constraints: serverless and git-native (no external store); nothing the machine proposes ever lands without the writer's explicit acceptance — a pending suggestion is always revertible in one gesture; no secret bytes in any persisted artifact.
- Dependencies: a local machine-coauthor runtime; the editor's stable extension
APIs for the surfaces above; the built-in Git extension API (to read
HEADfor git-tracked baselines — §6.4). Files outside a repo degrade to the snapshot baseline, so git is not strictly required.
1.9 Business Use Cases
BUC-1 — As a writer, I can see exactly what the machine changed in my document since a point I chose, so that I can judge it instead of trusting blindly.
Scenario: BUC-1 — see what changed
Given I marked my document as "good" at some earlier moment
And the machine has since revised it
When I ask to see what changed
Then the changes are shown to me as a clear before/after I can read
- BUC-1 acceptance criteria: the writer sees an accurate before/after of the document against the chosen point.
BUC-2 — As a writer, I can keep or reject the machine's proposed changes one at a time, so that I stay in control of what lands.
Scenario: BUC-2 — keep or reject, change by change
Given the machine has proposed changes to my document
When I review them
Then I can keep or discard each change individually, or all at once
And nothing the machine proposed lands in my document until I keep it —
discarding a change restores my text exactly as it was
- BUC-2 acceptance criteria: no proposed change settles until the writer keeps it, and rejecting one restores the prior text exactly; each change is independently keepable/discardable; the writer can edit a proposed change before keeping it, and those edits are attributed to the writer; keep-all and reject-all exist.
BUC-3 — As a writer, I can tell who wrote which parts of my document and read a clean rendered version, so that I can judge the result as finished prose.
Scenario: BUC-3 — authorship and rendered result
Given my document has mixed human/machine authorship and recent changes
When I open its rendered view with annotations on
Then I see the document rendered, with changes and authorship visibly marked
And I can turn annotations off to read it clean
- BUC-3 acceptance criteria: the rendered document shows changes and authorship when annotations are on, renders clean when off.
BUC-4 — As a writer, I can follow the machine's progress while it works and stop it, so that it is never an opaque black box.
Scenario: BUC-4 — visible, stoppable machine work
Given I asked the machine to revise my document
When it is working
Then I can see live progress
And I can cancel before it finishes
BUC-5 — As a writer, I can leave a remark on a passage of a document I'm coediting and have the machine respond and offer to make the change, so that a comment can become an edit without me leaving the document.
Scenario: BUC-5 — a comment summons a response and an offer to edit
Given I have invited the machine to coedit this document
When I leave a comment on a passage
Then the machine replies to my comment
And it offers to make the change I described
And if I accept, its edit arrives as a proposed change I review like any other
- BUC-5 acceptance criteria: on an invited document a comment elicits a machine reply and an offer; accepting produces a reviewable proposed change (it never edits directly); on a non-invited document nothing is summoned.
BUC-3a (unhappy) — As a writer, when a comment, an attributed span, or a proposed change can no longer be confidently placed after edits, I am shown it as displaced rather than silently moved to the wrong place.
2. Solution Proposal
Build software: a VS Code extension that lets a writer coedit a markdown document with a machine coauthor, delivering the whole loop through the editor's own native review surfaces.
The enabling idea: model the coauthored document as a tiny source control. The point the writer last marked "good" (the baseline checkpoint, set on entering coediting and re-pinnable) is the source-control original; the live document is the working tree; the machine's proposed edits are pending changes. Once the relationship is framed as source control, the editor's own machinery supplies the experience the writer needs — and it is machinery they already know:
| What the writer needs | Native surface that delivers it | What it gives them, for free |
|---|---|---|
| See what changed (BUC-1) | Source-control gutter change bars + the native diff editor against a virtual baseline document | The exact diff experience they use for everything else: inline/side-by-side, navigation, accessibility |
| Keep/reject — and tweak — changes (BUC-2) | Pending changes in the live editor — proposed edits land decorated-but-unsettled in the buffer, with per-hunk ✓ Keep / ✗ Reject CodeLens (D18) | The paradigm the platform's own AI edit sessions use: review the change in the document, tweak it by just typing (INV-12), revert it with one gesture (INV-5) |
| Authorship + rendered result (BUC-3) | The built-in Markdown preview, extended via a markdown-it plugin to inject authorship + change marks |
Native rendering, scroll-sync, theming, and cooperation with the writer's other Markdown extensions |
| Discuss a passage — and have the machine respond (BUC-5) | The Comments API — threaded comments where, on a coedited document, the machine replies in-thread and offers an edit | The familiar review-comment experience, in the gutter, that can turn a remark into a proposed change |
| Follow + stop the machine (BUC-4) | A progress notification + a shared OutputChannel, with Cancel | Live, stoppable, legible machine work |
Coediting is something a document opts into. A writer turns a document into a coedited document with one command — "Coedit this Document with Claude" — and only then do the surfaces above attach to it: the SourceControl/QuickDiff entry, the auto-responding comments, the annotation toggle, and the machine-edit entry points. The extension registers its providers once but scopes every surface to the active coediting set (INV-10), so it never hijacks Comments, Source Control, or the preview for the writer's other markdown files. This gate is what makes the auto-responding-comments behavior safe: a comment only summons the machine on a document the writer has explicitly invited it into.
Why this approach over the alternatives. Doing nothing leaves writers in the copy-paste-from-chat world (§1.2), which structurally cannot offer a diff, selective accept, or authorship. Building a bespoke coediting UI (custom panels, hand-rolled diff rendering, overlay decorations) can imitate these surfaces but won't inherit the platform's behavior — theming, scroll-sync, accessibility, the keyboard model, navigation, cooperation with other extensions — so it pays the cost of building review machinery without getting the polish, and asks the writer to learn a foreign surface. Delivering through the editor's native surfaces gets that behavior for free, makes coediting feel built-in, and shrinks what must be built and maintained to a thin layer of wiring over proven cores. The cost is accepting the platform's affordances where they differ from a bespoke one. (The v0.1.0 stable-API worry on per-change Keep/Undo resolved the other way: the spike showed CodeLens does not render in a diff editor at all, which — together with the shipped F12 experience — moved the review surface into the live buffer, D18.)
Decision posture: this is a north star with a committed delivery vehicle. It commits to the architecture and the native-surface mapping, and (as of v0.2.0, D17) to the packaging: the shipped extension evolves in place onto these surfaces — its pure cores are retained, its bespoke webview surfaces sunset (§6.10, §7.1).
Solution scope. In: the architecture, surface mapping, data model, contracts, and per-use-case design for native, in-editor human↔machine markdown coediting, including baseline pinning, authorship attribution, durable git-native persistence, live machine progress, region threads, and on-behalf-of identity. Out: product packaging/release, multi-file orchestration, non-markdown formats. Non-goal: mutating the writer's document without explicit acceptance.
3. Product Personas
| Product persona | In the plugin | Maps to business role(s) |
|---|---|---|
| PP-1 Writer | Pins a baseline, asks the machine to revise the document (or a selection), reviews proposed changes in the diff, reads the annotated preview, opens region threads, watches/stops the machine. | Writer (human) |
| PP-2 Machine coauthor | Runs a turn locally, streams progress, and emits proposed edits (never lands them); authored spans carry its provenance. | Machine coauthor |
4. Product Use Cases
Scenario: PUC-1 — see the diff against the baseline checkpoint (realizes BUC-1)
Given I am coediting a document (its baseline is the last commit if it's in git, else a snapshot taken on entry)
When I (or the machine) change the document
Then the source-control gutter shows change bars for what changed since the checkpoint
And a Review Changes button in the editor title bar and a "✦ Coediting · N changes" status-bar item open the review without visiting the Source Control pane
And "Open Changes" opens the native diff editor (baseline ⟷ live), inline or side-by-side at my choice
And committing the file (when it's in a git repo) advances the baseline to the new commit (the diff goes clean)
And "Mark Changes as Reviewed" is offered only when the file has no commit to anchor to (outside a repo, untitled, or not-yet-committed), and resets the checkpoint there
Scenario: PUC-2 — keep/reject the machine's proposed changes (realizes BUC-2)
Given I ask the machine to revise the document (or a selection)
When the turn completes
Then each proposed change appears in my editor as a pending, decorated change
(proposed words marked by author color, replaced words struck) — visible in
place but not settled
And each change carries ✓ Keep / ✗ Reject actions (CodeLens above the change)
And Keep all / Reject all are available (CodeLens menu, title bar, status bar)
And I can tweak the proposed wording by simply typing in the pending text
And rejecting a change restores my text exactly as it was
And on keep, the machine's words are attributed to the machine and my tweaks to me
And "Review Changes" opens the native diff (baseline ⟷ live) to sweep everything
at once
Scenario: PUC-3 — read the annotated rendered preview (realizes BUC-3)
Given my document has changes and mixed authorship
When I open the built-in Markdown preview with annotations enabled
Then insert/delete marks and authorship colors render inside the native preview
And toggling annotations off renders the document clean
Scenario: PUC-4 — region thread (realizes BUC-3a)
Given I select a passage — in the editor or in the rendered preview — and add a coauthoring thread
And I can also attach a comment to the whole document (anchored at its top) for a global remark
When I edit around it or reopen the document
Then the thread re-anchors; if it cannot be confidently placed it is shown displaced, never silently moved
And a thread started from the preview is created natively on the source (the preview shows a 💬 marker that deep-links to it)
Scenario: PUC-5 — live machine progress (realizes BUC-4)
Given I started a machine turn
When the machine is working
Then a notification shows the live activity line and token count
And an OutputChannel streams the full text
And I can cancel the turn
Scenario: PUC-6 — reopen / external change (realizes BUC-3a)
Given I reopen the workspace or an external change lands (a git pull)
When the document loads
Then baseline, proposed changes, attributions, and threads re-resolve against current text
And anything that cannot be confidently placed is shown displaced
Scenario: PUC-7 — enter (and leave) coediting mode (realizes INV-10)
Given an ordinary markdown document
When I run "Coedit this Document with Claude" (command, editor title, or context menu)
Then this document gains the Cowriting source-control entry, the annotation toggle, and machine-edit/comment-response behavior
And the baseline is established — the last commit if the file is in git, otherwise a snapshot of the current content
And any uncommitted changes already in a git-tracked file appear as my (human) changes since the baseline
And my other markdown documents are unaffected
And running "Stop editing with Claude" (context menu) removes those surfaces (the saved sidecar is kept for next time)
Scenario: PUC-8 — comment summons a machine reply + offer to edit (realizes BUC-5)
Given I am coediting this document
When I add a comment — on a passage (editor or preview selection) or on the whole document
Then a machine turn runs (with live progress, PUC-5) and replies in the thread
And a passage comment gives the machine that passage as context; a whole-document comment gives it the full document
And it offers to make the change; accepting opens its edit as a pending proposed change (PUC-2)
And on a document I am not coediting, adding a comment summons nothing
5. UX Layout
The experience is delivered through native VS Code surfaces; there is no bespoke panel to wireframe. Described in text (source of truth):
- Entering coediting. A "Coedit this Document with Claude" command (command
palette, editor title menu, and editor/explorer context menu, markdown-gated)
turns the active document into a coedited document; a "Stop editing with Claude"
command — in the same command palette and the editor/explorer context menu —
reverses it. A subtle status-bar item ("✦ Coediting") indicates when the active
document is in coediting mode and the change count (
✦ Coediting · N changes), and clicking it opens the review — so the writer never needs to find the Source Control pane (INV-13). Until a document is entered, none of the surfaces below attach to it (INV-10). Entering also sets the baseline to the document's current content — a clean checkpoint to measure future changes against (INV-7). - Primary actions — editor title bar + status bar (no navigation, INV-13). When
the document is coedited, title-bar buttons (Review Changes, Ask Claude,
Toggle Annotations, and in snapshot mode Mark Changes as Reviewed) and the
✦ Coediting · N changesstatus-bar item are the everyday entry points — visible without going anywhere. Ask Claude opens a comment thread (on the selection if there is one, else on the whole document) — the comment box is the ask input (D19). New proposals land as pending changes in the buffer in front of the writer (no navigation at all), and the live-progress notification carries a Review action. - Source Control gutter & viewlet (optional, for git users). A dedicated source-control entry, "Cowriting," coexists with Git: its quick-diff provider draws the familiar change bars in the editor gutter against the baseline checkpoint, its resource group lists the document's pending coauthoring changes, and "Open Changes" uses the native diff editor. A bonus home for those who use Source Control — never the required path (INV-13).
- Native diff editor. The writer's normal diff (
baseline ⟷ live), with the platform's inline/side-by-side toggle, navigation, and accessibility. - Pending changes in the editor (the accept/reject surface, D18). When the machine proposes edits, each change lands in the live buffer as a pending, decorated change — proposed words marked in the machine's author color, replaced words shown struck — with per-hunk ✓ Keep / ✗ Reject CodeLens (and a Keep-all/Reject-all menu). The writer tweaks a pending change by typing in it; Reject restores the prior text exactly. This is the same review-in-the-document paradigm the platform's own AI edit sessions use, so it reads as native even though the decorations are the extension's. (The v0.1.0 multi-diff Keep/Undo surface is superseded — the spike showed CodeLens cannot render in a diff editor, §7.1/D18.)
- Built-in Markdown preview. The writer's normal "Open Preview to the Side"; with annotations on, authorship colors and change marks appear inside it. A command/setting toggles annotations.
- Comments gutter — the primary ask surface (D19). Region threads via the native Comments API (create on selection, reply, resolve). The comment box is the one native multi-line input the editor offers, so it is how the writer asks for anything: an imperative ask ("tighten this paragraph") and a discussion remark are the same gesture. On a coedited document, the machine replies in-thread and offers an edit (BUC-5 / PUC-8); the offer, when accepted, becomes a pending proposed change reviewed in the buffer (PUC-2). A comment can be started from the rendered preview (select text there → "Comment on this"): the selection is mapped back to the source range and a native thread is created — the preview shows a 💬 marker that deep-links to it, but the thread itself renders in the gutter / Comments panel, never inside the webview (the Comments API can't render into a webview — §6.7). A whole-document comment (anchored at the top) carries the entire document as context.
- Notification + OutputChannel. Live machine progress, with a Cancel action.
5.1 Design tooling decision
- Decision: Code directly.
- Rationale: the experience is delivered through native VS Code chrome, so there is no net-new bespoke visual surface to explore. Scoring the six-question rubric: Q1 −1 (no net-new surface — native editor chrome), Q2 −1 (UI specified at wireframe-level prose against known platform surfaces), Q3 −1 (no multi-page visual language), Q4 −1 (the editor is the design system here), Q5 −1 (solo build), Q6 −1 (logic-dominant — wiring cores to platform APIs). Total −6 → Skip Design. The only "designed" pixels are the preview annotation stylesheet (a small CSS theme using the editor's theme variables), which does not warrant Design.
- Design: N/A — no bespoke UI surface (native VS Code surfaces only).
5.2 Screen: native surfaces (serves PUC-1…PUC-6)
Covered in the §5 text above. No custom screen; states map to platform states (diff editor empty/identical, no pending changes, preview loading/error) plus the coauthoring displaced state surfaced in the thread / proposed-change listing.
6. Technical Design
6.1 Invariants
- INV-1 — never silently moved: an anchored artifact (thread, attributed span, proposed change) that cannot be confidently re-resolved becomes displaced (visible, flagged), never relocated to a wrong span.
- INV-2 — git-native and serverless: durable coauthoring data is diffable, human-resolvable JSON committed alongside the document; no server, no external store. (The baseline is the exception — a throwaway local artifact kept outside the repo, INV-7.)
- INV-3 — native-first: the diff, accept/reject, and rendered-preview surfaces are delivered by native VS Code APIs, not bespoke webviews or decoration overlays. New custom rendering requires an explicit, recorded justification (§6.7) that no native surface fits.
- INV-4 — forward-compatible & portable: the persisted artifact carries a
schemaVersion;anchors,provenance,attributions,proposals, andthreadsshare one envelope; unknown fields are preserved on read/merge, so the artifact round-trips to an outer review forge (e.g. Gitea/GitHub) without loss. - INV-5 — proposed changes are pending until kept: a machine-proposed edit never settles until the writer keeps it. It appears in the live buffer as a visibly pending, decorated change (D18) — but it is fully revertible: Reject restores the prior text exactly, the model retains each proposal's original, and nothing is attributed as landed machine work until Keep. The machine proposes; the human disposes. (Amended in v0.2.0: v0.1.0 kept the buffer byte-untouched until Keep; the guarantee is now "nothing lands irrevocably," which is what the writer actually needs — and what makes tweak-by-typing possible, INV-12.)
- INV-6 — pure cores stay vscode-free: attribution, anchoring, merge, diffing,
and the progress reducer are pure modules with no
vscodeimport, unit-tested in isolation; controllers are the onlyvscode-aware layer, so surfaces can change without touching the logic. - INV-7 — baseline is the last settled version; its source follows the file's
home: the baseline is the diff original — the last "settled" version.
Where the file is tracked in a git repo, the baseline is the file at
HEAD(the last commit) and resets automatically when a commit is made — the diff is the file's uncommitted work, git-native and storage-free. Where there is no commit to anchor to (outside any repo, untracked/ignored, brand-new, or untitled), the baseline is a snapshot defaulted to the current content on entry and reset by "Mark Changes as Reviewed," stored in extension global storage keyed by URI hash (in-memory for untitled). "Mark Changes as Reviewed" exists only in snapshot mode (files with no committedHEADto anchor to) — for git-tracked files there is no manual pin; the writer commits and the baseline followsHEAD. A new in-repo file is in snapshot mode until its first commit, then flips toHEAD-tracking (and the pin command disappears). A coedited document therefore always has a baseline — there is no "missing baseline" state; the only empty state is a genuinely empty document. The baseline is disposable review state, never committed by us. - INV-8 — provenance carries on-behalf-of identity: every authored span /
message records
provenance(the human's git identity, or the machine actingonBehalfOfa human), reused across attributions, proposed changes, and threads. Changes already present when a coediting session starts (uncommitted work vs.HEAD, including edits made outside this experience — by hand or by a machine elsewhere) carry no prior provenance record, so they are attributed to the writer (human): the session cannot know their origin and treats them as the writer's existing work. (In snapshot mode the baseline is the content at entry, so there is nothing pre-existing to attribute.) - INV-9 — machine turns are observable and cancellable: a turn streams progress
and honors an
AbortSignal; the progress reducer is pure. - INV-10 — coediting is opt-in per document: every coauthoring surface (the "Cowriting" SourceControl + QuickDiff, auto-responding comments, preview annotations, the machine-edit entry points) activates only for a document the writer has explicitly entered into coediting mode ("Coedit this Document with Claude"). The extension registers its providers globally but gates every surface on membership in the active coediting set, so it never hijacks Comments/SCM/preview for documents the writer did not opt in. Exiting coediting (or closing the workspace) removes the document from the set; the durable sidecar remains, so re-entering restores state.
- INV-11 — the native diff is author-blind: the native diff editor shows what changed against the baseline, not who changed it (the diff API has no per-author color scoping — §6.7). Authorship (human vs. machine) is shown on surfaces the extension renders: the annotated preview and the in-editor author-colored track-changes decorations (D21), never by recoloring the native diff.
- INV-13 — no surface requires the Source Control pane: the primary,
always-visible entry points for a coedited document are the editor title-bar
actions and a status-bar item (both gated on
cowriting.isCoediting), and new machine proposals land in the buffer in front of the writer (D18) — the review requires no navigation at all. The SCM viewlet entry and the gutter dirty-diff are additional native affordances, not the required path — a writer unfamiliar with git / Source Control never has to find them to coedit, review, or settle a checkpoint. - INV-12 — proposed changes are editable before they are kept: a pending change sits in the live buffer, so the writer tweaks it by typing directly in it (the common "close, but a few human edits" case) — no separate proposal document, no mode. Nothing settles until Keep (INV-5); on Keep, the words the machine proposed are attributed to the machine and the writer's tweaks to the writer — by diffing final ↔ the machine's original proposal ↔ baseline — so authorship is correct at the moment the change is made, never reconstructed afterward. Once kept, the text is just text (no lingering "trapped proposal" state to untangle). A pending change re-anchors around the writer's edits; one displaced by heavy rewriting collapses to a whole-proposal Keep/Reject (D20, INV-1 discipline).
6.2 High-level architecture
Pure, vscode-free cores + thin controllers that wire those cores to native surfaces.
flowchart LR
subgraph Cores["Pure cores (vscode-free, unit-tested)"]
AT[Attribution tracker<br/>char-precise spans]
AN[Anchorer<br/>fingerprint + offsets + displaced]
MG[mergeArtifacts<br/>union-by-id, unknown-field-safe]
DF[Diff/hunk engine<br/>word- & block-level]
TP[turnProgress<br/>reducer]
RA[renderAnnotations<br/>markdown-it transform]
end
subgraph Ctrls["Thin controllers (vscode-aware)"]
SC[SourceControl + QuickDiffProvider]
BP[Baseline provider<br/>TextDocumentContentProvider]
PR[Pending-change controller<br/>in-buffer decorations + CodeLens]
PV[Preview extension<br/>extendMarkdownIt]
TC[ThreadController<br/>Comments API]
LP[liveProgress relay]
ST[Sidecar router + store]
end
subgraph Native["Native VS Code surfaces"]
GUT[Gutter dirty-diff + SCM viewlet]
DIFF[Native diff editor]
MDIFF[Live editor buffer<br/>pending changes + CodeLens]
MDPREV[Built-in Markdown preview]
COMM[Comments gutter/peek]
NOTE[Notification + OutputChannel]
end
SDK[[Machine-coauthor runtime<br/>dynamic import]]
AN --> SC --> GUT
BP --> DIFF
DF --> PR --> MDIFF
AT --> RA --> PV --> MDPREV
AN --> TC --> COMM
TP --> LP --> NOTE
SDK --> PR
Cores --- ST
Ctrls --- ST
- SourceControl + QuickDiffProvider — owns the "Cowriting" source control and the gutter dirty-diff; must never write to the document.
- Baseline provider — a
TextDocumentContentProviderfor thecowriting-baseline:scheme; serves the pinned baseline text for any document URI from the out-of-repo baseline store (INV-7). - Pending-change controller — turns the machine's emitted edits into pending proposed changes, lands them in the live buffer as decorated pending text (retaining each proposal's original for exact revert), and renders per-hunk Keep/Reject CodeLens; Keep settles + attributes, Reject restores; honors INV-5.
- Preview extension — contributes
markdown-itso the purerenderAnnotationstransform runs inside the built-in preview; owns no webview. - Sidecar router + store — persistence: a repo-committed sidecar when the document is in a workspace, a global-storage sidecar keyed by URI hash otherwise.
6.3 Data model & ownership
| Entity | Owned by | Key fields | System of record |
|---|---|---|---|
| Coauthoring sidecar | the extension (sole writer) | schemaVersion, document.path, anchors{}, provenance, attributions[], proposals[], threads[], unknown-field passthrough |
git-committed .threads/<path>.json (in-workspace) or global-storage sidecar (out-of-workspace) |
| Baseline (checkpoint) | the extension | git-tracked → resolved from HEAD (no storage); else → snapshot {text, reason, time} keyed by URI hash |
git object store (HEAD) or out-of-repo global storage (snapshot); never committed by us (INV-7) |
| Provenance | the extension | kind (human/machine), id, email, onBehalfOf, agent{runtime,model,sessionId} |
embedded in sidecar (INV-8) |
The sidecar envelope is forward-compatible and portable (INV-4): additive, unknown-field-preserving, so it round-trips to an outer review forge unchanged. The baseline is deliberately not in this envelope: it is local, disposable review state (INV-7), not durable coauthoring metadata.
// .threads/docs/spec.md.json — the coauthoring envelope (INV-4)
{
"schemaVersion": 1,
"document": { "path": "docs/spec.md" },
"anchors": { "a1": { "fingerprint": { "text": "…", "before": "…", "after": "…", "lineHint": 42 } } },
"attributions": [ { "anchorId": "a1", "provenance": <Provenance>, "span": { "start": 0, "end": 12 } } ],
"proposals": [ { "id": "p1", "anchorId": "a1", "status": "pending", // pending|accepted|rejected (INV-5)
"granularity": "block", // word|block — the decision unit
"provenance": <Provenance>, "before": "…", "after": "…" } ],
"threads": [ { "id": "t1", "anchorId": "a1", "status": "open", "messages": [ … ] } ]
// unknown top-level fields preserved verbatim (INV-4)
}
6.4 Interfaces & contracts
- Baseline router + provider — a
TextDocumentContentProviderforcowriting-baseline:whoseprovideTextDocumentContent(uri)resolves the original by the file's home (INV-7): git-tracked → theHEADblob (read via the built-in Git extension API; auto-advances on commit, watched viarepository.state.onDidChange); otherwise → a stored snapshot (BaselineStore.snapshot(docUri, text, reason)/get(docUri)), set onenterCoediting(reasonentered) and on "Mark Changes as Reviewed" (reasonpinned). The pin command is gated to snapshot mode — hidden/disabled forHEAD-anchored files (when: cowriting.baselineMode == snapshot), where committing is the reset. This mirrors the in-repo/out-of-repoSidecarRouter(§6.2). A coedited document always resolves a baseline; non-coedited documents resolve none. - QuickDiffProvider —
provideOriginalResource(docUri) → cowriting-baseline:URI (drives gutter bars). Registered on the "Cowriting"SourceControl. Spike-verified (E2, 2026-07-01): gutter bars and the native diff render in both baseline modes, including for a file in no repository at all;Repository.show('HEAD', path)andstate.onDidChangebehave as assumed (the change event is coarse — key the re-read offstate.HEAD.commit); a nested repo may need explicitopenRepository()discovery beforegetRepository()resolves. - Machine-edit seam — the single boundary where machine text enters the model:
proposeAgentEdit(docUri, range, newText, provenance)→ produces pendingproposals[]entries; nothing settles until Keep (INV-5). Block/word granularity via the pure diff engine (diffToBlockHunks/wordEditHunks): a block (paragraph, header, list item, fenced code) is the decision unit a writer keeps or rejects; the word diff inside it is the attribution unit, so keeping a block attributes only the words that actually changed. - Pending-change controller — on new proposals, lands each optimistically in
the live buffer as decorated pending text (the model retains the proposal's
originalfor exact revert) and renders per-hunk ✓ Keep / ✗ Reject CodeLens (D18).keep(proposalId)= Accept: settles the change and attributes it — machine-proposed words to the machine, any writer tweaks (typed directly into the pending text, INV-12) to the writer, by comparing final ↔ the machine's original proposal ↔ baseline.reject(proposalId)restores the retained original exactly.keepAll()/rejectAll()apply descending, skip + report any displaced change. Pending changes re-anchor around writer edits; a displaced one collapses to whole-proposal Keep/Reject (D20).openReview(docUri)opens the native diff editor (baseline ⟷ live) as the sweep-everything reference view — read-only, author-blind (INV-11); decisions stay in the buffer. - Preview extension —
extendMarkdownIt(md) → md.use(cowritingPlugin); the plugin invokes the purerenderAnnotations(tokens, attributions, proposals, options)transform, gated by thecowriting.annotationssetting/command. - Turn runner —
runEditTurn(prompt, { onProgress, signal, env }) → AgentResult; pureturnProgressreducer;liveProgressrelay to a notification- OutputChannel; Cancel →
abort(). The machine runtime is loaded via runtime dynamicimport()(it is ESM and must not be bundled), and spawned with IDE auto-connect disabled so it never triggers an OS automation-permission prompt.
- OutputChannel; Cancel →
- CoeditingRegistry (the activation gate, INV-10) —
enter(docUri)/exit(docUri)/isCoediting(docUri) → boolean/onDidChange. Holds the active coediting set (persisted across reloads). Every controller above checksisCoediting(docUri)before attaching a surface: the QuickDiffProvider returns no original for non-coedited docs, the comment-response controller ignores them, and the annotation/edit commands are disabled (whenclausecowriting.isCoediting). - ThreadController + comment-response — the primary ask surface (D19) —
create/reply/resolve via the Comments API. The comment box is the editor's one
native multi-line input, so asking is commenting: the title-bar Ask
Claude action just creates a thread (on the selection, else whole-document) and
focuses its input; no bespoke input surface exists. On a coedited document only,
a new comment fires
runEditTurnto reply in the thread and, when the remark implies a change, offer an edit; an accepted offer routes through the machine-edit seam to a pending proposed change (INV-5) — the comment never edits the document directly. The reply is posted as a machineCommentcarryingonBehalfOfprovenance (INV-8). Threads can be created from an editor selection, a preview selection (mapped to the source range via themarkdown-ittoken source map — the preview shows a deep-linking 💬 marker but cannot host the thread; the Comments API renders only in the gutter + Comments panel), or on the whole document (anchored at line 0, full-document context). All threads are native — the preview never hosts comment UI (INV-3). Spike-verified API seams (E4, 2026-07-01): (1) the editor caches commenting ranges per document — when the CoeditingRegistry gate flips for an already-open document, the controller must re-assigncommentingRangeProvider(its setter is the API's only ranges-changed signal), or the+gutter and Add Comment stay stale ("cursor must be within a commenting range"); (2) there is no API to focus a thread's input — "Ask Claude" focuses the comment box by invoking the built-inworkbench.action.addComment(cursor moved to the top first for a whole-document ask), not bycreateCommentThread+Expanded; (3) the thread-contextValue menuwhenkey iscommentThread(notcommentThreadContextValue); the "make this edit" offer is carried as contributed thread actions (comments/commentThread/title+…/context), optionally reinforced by an in-body command link (requiresisTrusted.enabledCommands).
6.5 Per–Product-Use-Case design
PUC-1 — see the diff against the baseline checkpoint
sequenceDiagram
actor U as Writer
participant C as SCM/QuickDiff ctrl
participant B as Baseline provider
participant V as Native diff editor
U->>C: Mark Changes as Reviewed
C->>B: BaselineStore.pin(docUri, text)
U->>U: edits document (or the machine proposes)
C-->>U: gutter dirty-diff bars (provideOriginalResource)
U->>V: Open Changes
V->>B: read cowriting-baseline:docUri
V-->>U: native diff (baseline ⟷ live)
- Implementation: the QuickDiffProvider points at the
cowriting-baseline:URI; the platform renders gutter bars and powers "Open Changes" /vscode.diff. No custom diff rendering. Honors INV-3, INV-7.
PUC-2 — keep/reject the machine's proposed changes
sequenceDiagram
actor U as Writer
participant R as Pending-change ctrl
participant S as Machine runtime
participant E as Live editor buffer
U->>R: Ask the machine to revise
R->>S: runEditTurn(prompt, {onProgress, signal, env})
S-->>R: proposed text
R->>R: proposeAgentEdit → pending proposals[] (INV-5)
R->>E: land pending changes (decorated, original retained)
U->>E: tweak by typing · ✓ Keep / ✗ Reject per change (CodeLens)
E->>R: keep(p)/reject(p) · keepAll()/rejectAll()
R-->>U: Keep settles + attributes · Reject restores the original exactly
- Implementation: the machine emits pending proposed changes; each lands in
the live buffer as a decorated pending change with its original retained, and
nothing settles until Keep (INV-5). Per-hunk CodeLens carries ✓ Keep /
✗ Reject (with a Keep-all/Reject-all menu); keep-all/reject-all apply descending
and skip + report any displaced change. The native diff editor
(
baseline ⟷ live) is the sweep-everything reference view; the buffer is where decisions happen. Why in-buffer (D18): the rung-2 spike verified VS Code does not render extension CodeLens inside a diff editor, ruling out the v0.1.0 multi-diff Keep/Undo surface; the shipped F12 experience proved the in-buffer model end-to-end, and it matches the platform's own AI edit-session paradigm. - Editable before keep (INV-12): the pending change sits in the live buffer, so
the writer tweaks it by typing directly in it ("close, but a few human edits")
before keeping.
keepattributes the machine's words and the writer's tweaks separately by diffing final ↔ machine-original ↔ baseline, so authorship is correct at creation — the preview and the in-editor track-changes then show clean green/blue with no post-hoc untangling. Pending changes re-anchor around writer edits; one displaced by heavy rewriting collapses to a whole-proposal Keep/Reject (D20, resolving the old Q7).
Always-visible entry points (no Source Control navigation) — INV-13
- Implementation: reviewing changes must never require finding the Source
Control pane (writers working outside git —
~/Downloads, untitled — won't know it exists). Whencowriting.isCoeditingis set, the actions surface where the writer already is: editor title-bar buttons (Review Changes, Ask Claude, Toggle Annotations, and in snapshot mode Mark Changes as Reviewed) and a status-bar item (✦ Coediting · N changes, click → Review Changes). New proposals land in the buffer in front of the writer (D18 — the review comes to them), and the live-progress notification carries a Review action. The SCM viewlet entry and the gutter dirty-diff remain as native extras for git-literate users, never the required path.
PUC-3 — annotated rendered preview
sequenceDiagram
actor U as Writer
participant P as Built-in MD preview
participant E as Preview extension (extendMarkdownIt)
participant T as renderAnnotations (pure)
U->>P: Open Preview (annotations on)
P->>E: render document tokens
E->>T: transform(tokens, attributions, proposals)
T-->>E: tokens w/ authorship + change marks
E-->>P: annotated HTML (preview stylesheet)
P-->>U: native preview, annotated
- Implementation: a contributed
markdown-itplugin runs the purerenderAnnotationstransform and a small preview stylesheet using the editor's theme variables. Toggle viacowriting.annotations. No webview. Authorship coloring + insert/delete marks are shown for changes since the baseline checkpoint; a freshly entered or freshly pinned, zero-diff document renders clean.
PUC-4 / PUC-6 — threads & re-anchoring
- Implementation: ThreadController + Anchorer, gated on
isCoediting(docUri)(INV-10). On load / external change (FileSystemWatcher), every anchor re-resolves (exact-unique → context-disambiguated → displaced, INV-1). Proposed changes and attributions ride the sameanchorsprimitive, so they re-anchor and displace by the same ladder.
PUC-7 — enter / leave coediting
- Implementation:
cowriting.enterCoediting(docUri)adds the URI to the CoeditingRegistry's active set and persists it;cowriting.exitCoeditingremoves it (the sidecar is kept). The registry firesonDidChange; controllers (QuickDiff, comment-response, annotations, edit commands) attach/detach and thecowriting.isCoeditingcontext key flips so menuwhenclauses show/hide. Commands are markdown-gated. No surface exists for a document until it is entered.
PUC-8 — comment summons a machine reply + offer
sequenceDiagram
actor U as Writer
participant T as ThreadController
participant G as CoeditingRegistry
participant S as Machine runtime
participant R as Review ctrl
U->>T: add comment on a passage
T->>G: isCoediting(docUri)?
G-->>T: yes
T->>S: runEditTurn(comment + context, {onProgress, signal})
S-->>T: reply text (+ optional edit)
T-->>U: machine reply posted in thread (onBehalfOf provenance)
U->>T: accept the offered edit
T->>R: proposeAgentEdit → pending proposal (INV-5)
R-->>U: pending change lands in the buffer (PUC-2)
- Implementation: gated on
isCoediting(INV-10) — inert otherwise. The turn runs with live progress (PUC-5). The reply is a machineComment; an accepted offer never edits the document directly, only proposes (INV-5). While the document is in coediting mode, every new comment fires a turn (D10) — the opt-in mode is the control, so there is no per-comment gate; "Stop editing with Claude" is the off switch. A comment may originate at an editor selection, a preview selection (mapped to the source range via themarkdown-ittoken source map), or the whole document (top-anchored); the machine receives the passage — or the full document — as context accordingly.
PUC-5 — live machine progress
- Implementation: pure
turnProgressreducer;runEditTurnonProgress/AbortSignal;liveProgressrelay to a notification activity-line + token count + an OutputChannel; Cancel →abort(). The machine runtime is spawned with IDE auto-connect disabled so it never triggers an OS automation prompt.
6.6 Non-functional requirements & cross-cutting concerns
- Security & privacy: serverless; no secret bytes in sidecars or transcripts. The machine runs locally; provenance records identity, not credentials. The runtime is spawned with IDE auto-connect disabled (no spurious OS automation-permission prompt).
- Performance & scale: anchor resolution is O(document) text search per anchor on load/change — fine for human-scale documents; skip non-text/oversized files. Native diff/preview rendering is the platform's, not ours.
- Availability & resilience: no service; the worst case is a displaced anchor (handled, INV-1); a baseline always exists once coediting is entered (INV-7), so there is no missing-baseline path.
- Observability: the OutputChannel stream (PUC-5) is the user-facing turn log; no telemetry service (a local extension — §9).
- Accessibility: inherited from the native surfaces — the diff editor, CodeLens, Markdown preview, and Comments all carry the editor's accessibility. This is a primary reason for the native-first choice.
6.7 Key decisions & alternatives considered
| Decision | Chosen | Alternatives considered | Why chosen |
|---|---|---|---|
| Diff surface | QuickDiffProvider + native diff editor against a cowriting-baseline: virtual doc |
bespoke webview diff; decoration-only inline diff | Native gutter + diff editor for free: theming, inline/side toggle, a11y, navigation; zero custom rendering (INV-3) |
| Accept/reject surface (v0.2.0, D18 — supersedes the v0.1.0 multi-diff choice) | Pending changes in the live buffer with per-hunk ✓ Keep / ✗ Reject CodeLens, backed by the proposed-change model; native diff editor as the read-only sweep view | multi-diff editor with Keep/Undo (v0.1.0 choice — ruled out: the spike verified CodeLens does not render in a diff editor, and inline overlays lean on evolving APIs); a separate writable proposal document (spike E1 pivot — works, but puts an artifact between the writer and their document); source-control stage/unstage; bespoke webview ✓/✗ | The platform's own AI edit-session paradigm: review the change in the document, tweak by typing, revert in one gesture. Shipped and proven end-to-end (F12); all-stable APIs (decorations + CodeLens) |
| Rendered preview | Extend the built-in Markdown preview (markdown-it contribution) |
bespoke sealed webview; both surfaces | Native scroll-sync, theming, refresh, and cooperation with the writer's other Markdown extensions; no webview to own (INV-3) |
| Baseline source | follows the file's home (INV-7): git HEAD where tracked (resets on commit, storage-free), a snapshot (set on entry, re-pinnable; global storage / in-memory) where there is no commit to anchor to — outside any repo, untracked, or untitled |
always a stored snapshot; always defer to git (breaks outside a repo) | Git-tracked files get the native "uncommitted work, reset on commit" diff for free; ~/Downloads / untitled still work via the snapshot fallback. Mirrors the in-repo/out-of-repo SidecarRouter |
| Persistence envelope | one forward-compatible, unknown-field-preserving sidecar (INV-4) | a bespoke per-feature format; preview-only ephemeral state | Round-trips to an outer review forge; survives reload; one shared anchors/provenance primitive |
| Core/controller split | pure vscode-free cores + thin controllers | UI-coupled logic | Unit-testable cores, swappable surfaces; the surface can change without touching the logic (INV-6) |
| Decision vs attribution unit | block = decision unit (keep/reject); word = attribution unit | per-word proposals (too granular to decide on); per-file (all-or-nothing) | The writer decides at a readable grain (a paragraph) while authorship stays word-precise |
| Authorship surface (v0.2.0, D21 — updates D11 to shipped reality) | the annotated preview and the in-editor author-colored track-changes decorations (both shipped); native diff stays author-blind (INV-11) | preview-only (v0.1.0 D11 — overtaken: the in-editor decorations shipped 2026-06-27 and earn their keep); Option 3 (owned author-colored track-changes diff replacing native diff) | Both surfaces ride the same pure render/attribution cores; the writer gets authorship where they read (preview) and where they write (editor). Opt 3 still out |
| Editing proposals | pending changes are tweaked by typing directly in the buffer; pre-Keep tweaks are absorbed and attributed (machine vs. writer) at Keep (INV-12) | a separate writable proposal document (v0.1.0); machine proposal read-only (tweak only after accepting) | Most revisions are "close, but a few human edits"; editing in place, at review time, keeps authorship clean at creation with no extra artifact and no messy post-acceptance untangling |
| Ask surface (v0.2.0, D19) | comments-first — the native comment thread input is the ask input; title-bar "Ask Claude" creates/focuses a thread (selection or whole-document) | the shipped bespoke split-pane input webview (sunset — one custom surface eliminated); single-line InputBox (too small for real asks) |
The Comments API is the editor's one native multi-line input; ask-for-an-edit and remark-on-a-passage collapse into a single familiar gesture, and the machine's answer lands in the same thread (D8/D10) |
| Coediting activation | opt-in per document via "Coedit this Document with Claude"; every surface gated on the active coediting set (INV-10) | always-on for all markdown; opt-in per workspace/folder | Per-document opt-in is the only grain that avoids hijacking Comments/SCM/preview for the writer's other files while keeping single-file precision |
| Change discovery (resolves Q3) | primary entry = editor title bar + status-bar item, and proposals land in the buffer in front of the writer (D18); the SCM viewlet + gutter are optional extras (INV-13) | rely on the Source Control pane / gutter as the entry point | Writers working outside git won't find (or know) the Source Control pane; always-visible title-bar / status-bar actions need no git familiarity |
| Authorship in the diff | the native diff is author-blind (what changed); authorship is shown in the preview, optionally a decoration track-changes editor (INV-11) | recolor machine changes inside the native diff editor | Not possible: diffEditor.* colors are global theme tokens with no per-author scoping; inlineEdit.*/chat tokens differentiate accept-state not author; decorations over the diff pane composite over green/red and are unreliable (vanish on tab-switch). Verified against the VS Code API (§13-Q5) |
| Comment → machine response | on a coedited doc, a comment fires a turn that replies + offers an edit → pending proposal | comment is inert (human-only threads) | Turns the familiar review-comment gesture into a coediting action without a new surface; the opt-in gate (INV-10) keeps it from firing on other docs |
| Comments & the preview | comments are native (source gutter + Comments panel); the preview can initiate a comment (selection → source range via the markdown-it source map) and show a deep-linking 💬 marker, but cannot host the thread (it's a webview — verified) |
render a bespoke comment widget inside the preview webview | Keeps comments on the native surface (no INV-3 exception, keeps the Comments panel + gutter); the writer can still comment on what they're reading and add whole-document comments (top-anchored) |
| Scope & packaging (v0.2.0, D17 — resolves Q1) | north star with a committed vehicle: the shipped extension evolves in place onto these surfaces (pure cores retained, webview surfaces sunset) | greenfield extension importing the cores as a library (re-derives wiring + E2E harness the shipped repo already has); stay design-only (stalls — §7.3) | ~60% of the shipped code (21 pure modules, ~3.5k LOC) is this design's cores already; evolve-in-place keeps tests, history, and continuous shipping |
6.8 Testing strategy
- Unit (vitest, vscode-free): the pure cores — attribution spans, anchoring
(fingerprint/resolve/displaced/offset shift),
mergeArtifacts(union-by-id, unknown-field preservation), the diff engine (wordEditHunks/diffToBlockHunks), theturnProgressreducer, and therenderAnnotationsmarkdown-it transform (token-in → annotated-token-out). - Integration / E2E (
@vscode/test-electron): drive the controllers in the Extension Development Host — mark-reviewed → assert checkpoint reset; ask-to-edit → assert pending proposals landed decorated in the buffer with originals retained; Keep/Reject → assert Keep settles + attributes and Reject restores the original text exactly; toggle annotations → assert preview HTML marks; thread create → re-anchor → displaced. Where the harness cannot drive a native surface's chrome directly (CodeLens clicks, Comments UI, preview DOM), assert controller/provider state and pure-transform output instead. No Playwright — these are native surfaces, not a webview. E2E are first-class plan tasks (handbook §4 / §6.8). The shipped repo's 19 E2E suites carry forward under evolve-in-place (D17). - Named E2E scenarios this design must pass: PUC-1 pin→diff; PUC-2 propose→tweak→keep/reject→keep-all (incl. reject-restores-exactly); PUC-3 annotate-toggle; PUC-4/6 thread→reanchor→displaced; PUC-5 progress→cancel.
6.9 Failure modes, rollback & flags
- Displaced anchor (INV-1) → shown displaced at last-known
lineHint, never moved → auto-re-anchors if the text reappears → no alert (local). - Empty document (genuinely no content) → the only true empty state → a gentle "start writing, or ask Claude to draft something" affordance; not an error. A coedited document always has a baseline (set on entry, INV-7), so there is no "missing baseline" state to handle.
- Save while changes are pending → the buffer (and on save, the disk file) contains visibly-pending machine text; safe because every pending proposal's original is retained in the sidecar and Reject restores it exactly after reload (INV-5, re-anchored on load per INV-1/PUC-6).
- Feature flags / settings:
cowriting.annotations(preview annotations on/off),cowriting.liveProgress.revealOutput. The whole capability is additive — off by absence of coediting mode for the document (INV-10).
6.10 Migration & compatibility
Evolve in place (D17, resolves Q1). The v0.1.0 draft called this greenfield; that undersold the shipped extension: ~60% of its code — 21 pure, vscode-free modules (~3.5k LOC) — already implements this design's cores (attribution span algebra, fingerprint anchoring, the sidecar envelope + router + merge, the block/word diff engine, the render/annotation transform, the turn-progress reducer, the machine turn runner). The delivery vehicle is therefore the shipped extension itself, migrated surface by surface:
- Retained as-is (the cores): the 21 pure modules and their unit suites; the
sidecar contract (INV-4) and stores; the
@cline/sdkturn runner + live progress; the in-buffer pending-change experience (F12) and the in-editor author-colored track-changes decorations — both already match this design (D18, D21). - Rewired (thin controllers): baseline moves behind the router (git
HEAD+ snapshot, INV-7 — today it is snapshot-only); a QuickDiffProvider +cowriting-baseline:content provider replace nothing (net-new native surface); the CoeditingRegistry opt-in gate (INV-10) replaces the current always-on-for- markdown behavior; ThreadController gains the comment-response loop (D8/D10). - Sunset (the bespoke webview stack, ~800 LOC): the sealed review-panel
webview (
trackChangesPreview+ its client) is replaced by the extended built-in Markdown preview (D3); the split-pane ask-input webview is replaced by comments-first asking (D19). Each sunset lands only after its native replacement is green. - Data migration: none needed — the sidecar envelope carries forward (additive, unknown-field-preserving, INV-4). The baseline (out-of-repo, disposable) needs no migration ever.
- Stable-API boundary: every surface this design now stands on — diff editor,
SCM/QuickDiff, Markdown-preview
markdown-itcontribution, Comments, decorations, CodeLens — is stable API. The one evolving-API worry from v0.1.0 (per-change overlays in a multi-diff review) is gone with the in-buffer decision (D18).
7. Delivery Plan
7.1 Approach / strategy
Delivery proceeds in four de-risking rungs, riskiest-API-assumption first, so any forced design change is discovered on throwaway code rather than after the full build (the explicit goal: post-ship iteration is polish, not design). The vehicle is the shipped extension, evolved in place (D17/§6.10):
- Static mockups (done). Validate visual design + state coverage. (Already surfaced and closed the empty-state, comments-from-preview, and discoverability gaps.)
- Throwaway feasibility spike (complete — 5 of 5 experiments, 2026-07-01). A
disposable VS Code extension (
vscode-cowriting-prototype; the machine stubbed with canned edits) proving the API-dependent assumptions on the real surfaces. Companion:coauthoring-feasibility-spike.md. Findings:- E1 (editable proposals + Keep/Undo) — PASS with pivot. VS Code does not render extension CodeLens inside a diff editor, ruling out the multi-diff review surface; per-hunk CodeLens works in a normal editor, and tweak-then-Keep attributes machine words vs. writer edits correctly. Folded back as D18 (with the shipped F12 experience): the review surface is the live buffer itself.
- E3 (preview authorship) — PASS. The
markdown-itcontribution injects author-colored marks + deletions into the built-in preview, theme-aware, with a working toggle (D3 confirmed). - E5 (discoverability) — PASS. Title-bar + status-bar entry points read as obvious with no Source Control pane (INV-13 confirmed).
- E2 (baseline router) — PASS. QuickDiff gutter bars + the native diff
render in both modes, driven end to end: git
HEAD(tracked file — the baseline advanced automatically on commit) and snapshot (a file in no repo at all — "Mark Changes as Reviewed" re-pinned and the diff went clean). INV-7/D13/D14 confirmed; API notes folded into §6.4. - E4 (comment → reply → offer → proposal) — PASS with fallback. The full
loop works on the real Comments API (thread → turn with progress/cancel →
in-thread machine reply → offer → pending in-buffer proposal). Three API
seams required workarounds, folded into §6.4: commenting-range refresh on
gate change, input focus only via
workbench.action.addComment, and thecommentThreadmenu when-key. (The preview-selection → source-thread half of E4/D15 was not spiked — confirm at the preview-surface build.) - The spike is complete; delivery moves to rung 3 (vertical slice) / rung 4.
- Vertical slice. One real end-to-end flow (enter → ask via a comment thread
with the real
@cline/sdk→ real pending change in the buffer → tweak → keep) on a real document, minimal breadth — proving the loop works for real once E2/E4 are validated. - Full build (the evolve-in-place migration, §6.10). The implementation plan decomposed from this (spike-revised) spec, migrated surface-by-surface in the validated order: baseline router + QuickDiff/diff surface (E2) → coediting opt-in gate → preview extension replaces the review-panel webview → comments-first ask + comment-response loop replaces the input webview (E4) — each bespoke surface sunsetting only after its native replacement is green. One SPEC → one plan → one execution per increment.
7.2 Rollout / launch plan
A VS Code extension has no §9 webapp deploy pipeline (localhost/PPE/prod) — it is not a webapp. "Done" for a delivered increment = acceptance met (§1.9) + unit and host E2E green (§6.8). Marketplace packaging/release is out of scope here.
7.3 Risks & mitigations
| Risk | Likelihood / impact | Mitigation |
|---|---|---|
| Baseline router assumptions fail on real git repos | retired | E2 PASSed (2026-07-01) — both baseline modes verified on the real API; snapshot mode remains the always-works fallback (INV-7) |
| Comment-response loop feels laggy or noisy with the real SDK (E4 spiked with a ~2s stub) | M / M | E4 PASSed on feel with the stub; real-turn latency lands at the rung-3 vertical slice; D10's off-switch ("Stop editing with Claude") bounds the blast radius |
| Save-while-pending leaves machine text on disk before a decision | M / L | Pending state + originals persist in the sidecar; Reject restores exactly after reload (§6.9); status-bar count keeps pending changes visible |
| Two source controls (Git + Cowriting) confuse the SCM viewlet | L / M | Multiple SCM providers coexist by design; name it clearly; QuickDiff alone (no full SCM) is the minimal fallback |
| Built-in preview can't host interactive ✓/✗ inside the preview | M / L | Accept/reject lives in the buffer (D18), not the preview; the preview is read/annotate only (a deliberate split) |
| Native surfaces are harder to E2E than a queryable webview | M / L | Assert controller/provider state + pure-transform output; the shipped E2E harness carries forward (D17) |
| Evolve-in-place migration destabilizes a working product mid-flight | M / M | Surface-by-surface order (§7.1 rung 4): each bespoke surface sunsets only after its native replacement is green; the pure cores never move |
8. Success Metrics & Measurement
8.1 Business success metrics
| Outcome | Measured by (source) | Baseline captured how | Owner | Review cadence |
|---|---|---|---|---|
| BO-1 (adoption for real docs) | Dogfooding: complete revise→review→keep/reject→preview without leaving the editor or copy-pasting | The copy-paste-from-chat flow as the before | Ben | At first build |
| BO-2 (feels built-in) | The diff, accept/reject, and preview use surfaces the writer already knows | Bespoke-UI alternatives as the before | Ben | At first build |
8.2 Product success metrics
| # | Metric (scores PUC-n) | Target | Fed by | Review cadence |
|---|---|---|---|---|
| PM-1 | PUC-1 diff opens in the native diff editor with an accurate baseline⟷live | works for any file incl. untitled | host E2E | per build |
| PM-2 | PUC-2 no proposed change settles until Keep; Reject restores the original exactly; each change independently keep/reject-able | 100% (INV-5) | host E2E | per build |
| PM-3 | PUC-3 annotations render in the built-in preview and toggle clean | on/off both correct | unit + host E2E | per build |
9. Telemetry, Observability & Monitoring
A local VS Code extension — no telemetry service, no server monitoring. The user-facing turn log is the OutputChannel (PUC-5). Deliberately omitted: events pipeline, dashboards, alerts, SLOs (nothing is operated as a service). N/A — TEL/SLO/ALR rows are not applicable to a serverless local extension.
10. Operations
10.1 Operational plan
N/A — a local VS Code extension has no day-to-day operator; the "operator" is the developer running it. No on-call, no service runbooks. Routine maintenance is dependency patching (§10.3).
10.2 Support & incident response
Pre-v1; issues are filed on the project tracker and triaged in a session. One line: a bug reaches us as a tracker issue.
10.3 Maintenance & lifecycle
- Dependency patching: the machine-coauthor runtime and dev deps via the normal tracker flow.
- Backup / restore: N/A — sidecars are git-committed; the baseline is disposable.
- Capacity & cost: local; the only cost is the machine's token usage, surfaced live (PUC-5).
- Deprecation / sunset: this design is superseded only by a later architecture spec.
11. Documentation plan
- Where docs live: spec + dev docs → framework repo; this Solution Design →
content repo
specs/; no public-facing site in scope.
| # | Document | Audience | Location | New / updated |
|---|---|---|---|---|
| DOC-1 | This Solution Design | dev/operator | content repo specs/coauthoring-native-surfaces.md |
new |
| DOC-2 | README "how coediting maps to native editor surfaces" | dev/user | framework repo README | new (at delivery) |
12. Traceability matrix
| Pain | Business UC | Product UC | Tests | Measured by |
|---|---|---|---|---|
| PP-1 | BUC-1 | PUC-1 | host E2E: pin→QuickDiff→native diff | PM-1 / BO-1 |
| PP-2 | BUC-2 | PUC-2 | host E2E: propose→review→keep/undo→keep-all | PM-2 / BO-1 |
| PP-3, PP-4 | BUC-3 | PUC-3 | unit (renderAnnotations) + host E2E: annotate toggle | PM-3 / BO-1 |
| PP-5 | BUC-4 | PUC-5 | host E2E: progress→cancel | BO-1 |
| (displacement) | BUC-3a | PUC-4/6 | unit (anchorer) + host E2E: reanchor→displaced | PM-1 |
| PP-1 (comment→edit) | BUC-5 | PUC-8 | host E2E: comment→reply→offer→pending proposal | BO-1 |
| (no hijack) | — | PUC-7 | host E2E: enter/exit gates every surface; non-coedited doc inert | BO-2 |
13. Open Questions & Decisions log
Open
| # | Question | Owner | Blocks |
|---|---|---|---|
| Q4 | Does any need genuinely require a webview the native surfaces can't serve (INV-3 escape hatch)? — narrowed at v0.2.0: the two shipped webviews both have native replacements (D3, D19); the one candidate left is mermaid rendering in the preview, expected to ride markdown.previewScripts (the markdown-mermaid pattern), to be confirmed at the preview-surface build |
Ben | Per-surface build |
Resolved
| # | Decision | Resolution | Date |
|---|---|---|---|
| D1 | Scope / altitude | Design-only north star — architecture + surface mapping, no packaging/migration commitment. (Amended by D17: the packaging commitment is now taken.) | 2026-06-27 |
| D2 | Accept/reject primary surface | Multi-diff editor with Keep/Undo (CodeLens stable fallback). (Superseded by D18: the spike showed CodeLens cannot render in a diff editor.) | 2026-06-27 |
| D3 | Rendered preview surface | Extend the built-in Markdown preview via a markdown-it contribution (no webview) |
2026-06-27 |
| D4 | Diff surface | QuickDiffProvider + native diff editor against a cowriting-baseline: virtual doc |
2026-06-27 |
| D5 | Core capabilities | Pinned baseline, char-precise authorship attribution, durable git-native sidecar, live cancellable progress, region threads, on-behalf-of identity, pure-core/thin-controller discipline | 2026-06-27 |
| D6 | Design tooling | Code directly (rubric −6); no bespoke UI surface | 2026-06-27 |
| D7 | Coediting is opt-in per document | A document enters coediting via "Coedit this Document with Claude"; every surface gates on the active coediting set (INV-10), so other markdown files are never hijacked | 2026-06-27 |
| D8 | Comments can summon the machine | On a coedited doc, a comment fires a turn that replies in-thread + offers an edit → pending proposal (BUC-5, INV-5); inert on non-coedited docs | 2026-06-27 |
| D9 | Native diff is author-blind | Verified the diff editor has no per-author color API; authorship lives in the preview (and optionally a decoration track-changes editor), not the native diff (INV-11) | 2026-06-27 |
| D10 | Comment trigger | While a document is in coediting mode, every new comment summons the machine (no per-comment gate); coediting mode is the control, "Stop editing with Claude" the off switch | 2026-06-27 |
| D11 | Authorship surface (Q5) | Option 1 — the annotated preview is the authorship view; native diff stays author-blind; in-editor decorations (opt 2) and an owned track-changes diff (opt 3) are out of scope. (Updated by D21: opt 2 shipped and is retained.) | 2026-06-27 |
| D12 | Editable proposals | The review's proposed side is writable; the writer tweaks before Keep; machine words vs. writer tweaks are attributed at Keep (INV-12); live doc untouched until Keep (INV-5). (Amended by D18/D20: the writable surface is the live buffer itself.) | 2026-06-27 |
| D13 | Baseline semantics & source | Baseline = the last settled version. Its source follows the file's home: git HEAD where tracked (auto-resets on commit, storage-free), else a snapshot set on entry (global storage / in-memory). A coedited doc always has a baseline; the only empty state is a genuinely empty document (INV-7) |
2026-06-27 |
| D14 | Pin is the non-git fallback only (resolves Q8) | "Mark Changes as Reviewed" exists only in snapshot mode (no committed HEAD); git-tracked files have no manual pin — the writer commits and the baseline follows HEAD. The pin command is when-gated on cowriting.baselineMode == snapshot |
2026-06-27 |
| D15 | Comments & the preview | Native comment threads can't render in the preview (it's a webview — verified). The preview initiates a comment (selection → source range via the markdown-it source map) and shows deep-linking 💬 markers; a whole-document comment anchors at the top. Threads stay native (gutter + Comments panel); no bespoke in-webview comment UI |
2026-06-27 |
| D16 | Change discovery (resolves Q3) | No flow requires the Source Control pane (INV-13). Primary entry = editor title-bar actions + ✦ Coediting · N changes status-bar item + auto-open review; SCM viewlet + gutter kept as optional git-user extras. (v0.2.0: "auto-open review" becomes "proposals land in the buffer in front of the writer," D18.) |
2026-06-27 |
| D17 | Packaging & delivery vehicle (resolves Q1) | Evolve the shipped extension in place onto the native surfaces: the 21 pure core modules (~3.5k LOC, ~60%) are retained as-is, thin controllers are rewired, and the two bespoke webviews sunset only after their native replacements are green (§6.10). Greenfield rejected (re-derives wiring + E2E harness); staying design-only rejected (stalls) | 2026-07-01 |
| D18 | Accept/reject surface (resolves Q2; supersedes D2) | Pending changes live in the buffer — proposals land optimistically as decorated pending text (originals retained), decided by per-hunk ✓ Keep / ✗ Reject CodeLens, tweaked by typing. Grounds: the spike verified CodeLens does not render in a diff editor (killing the multi-diff surface); the shipped F12 proved the in-buffer model end-to-end; it matches the platform's own AI edit-session paradigm and uses only stable APIs. INV-5 reworded from "buffer untouched" to "nothing settles until Keep; Reject restores exactly" | 2026-07-01 |
| D19 | Ask surface | Comments-first: the native comment thread input is the ask input for both imperative asks and remarks; the title-bar "Ask Claude" creates/focuses a thread (selection, else whole-document); the shipped split-pane input webview sunsets. One custom surface eliminated; ask and discuss collapse into one native gesture (D8/D10) | 2026-07-01 |
| D20 | Tweaks × per-hunk decisions (resolves Q7; amends D12) | Free editing happens in the live buffer; each pending change re-anchors around writer edits so per-hunk Keep/Reject survives ordinary tweaking; a change displaced by heavy rewriting collapses to a whole-proposal Keep/Reject (skip + report in keep-all/reject-all) — the INV-1 discipline applied to proposals | 2026-07-01 |
| D21 | Authorship surfaces (updates D11) | The annotated preview and the in-editor author-colored track-changes decorations are both authorship surfaces (the latter shipped 2026-06-27, after D11 deferred it); both ride the same pure cores; the native diff stays author-blind (INV-11). Option 3 (owned track-changes diff) remains out | 2026-07-01 |
14. Glossary & References
- Baseline (checkpoint) — the last settled version, used as the diff original.
Its source follows the file's home: git
HEADfor tracked files (resets on commit), else a snapshot set on entry and reset by "Mark Changes as Reviewed." Disposable; never committed by us (INV-7). - QuickDiffProvider — the VS Code API that supplies gutter dirty-diff bars from a virtual original document.
- Pending change — a machine-proposed edit landed optimistically in the live buffer as decorated, unsettled text (original retained); decided by per-hunk ✓ Keep / ✗ Reject CodeLens, tweaked by typing (D18). The accept/reject surface.
- Coauthoring sidecar — the forward-compatible, unknown-field-preserving artifact that persists anchors, authorship, proposed changes, and threads, and round-trips to an outer review forge.
- Provenance — the author envelope (the human's git identity, or the machine acting on-behalf-of a human) reused across attributions, proposed changes, and threads (INV-8).
- Displaced — an anchor that could not be confidently re-resolved; shown, never silently moved (INV-1).
- Decision unit vs attribution unit — a block (paragraph/header/list-item/fence) is what the writer keeps or rejects; the word diff inside it is what carries authorship.
- Pure core / thin controller — the vscode-free, unit-tested logic vs. the vscode-aware wiring split (INV-6).
- References: VS Code APIs — SCM/
QuickDiffProvider, diff editor (vscode.diff), Markdown previewmarkdown-itcontribution, Comments,TextDocumentContentProvider, decorations, CodeLens. Prior art (patterns proven in earlier coauthoring work, reused here on their own merits): baseline pinning, char-precise attribution, the portable coauthoring sidecar, live turn progress, and editable proposals (tweak-before-keep with mixed human/machine authorship).