Files
vscode-cowriting-plugin-con…/specs/coauthoring-document-edit-flow.md
T

593 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: graduated
---
# Solution Design: The Ask-Claude Document-Edit Flow (#42 · #47 · #46)
| | |
| --- | --- |
| **Author(s)** | Ben Stull (with Claude) |
| **Reviewers / approvers** | Ben Stull |
| **Status** | `draft` |
| **Version** | v0.1.0 |
| **Source artifacts** | Features `benstull/vscode-cowriting-plugin#42` (Ask Claude to Edit Document, `type/feature`, `priority/P2`) · `#47` (block-granularity proposals, `type/feature`, `priority/P1`) · `#46` (accept-all, `type/feature`, `priority/P2`) · Epic `#1` (closed) · Capture sessions `vscode-cowriting-plugin-0035` (#42), `0040` (#47), `0039` (#46) · Brainstorming session `vscode-cowriting-plugin-0041` · Builds on (all shipped): F4 `#12` (propose/accept seam), F10 `#29` (interactive review preview), F11 `#43` (preview toolbar + `editDocument`/`runEditAndPropose`) · Parent specs (graduated): `coauthoring-propose-accept.md`, `coauthoring-attribution.md`, `coauthoring-interactive-review.md`, `coauthoring-rendered-preview.md` · F11 design currently lives in code + issue draft `issues/preview-toolbar-interaction-surface.md` (un-graduated — see §9) · Lineage: `ben.stull/rfc-app#48` |
**Change log**
| Date | Version | Change | By |
| --- | --- | --- | --- |
| 2026-06-12 | v0.1.0 | Initial draft — brainstorming session 0041. One combined design for the document-edit cluster, sequencing #42#47#46. Two forks locked with the operator: per-block proposals **preserve unchanged-span attribution** (block = decision unit, word = attribution unit); the cluster ships as **one** Solution Design. | Ben Stull + Claude |
---
## 1. Business Context
### 1.1 Executive Summary
The plugin can already ask Claude to edit a whole document (F11 shipped
`cowriting.editDocument``runEditAndPropose({kind:"document"})`), but the flow
around that capability is rough on three sides, each captured as its own feature:
- **You can't easily reach it** (#42). "Ask Claude to Edit" only appears in the
editor body **when text is selected**. Right-clicking the body with *no*
selection, or right-clicking the editor **tab**, offers nothing — two natural
"do something with this document" gestures hit a dead end.
- **What comes back is unreviewable** (#47, **P1**). A whole-document rewrite is
diffed at **word** granularity (`diffToHunks`, INV-37): a light copy-edit pass
explodes into dozens of tiny blue ✓/✗ blocks, forcing the reviewer to
adjudicate word-fragments out of the sentence they live in.
- **Taking the result is tedious** (#46). Accepting Claude's edits is **one
proposal at a time**; a document a writer wants wholesale still demands a click
per proposal.
This design treats the three as **one flow****reach → review → accept** — and
ships them in that order. **#42** makes the document edit reachable from the
obvious gestures; **#47** cuts the rewrite into **one proposal per changed block**
(paragraph / header / bullet / fence) so the reviewer decides in the unit they
think in; **#46** adds a single **Accept all** gesture. The whole flow is built on
the existing F4 propose/accept seam and the F10/F11 preview — **no new edit path,
no new accept mechanism, no new persisted model.**
### 1.2 Background
The inner loop shipped F2F5 (threads · attribution · propose/accept ·
cross-rung). F10 (`#29`) collapsed the review surfaces into one: a **clean
editor** + the **rendered markdown preview** as the single annotated review
surface, with ✓/✗ acting only on pending F4 proposals (INV-32..34). F11 (`#43`)
made the preview's **toolbar the primary interaction surface** — a **Pin
baseline** button and an **adaptive Ask-Claude** button — and, as part of that,
added the host-side **`cowriting.editDocument`** command and the
**`runEditAndPropose`** path that diffs a whole-document rewrite into per-hunk F4
proposals (INV-35 sealed-webview intent, INV-36 `data-src` block→source mapping,
INV-37 word-level per-hunk granularity).
So the *capability* exists; this cluster is about the **experience around it**.
Three captures landed it:
- **0035 → #42**: "Add 'Ask Claude to Edit Document' on right-click with no
selection; same Edit-Selection/Edit-Document on the tab."
- **0040 → #47**: "A single change should be a paragraph (or header, or
bulletpoint), not individual words. It's too much to review."
- **0039 → #46**: "We need a way to accept all of Claude's edits."
The captures themselves flagged that these "form an edit-flow cluster — brainstorm
/ sequence together," which this session does.
### 1.3 Business Actors / Roles
- **Coauthor (human)** — the writer/engineer; the sole user of every gesture
here (right-click to ask, ✓/✗ to adjudicate, Accept-all to commit).
- **Coauthor (machine)** — Claude via `@cline/sdk`; produces the document
rewrite that becomes block-level proposals; its accepted blocks land
Claude-attributed (word-precise — §6, INV-40).
### 1.4 Problem Statement
How do we make "ask Claude to revise this whole document, look at what it did,
and take the parts I want" a **fluid, reviewable, low-friction** loop — when today
it is **hard to invoke, exploded into word-fragments, and accepted one click at a
time** — **without** forking a second edit path, a second accept mechanism, or a
new persisted artifact?
### 1.5 Pain Points
| # | Pain | Felt by | Today |
| --- | --- | --- | --- |
| P-1 | No whole-document ask from the body (no selection) or the tab | Mouse-first / tab-oriented writers | Dead-end gestures |
| P-2 | A document edit fans out into many word-level proposals | Anyone reviewing a document edit (worse the lighter the edit) | "Too much to review" (#47, P1) |
| P-3 | Accepting many proposals is one click each | Writers who want Claude's whole pass | Manual sweep of ✓ |
| P-4 | Block-level accept could lose attribution fidelity inside a block | The product's F3 attribution value | (a design risk this spec resolves — INV-40) |
### 1.6 Targeted Business Outcomes
- A **consistent "Ask Claude to Edit"** entry point — selection-scoped with a
selection, whole-document without — from both the body and the tab.
- A document edit produces a **small number of in-context, block-sized**
decisions instead of a flood of word-fragments.
- **"Take Claude's whole pass"** is a **single gesture**.
- Attribution stays **word-precise** even though the decision unit is the block.
### 1.7 Scope (business)
**In scope:** the three features as one sequenced flow — entry-point wiring
(#42); document-edit proposal granularity word→block (#47); bulk-accept over the
current document (#46). Each remains its own implementation increment / PR.
**Out of scope / non-goals:**
- **Selection edits are unchanged** — a selection edit is already a single
proposal; #47 touches only the document path, #42 leaves selection behavior
intact, #46 reuses the same seam.
- **No Explorer context-menu entry** for Ask-Claude (#42 is tab + body only, per
the capture; the review-panel Explorer entry shipped separately as #41).
- **No "reject all"** (#46 is accept-only; a sibling ask if wanted).
- **No cross-document / workspace-wide accept-all** — current document only.
- **No new proposal-granularity *setting*** — block is the chosen unit, not a
toggle.
- **No change to the inline word-level `<ins>`/`<del>` rendering** inside a block
— the reviewer still sees exactly what changed; only the *decision unit* and the
*attribution reconciliation* change.
### 1.8 Assumptions · Constraints · Dependencies
- **F11 already shipped the document-edit path** (`editDocument`,
`askClaude`, `runEditAndPropose`, `diffToHunks`). This is the strongest
constraint: #42 is mostly **menu wiring** (the command exists); #47 **replaces
the diff inside `runEditAndPropose`'s document branch**; #46 **loops the existing
`acceptById`**. The cluster adds almost no new surface.
- **The block model already exists** — `splitBlocks` / `splitBlocksWithRanges` /
`diffBlocks` (`BlockOp`: unchanged / added / removed / changed+atomic; fences
atomic per INV-23). #47 **reuses this**, not a second notion of "block."
- **The F4 accept seam is the only mutation path** —
`ProposalController.accept``AttributionController.applyAgentEdit`
(`WorkspaceEdit`); the sealed webview posts **intent** only (INV-8/21/35).
- **No new persisted artifact** (INV-20) — proposals live in the F4 sidecar; this
cluster adds at most one transient field on the proposal (granularity marker —
§6.3) and transient webview messages.
- VS Code context menus render a command's own `title`; the tab menu picks one of
two commands by a `when` clause on selection state (no per-menu relabel of one
command).
### 1.9 Business Use Cases
- **BUC-1 (reach)** — A writer right-clicks the document (body, no selection) or
the tab and asks Claude to edit the whole document.
- **BUC-2 (review)** — The writer reads Claude's pass as block-sized proposals
and accepts/rejects each.
- **BUC-3 (accept)** — The writer, happy with the pass, accepts all pending
proposals in one gesture.
---
## 2. Solution Proposal
**One flow, three increments, all on existing seams.**
```mermaid
flowchart LR
subgraph reach["#42 — REACH"]
body["right-click body / tab\n(selection-aware)"] --> cmd["editSelection /\neditDocument"]
end
subgraph review["#47 — REVIEW"]
cmd --> rep["runEditAndPropose\n(document branch)"]
rep -- "diffToBlockHunks\n(per changed block)" --> props["F4 proposals\n(one per block)"]
end
subgraph accept["#46 — ACCEPT"]
props --> one["✓/✗ per block"]
props --> all["Accept all\n(toolbar intent)"]
end
one --> seam["F4 acceptById →\napplyAgentEdit"]
all --> seam
seam -- "block proposals: word-precise\nintra-block attribution (INV-40)" --> doc[(document)]
```
The defining idea of the cluster is in **#47's accept**: a document edit's
proposals are cut at **block** granularity (the unit a human reviews), but each
accept reconciles attribution at **word** granularity (the unit F3 records). The
block is the **decision** unit; the word is the **attribution** unit. This is what
lets us coarsen the review without coarsening the attribution — and it reuses the
*exact* word-level machinery (`diffToHunks`) that INV-37 used, now repurposed as
the intra-block sub-diff at accept time rather than the proposal cut.
---
## 3. Product Personas
- **PP-1 (the coauthoring writer)** — edits a markdown document with Claude;
wants to invoke a whole-document pass quickly, review it at paragraph altitude,
and take it wholesale or selectively.
---
## 4. Product Use Cases
| PUC | As PP-1 I… | Feature | Acceptance |
| --- | --- | --- | --- |
| PUC-1 | right-click the body with **no selection****Ask Claude to Edit Document** | #42 | whole-document edit → block proposals in the preview |
| PUC-2 | right-click the body **with a selection****Ask Claude to Edit Selection** (unchanged) | #42 | one proposal over the selection (existing behavior) |
| PUC-3 | right-click the **tab** → Edit Selection (if selection) / Edit Document (if not) | #42 | same behavior as the body equivalents, against that doc |
| PUC-4 | see Claude's document edit as **one proposal per changed block** | #47 | M changed blocks → M proposals; unchanged blocks → none; fences atomic |
| PUC-5 | accept a block proposal and have **only Claude's actual changes** attributed to Claude | #47 | unchanged words inside the accepted block keep their prior author (INV-40) |
| PUC-6 | press **Accept all** to take every pending proposal at once | #46 | all anchored proposals applied; orphans skipped + reported |
---
## 5. UX Layout
- **#42 entry points.** Two existing commands, surfaced in two more menus:
- **Editor body** (`editor/context`): **Ask Claude to Edit Selection** when
`editorHasSelection`, **Ask Claude to Edit Document** when not.
- **Editor tab** (`editor/title/context`): the same pair, gated the same way,
targeting the tab's document.
- Both entries appear **only for markdown / authorable** docs (mirroring
`editSelection`'s `when`: `resourceScheme == file || untitled` +
`editorLangId == markdown`).
- **#47 review.** The preview is unchanged in *look* — the reviewer still sees
word-level `<ins>`/`<del>` inside each block. What changes: each **changed
block** is **one `cw-proposal` block** carrying one `data-proposal-id` and one
✓/✗ pair, instead of several. A changed code/mermaid fence is one atomic
proposal (INV-23).
- **#46 accept-all.** An **Accept all** button in the F11 preview **toolbar**,
shown when the document has **≥ 2 pending proposals**. Pressing it posts the
`acceptAll` intent; on completion the host reports applied-vs-skipped (status
message / toast). No confirmation dialog — VS Code undo restores (consistent
with single accept).
```
┌─ Review: my-doc.md ──────────────────────────────┐
│ [ Pin baseline ] [ Ask Claude ▾ ] [ Accept all ]│ ← Accept all visible only when ≥2 pending
├───────────────────────────────────────────────────┤
│ ## Heading │
│ ┌───────────────────────────── proposal ───────┐ │
│ │ The ~~quick brown~~ swift fox … ✓ ✗ │ │ ← one block, word-level ins/del inside
│ └───────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
```
---
## 6. Technical Design
### 6.1 Invariants
Parent invariants **INV-1..INV-37 carry over except where #47 supersedes
INV-37**. This cluster adds:
- **INV-38 (consistent, selection-aware Ask-Claude entry points)** — "Ask Claude
to Edit" is reachable from the editor **body** and the editor **tab**,
**selection-scoped when there is a selection** (`cowriting.editSelection`),
**whole-document when there is not** (`cowriting.editDocument`). Both menus are
gated to **markdown / authorable** docs. Both scopes route through the **single**
`runEditAndPropose` path — no divergent edit code (#42).
- **INV-39 (document edits propose per changed block — supersedes INV-37)** — a
whole-document rewrite is cut into **one F4 proposal per changed block**, where a
block is the unit `splitBlocks` already recognizes (prose paragraph / header /
bullet; code & mermaid fences **atomic**, INV-23). **Unchanged blocks produce no
proposal.** A block proposal's anchor spans the **whole** source block and its
replacement is Claude's **whole** version of that block. This **replaces**
INV-37's per-hunk word-level granularity **for document edits** (selection edits
are unaffected) (#47).
- **INV-40 (block decision, word-precise attribution)** — accepting a block
proposal applies Claude's whole block but attributes **only the runs Claude
actually changed** to Claude; **unchanged spans within the block keep their
prior authorship**. Mechanism: an intra-block **word** sub-diff at accept time
(reusing `diffToHunks`) drives one `applyAgentEdit` per changed run. The **block
is the decision unit; the word is the attribution unit** (#47).
- **INV-41 (block-insertion anchoring)** — a newly **inserted** block (one Claude
added) is anchored to an adjacent block boundary so its proposal **resolves and
is acceptable** — the block-level analogue of `anchorInsertion`; **no
unresolvable zero-width block proposal** (#47).
- **INV-42 (accept-all = batched F4 seam; current doc; orphan-skip)** —
`cowriting.acceptAllProposals` applies **every pending proposal on the current
document** through the existing `acceptById` seam, in a **re-anchor-safe order**,
**skipping (never force-applying)** proposals that can't anchor, and **reports
applied-vs-skipped**. It is a **batched application of the existing accept path**,
not a new mechanism; the webview button posts **intent** (INV-35). Per-proposal
✓/✗ is unchanged (#46).
**Supersession (explicit):** **INV-39 reverses INV-37** (document rewrite → per
*hunk*/word) in favour of per *block*. INV-37's word-level engine (`diffToHunks`)
is **not removed** — it is **repurposed** as the intra-block sub-diff that
realizes INV-40's word-precise attribution. The F11 spec carrying INV-37 is
un-graduated (§9); this spec is the canonical home of the superseding decision.
### 6.2 High-level architecture
All three increments are localized to four already-existing files; no new module
is required (one new pure function + one new command + menu JSON).
```mermaid
flowchart TD
pkg["package.json\n(menus: editor/context + editor/title/context)"] -. "#42 wiring" .-> cmds
cmds["editSelection / editDocument\n(existing commands)"] --> rep["runEditAndPropose\n(trackChangesPreview.ts)"]
rep -- "selection branch (unchanged)" --> single["one single-range proposal"]
rep -- "document branch (#47)" --> dbh["diffToBlockHunks\n(trackChangesModel.ts — NEW, pure)"]
dbh --> blockprops["one proposal per changed block\n(granularity: 'block')"]
single & blockprops --> pc["ProposalController\n(proposalController.ts)"]
pc -- "accept (block) → intra-block sub-diff (#47, INV-40)" --> attr["AttributionController.applyAgentEdit\n(per changed run)"]
pc -- "acceptAll (#46, INV-42)" --> pc
tb["preview toolbar\nAccept all button"] -- "{type:'acceptAll'} intent (INV-35)" --> pc
```
- **`package.json`** (#42) — add `editSelection` + `editDocument` to
`editor/context` and `editor/title/context`, each with a selection-state `when`
clause; titles "Ask Claude to Edit Selection" / "Ask Claude to Edit Document".
- **`trackChangesModel.ts`** (#47, pure / vscode-free) — add
**`diffToBlockHunks(currentText, rewrittenText): EditHunk[]`**: split both sides
into the existing block units, diff blocks (reusing `diffBlocks`/`diffArrays`
keying), and emit **one `EditHunk` per changed-or-added block** spanning the
whole block's source range → its rewrite text; fences atomic; inserted blocks
anchored (INV-41). Returns the same `EditHunk[]` shape `diffToHunks` does, so the
proposal-minting loop is unchanged. **`diffToHunks` stays** as the intra-block
sub-diff helper for INV-40.
- **`trackChangesPreview.ts`** (#42/#47) — `runEditAndPropose`'s **document
branch** calls `diffToBlockHunks` instead of `diffToHunks`, and tags each minted
proposal `granularity: "block"`; `editDocument` resolves the **tab's** document
(URI arg) when invoked from `editor/title/context`, not only `activeTextEditor`.
Adds the `acceptAll` inbound-message case (#46) → `proposals.acceptAllProposals`.
- **`proposalController.ts`** (#47/#46):
- `accept` (#47): when `proposal.granularity === "block"`, instead of one
whole-block `applyAgentEdit`, compute the intra-block word sub-diff
(`diffToHunks(resolvedBlockText, proposal.replacement)`) and apply **one
`applyAgentEdit` per changed run** (Claude-attributed), unchanged runs left in
place — one undo-grouped edit. Non-block proposals keep the existing single
`applyAgentEdit`.
- `acceptAllProposals(document)` (#46): gather pending proposals, apply via
`acceptById` in **descending document order** (last anchor first, so earlier
anchors' offsets stay valid), skip orphans (suppressing per-item warnings),
and return `{ applied, skipped }` for the controller to report.
### 6.3 Data model & ownership
**No new persisted artifact** (INV-20). The only model change is **one optional
field** on the F4 `Proposal`:
```ts
// F4 sidecar Proposal — additive, optional, defaulted (back-compat):
interface Proposal {
// …existing fields…
granularity?: "block" | "single"; // "block" ⇒ accept reconciles attribution per word (INV-40)
}
```
- A document-edit proposal is minted with `granularity: "block"`; selection
proposals are `"single"` (or absent → treated as single). A missing field on an
older sidecar reads as `"single"` — existing proposals accept exactly as before.
- New transient webview message (host ⇄ webview), extending the F11 toolbar
intents:
```ts
// webview → host (intent only — INV-35)
type ToolbarMsg =
| /* …existing: setMode | accept | reject | pinBaseline | askClaude… */
| { type: "acceptAll" };
```
Baseline owned by F6; proposals by F4 (sidecar); attribution by F3. This cluster
adds no new ownership.
### 6.4 Interfaces & contracts
- **`trackChangesModel`** (vscode-free): `diffToBlockHunks(currentText: string,
rewrittenText: string): EditHunk[]` — deterministic; one hunk per changed/added
block over the existing block units; fences atomic; inserted blocks anchored
(INV-41). Same `EditHunk` shape as `diffToHunks`. `diffToHunks` is **retained**
(intra-block sub-diff for INV-40).
- **`ProposalController`**: `acceptAllProposals(document): Promise<{ applied:
number; skipped: number }>` — batched accept over the document's pending set via
the existing `acceptById` seam, re-anchor-safe order, orphan-skip (INV-42). The
private `accept` gains the block-aware attribution branch (INV-40) keyed off
`proposal.granularity`.
- **Commands**: `cowriting.editDocument` (existing — extended to resolve a tab
URI arg); new `cowriting.acceptAllProposals` (active doc; reachable outside the
webview).
### 6.5 PerProduct-Use-Case design
- **PUC-1/2/3 (#42)** — pure `package.json` menu wiring + the `editDocument` tab
URI resolution. `runEditAndPropose` is reached unchanged; the only code touch is
resolving the right document for the tab gesture (mirroring #41's clicked-doc
resolution).
- **PUC-4 (#47 cut)** — `diffToBlockHunks` replaces `diffToHunks` in the document
branch. For each `BlockOp` that is `changed` or `added`, emit one `EditHunk`:
`changed` → `[block.start, block.end) → after-block text`; `added` → a zero-width
range anchored to the adjacent block boundary (INV-41) with the new block text +
a separating newline as needed; `removed` → a hunk that deletes the block;
`unchanged` → nothing. Fences (`atomic`) emit one whole-fence hunk (never
word-refined).
- **PUC-5 (#47 attribution, INV-40)** — at accept of a `block` proposal: re-resolve
the anchor → the current block text; `diffToHunks(currentBlockText,
replacement)` → the word-level changed runs *within* the block; apply each run's
replacement via `applyAgentEdit` (Claude-attributed) in one undo-grouped
`WorkspaceEdit`, leaving unchanged runs untouched so their prior attribution
stands. Net buffer text equals the whole-block replacement; net attribution is
word-precise.
- **PUC-6 (#46)** — `acceptAllProposals`: snapshot the pending list, sort
descending by resolved anchor start, `acceptById` each (block proposals take the
INV-40 path automatically), tally applied vs. skipped-orphan, report once.
### 6.6 Non-functional requirements & cross-cutting concerns
- **Determinism / testability** — `diffToBlockHunks` is pure and vscode-free
(unit-tested like `diffToHunks`/`diffBlocks`). The INV-40 attribution
reconciliation is exercised through the controller with a fake document.
- **Security** — unchanged: the webview stays sealed; **Accept all** posts intent;
the host validates and performs every mutation (INV-8/21/35). No LLM/secret
surface enters the sandbox.
- **Performance** — block diffing is cheaper than the prior word fan-out (fewer
proposals minted/rendered). Accept-all is O(pending) seam calls; large documents
bounded by proposal count, not document size.
- **Reversibility** — accept-all is one undo-restorable step-set; block accept is
one undo-grouped edit. No destructive irreversibility.
### 6.7 Key decisions & alternatives considered
| Decision | Chosen | Alternative (rejected) |
| --- | --- | --- |
| Cluster artifact | **One combined Solution Design**, three increments sequenced #42→#47→#46 (§3.3 — one design absorbs the roadmap) | Three separate specs — more duplication of the shared F11 edit path + F4 seam |
| #47 block model | **Reuse `splitBlocks`/`diffBlocks`** | Invent a second "block" notion — divergence risk (capture warned against it) |
| #47 attribution of unchanged words in an accepted block | **Preserve unchanged spans** — block decision, word-precise attribution (INV-40), via intra-block `diffToHunks` sub-diff at accept | **Whole block → Claude** — simpler (one seam call) but loses F3 fidelity inside the block; operator chose fidelity |
| #47 vs INV-37 | **Supersede** INV-37 for document edits; **repurpose** `diffToHunks` as the intra-block engine | Keep word-level proposals + a separate "group view" — two models, more surface |
| #46 apply-order | **Descending document order** through `acceptById` (each accept re-resolves the fingerprint, INV-11) | Re-resolve-then-rescan after every accept — more work for the same result |
| #46 confirmation | **None** (undo restores) | A confirm dialog — friction the single-accept path doesn't have |
| #46 scope | **Current document only** | Workspace-wide — capture non-goal |
| #42 surfaces | **Body + tab only**, selection-aware | Add Explorer entry — capture non-goal (#41 covers Explorer for the review panel) |
### 6.8 Testing strategy
- **Unit (vitest, pure)** — `diffToBlockHunks`: N words across M blocks → **M**
hunks (one per changed block, spanning the block); unchanged block → none;
changed fence → **one atomic** hunk; inserted block → an anchored, resolvable
hunk (INV-41); each hunk's `[start,end)` lands on real block boundaries in
`currentText`.
- **Unit (controller)** — INV-40: accepting a block proposal whose replacement
changes 2 of 6 words attributes **only those 2 runs** to Claude (the other 4
keep prior author); orphaned block proposal is flagged, not force-applied.
- **Host E2E (Playwright)** —
- #42: body no-selection shows **Edit Document**; body selection shows **Edit
Selection**; tab shows the right one per selection state; each produces
proposal(s).
- #47: a document edit changing words across M blocks yields **M proposals**,
each spanning its block; unchanged block → none; changed fence → one atomic
proposal; an inserted block accepts cleanly.
- #46: with N pending proposals, **Accept all** applies all N (text replaced,
Claude-attributed, proposals cleared); an orphaned proposal in the set is
skipped and **reported**, not mangled; the button is hidden with < 2 pending.
- **Manual smoke** — extend `docs/MANUAL-SMOKE-F11.md` (or a new
`MANUAL-SMOKE-F12.md`) covering the three gestures end-to-end.
### 6.9 Failure modes, rollback & flags
- **Orphaned proposal** (target text changed / missing) — single accept already
warns + flags (never applied by guess); accept-all **skips** it and includes it
in the applied-vs-skipped report (INV-42). Reject-or-undo remains the recovery.
- **Inserted block fails to anchor** — INV-41's adjacent-boundary anchoring makes
this resolvable; if the boundary block itself later changes, the proposal orphans
and is handled as above (never silently dropped).
- **Back-compat** — the additive `granularity` field defaults to `"single"`;
pre-existing sidecars/proposals accept unchanged. No migration.
- **No feature flags** — each increment ships green through the pipeline; this is
a VS Code extension (no flotilla deploy stage).
---
## 7. Delivery Plan
### 7.1 Approach / strategy
Ship the flow **reach → review → accept** as three independent
planning-and-executing increments, each its own PR, in sequence. They are
**code-independent** (different files / functions), so the order is chosen for
**user-visible value compounding**, not technical dependency — each increment is
usable on its own.
> **WSJF note (transparent):** **#47 carries the P1 value** ("too much to review"
> is the sharpest pain); #42 and #46 are P2. The operator set the goal "starting
> with #42," and #42 is the smallest, lowest-risk increment (the command already
> exists — it's menu wiring), so leading with it completes the entry-point story
> first. Because the three are code-independent, **#47 and #42 could swap** with no
> rework if the P1 review pain should land first — a one-line resequencing.
### 7.2 Slicing plan
- **SLICE-1 — #42 (reach).** `package.json` menus (`editor/context` +
`editor/title/context`, selection-aware, markdown-gated); `editDocument`
resolves the tab's document; titles. **+ E2E** (body/tab × selection state →
right item → proposal). Smallest; no model change. (INV-38.)
- **SLICE-2 — #47 (review).** `diffToBlockHunks` (pure, + unit tests); document
branch of `runEditAndPropose` switches to it and tags `granularity:"block"`;
block-aware accept in `ProposalController` (intra-block sub-diff, INV-40);
block-insertion anchoring (INV-41). **Supersede INV-37 → INV-39** in the spec/
invariant set. **+ unit + E2E.** The substantive increment. (INV-39/40/41.)
- **SLICE-3 — #46 (accept).** `acceptAllProposals` (controller, re-anchor-safe
order, orphan-skip + report); `cowriting.acceptAllProposals` command; preview
toolbar **Accept all** button + `{type:"acceptAll"}` intent (shown ≥2 pending).
**+ E2E.** (INV-42.)
### 7.3 Rollout / launch plan
No deploy pipeline (VS Code extension — no flotilla / PPE / prod stages; the §9
pipeline's deploy stages don't apply). Each slice merges to `main` green
(unit + host E2E). "Done" = merged with tests green; there is no prod-promotion
gate for an extension increment.
### 7.4 Risks & mitigations
| Risk | Mitigation |
| --- | --- |
| INV-40 intra-block multi-edit accept skews offsets mid-apply | Apply changed runs **last-position-first** within the block, one undo-grouped `WorkspaceEdit`; unit-test the 2-of-6-words case |
| Block taxonomy edge cases (lists w/o blank lines, tables, blockquotes, nested) | **Reuse `splitBlocks` exactly** — whatever it already treats as one block *is* one proposal; document the taxonomy = the splitter's, no second notion |
| Inserted-block anchor collides / can't resolve | INV-41 anchors to an adjacent **unchanged** block boundary; orphan-handling is the backstop (never force-applied) |
| Accept-all on a large pending set partially applies then an orphan appears | Each `acceptById` is independent + re-resolves (INV-11); skipped orphans reported; undo restores the whole step-set |
| F11 spec un-graduated → INV-35/36/37 only in code | This spec restates the load-bearing F11 invariants it depends on; §9 logs the graduation gap as a follow-up |
---
## 8. Traceability matrix
| Requirement | PUC | Invariant / §6 | Slice | Feature |
| --- | --- | --- | --- | --- |
| Ask-Claude reachable body (no-sel) + tab, selection-aware | PUC-1/2/3 | INV-38, §6.5 | SLICE-1 | #42 |
| Selection edit unchanged | PUC-2 | INV-38 (single path) | SLICE-1 | #42 |
| Document edit → one proposal per changed block | PUC-4 | INV-39, §6.5 (`diffToBlockHunks`) | SLICE-2 | #47 |
| Unchanged block → no proposal; fence atomic | PUC-4 | INV-39, INV-23 | SLICE-2 | #47 |
| Accepted block attributes only changed words to Claude | PUC-5 | INV-40, §6.5 | SLICE-2 | #47 |
| Inserted block anchors + accepts | PUC-4 | INV-41 | SLICE-2 | #47 |
| Accept-all over current doc, orphan-skip + report | PUC-6 | INV-42, §6.5 | SLICE-3 | #46 |
| All mutation via the F4 seam; webview intent-only | PUC-4/5/6 | INV-35, §6.6 | SLICE-2/3 | #47/#46 |
---
## 9. Open Questions & Decisions log
**Decided this session (operator):**
- **D-1** — #47 accepts **preserve unchanged-span attribution** (block = decision
unit, word = attribution unit; INV-40). *Not* whole-block→Claude.
- **D-2** — the cluster ships as **one** combined Solution Design (this doc),
three sequenced increments.
- **D-3 (driver call, low-confidence — for finalize review)** — sequence **#42 →
#47 → #46** per the goal's "starting with #42," despite #47 carrying the P1
value. The three are code-independent so the order is reversible (§7.1).
- **D-4 (driver call)** — block taxonomy = **exactly `splitBlocks`'s** units (no
second notion of "block"); list/table/blockquote/nested behavior is whatever the
splitter already does.
- **D-5 (driver call)** — accept-all needs **no confirmation** (undo restores);
button shown at **≥ 2** pending.
**Open / deferred:**
- **OQ-1** — Should **selection** edits *also* preserve-unchanged-span
attribution for consistency with INV-40? Out of #47's captured scope (selection
edits explicitly unchanged); a future consistency pass could unify them. **Not
in this cluster.**
- **OQ-2 (spec-hygiene, not blocking)** — the **F11 (#43) Solution Design is
un-graduated** — INV-35/36/37 live only in code comments + the issue draft
`issues/preview-toolbar-interaction-surface.md`, not in `specs/`. This spec
restates the F11 invariants it depends on and supersedes INV-37 here, but the
F11 design should be graduated to `specs/` for a clean invariant ledger.
Captured for finalize as a follow-up.
- **OQ-3** — Very large blocks: no special fallback is specified (block proposals
are bounded by the block, and accept reconciles per word). Revisit only if a
pathological single-giant-block document surfaces.
---
## 10. Glossary & References
- **Block** — the unit `splitBlocks` recognizes: a prose paragraph, header, or
bullet/list item; a code or mermaid fence (atomic, INV-23). The #47 decision
unit.
- **Block proposal** — an F4 proposal whose anchor spans a whole block and whose
replacement is Claude's whole version of that block (`granularity: "block"`).
- **Intra-block sub-diff** — the word-level `diffToHunks` run *inside* an accepted
block to realize word-precise attribution (INV-40).
- **`diffToHunks` / `diffToBlockHunks`** — word-level (legacy INV-37; now the
intra-block engine) / block-level (INV-39, new) document-rewrite differs, both
emitting `EditHunk[]`.
- **References** — `coauthoring-propose-accept.md` (F4 seam, INV-9..13),
`coauthoring-interactive-review.md` (F10 preview, INV-32..34),
`coauthoring-attribution.md` (F3, `applyAgentEdit`/`spansFor`),
`issues/preview-toolbar-interaction-surface.md` (F11, INV-35..37 — un-graduated),
features `#42` / `#47` / `#46`, capture sessions `0035` / `0040` / `0039`,
brainstorming session `0041`. Lineage `ben.stull/rfc-app#48`.