---
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 `HEAD` for
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.**
```gherkin
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.**
```gherkin
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.**
```gherkin
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.**
```gherkin
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.**
```gherkin
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
```gherkin
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
```
```gherkin
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
```
```gherkin
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
```
```gherkin
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)
```
```gherkin
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
```
```gherkin
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
```
```gherkin
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)
```
```gherkin
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 changes` status-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`, and
`threads` share 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 `vscode` import, unit-tested in
isolation; controllers are the only `vscode`-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 committed `HEAD` to anchor to) — for git-tracked
files there is no manual pin; the writer **commits** and the baseline follows
`HEAD`. A new in-repo file is in snapshot mode until its first commit, then flips to
`HEAD`-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 acting
`onBehalfOf` a 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**.
```mermaid
flowchart LR
subgraph Cores["Pure cores (vscode-free, unit-tested)"]
AT[Attribution tracker
char-precise spans]
AN[Anchorer
fingerprint + offsets + displaced]
MG[mergeArtifacts
union-by-id, unknown-field-safe]
DF[Diff/hunk engine
word- & block-level]
TP[turnProgress
reducer]
RA[renderAnnotations
markdown-it transform]
end
subgraph Ctrls["Thin controllers (vscode-aware)"]
SC[SourceControl + QuickDiffProvider]
BP[Baseline provider
TextDocumentContentProvider]
PR[Pending-change controller
in-buffer decorations + CodeLens]
PV[Preview extension
extendMarkdownIt]
TC[ThreadController
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
pending changes + CodeLens]
MDPREV[Built-in Markdown preview]
COMM[Comments gutter/peek]
NOTE[Notification + OutputChannel]
end
SDK[[Machine-coauthor runtime
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 `TextDocumentContentProvider` for the
`cowriting-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-it` so the pure `renderAnnotations`
transform 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/.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.
```jsonc
// .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": , "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": , "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 `TextDocumentContentProvider` for
`cowriting-baseline:` whose `provideTextDocumentContent(uri)` resolves the *original*
by the file's home (INV-7): **git-tracked → the `HEAD` blob** (read via the built-in
Git extension API; auto-advances on commit, watched via
`repository.state.onDidChange`); **otherwise → a stored snapshot**
(`BaselineStore.snapshot(docUri, text, reason)` / `get(docUri)`), set on
`enterCoediting` (reason `entered`) and on **"Mark Changes as Reviewed"** (reason
`pinned`). The **pin command is gated to snapshot mode** — hidden/disabled for
`HEAD`-anchored files (`when: cowriting.baselineMode == snapshot`), where committing
is the reset. This mirrors the in-repo/out-of-repo `SidecarRouter` (§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)` and `state.onDidChange` behave as assumed (the
change event is coarse — key the re-read off `state.HEAD.commit`); a **nested**
repo may need explicit `openRepository()` discovery before `getRepository()`
resolves.
- **Machine-edit seam** — the single boundary where machine text enters the model:
`proposeAgentEdit(docUri, range, newText, provenance)` → produces **pending**
`proposals[]` 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
`original` for 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 pure `renderAnnotations(tokens, attributions, proposals,
options)` transform, gated by the `cowriting.annotations` setting/command.
- **Turn runner** — `runEditTurn(prompt, { onProgress, signal, env }) →
AgentResult`; pure `turnProgress` reducer; `liveProgress` relay to a notification
+ OutputChannel; Cancel → `abort()`. The machine runtime is loaded via runtime
dynamic `import()` (it is ESM and must not be bundled), and spawned with IDE
auto-connect disabled so it never triggers an OS automation-permission prompt.
- **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 checks
`isCoediting(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 (`when` clause `cowriting.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 `runEditTurn` to **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
machine `Comment` carrying `onBehalfOf` provenance (INV-8). Threads can be created
from an **editor selection**, a **preview selection** (mapped to the source range
via the `markdown-it` token 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-assign `commentingRangeProvider`** (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-in
**`workbench.action.addComment`** (cursor moved to the top first for a
whole-document ask), not by `createCommentThread` + `Expanded`; (3) the
thread-contextValue menu `when` key is **`commentThread`** (not
`commentThreadContextValue`); the "make this edit" offer is carried as contributed
thread actions (`comments/commentThread/title` + `…/context`), optionally
reinforced by an in-body command link (requires `isTrusted.enabledCommands`).
### 6.5 Per–Product-Use-Case design
#### PUC-1 — see the diff against the baseline checkpoint
```mermaid
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
```mermaid
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. `keep` attributes 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). When `cowriting.isCoediting` is 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
```mermaid
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-it` plugin runs the pure
`renderAnnotations` transform and a small preview stylesheet using the editor's
theme variables. Toggle via `cowriting.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 same `anchors` primitive, 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.exitCoediting` removes
it (the sidecar is kept). The registry fires `onDidChange`; controllers (QuickDiff,
comment-response, annotations, edit commands) attach/detach and the
`cowriting.isCoediting` context key flips so menu `when` clauses show/hide. Commands
are markdown-gated. No surface exists for a document until it is entered.
#### PUC-8 — comment summons a machine reply + offer
```mermaid
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 machine `Comment`; 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 the `markdown-it` token 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 `turnProgress` reducer; `runEditTurn`
`onProgress`/`AbortSignal`; `liveProgress` relay 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`), the `turnProgress` reducer, and the `renderAnnotations`
markdown-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/sdk` turn 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-it` contribution, 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):
1. **Static mockups (done).** Validate visual design + state coverage. *(Already
surfaced and closed the empty-state, comments-from-preview, and discoverability
gaps.)*
2. **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-it` contribution 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 the
`commentThread` menu 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.**
3. **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.
4. **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 `HEAD` for 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 preview `markdown-it` contribution, 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).*