34 KiB
status
| 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 F2–F5 (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 insiderunEditAndPropose's document branch; #46 loops the existingacceptById. 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 awhenclause 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.
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 wheneditorHasSelection, 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'swhen:resourceScheme == file || untitled+editorLangId == markdown).
- Editor body (
- #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 onecw-proposalblock carrying onedata-proposal-idand 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
acceptAllintent; 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 singlerunEditAndProposepath — 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
splitBlocksalready 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 oneapplyAgentEditper 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.acceptAllProposalsapplies every pending proposal on the current document through the existingacceptByIdseam, 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).
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) — addeditSelection+editDocumenttoeditor/contextandeditor/title/context, each with a selection-statewhenclause; titles "Ask Claude to Edit Selection" / "Ask Claude to Edit Document".trackChangesModel.ts(#47, pure / vscode-free) — adddiffToBlockHunks(currentText, rewrittenText): EditHunk[]: split both sides into the existing block units, diff blocks (reusingdiffBlocks/diffArrayskeying), and emit oneEditHunkper changed-or-added block spanning the whole block's source range → its rewrite text; fences atomic; inserted blocks anchored (INV-41). Returns the sameEditHunk[]shapediffToHunksdoes, so the proposal-minting loop is unchanged.diffToHunksstays as the intra-block sub-diff helper for INV-40.trackChangesPreview.ts(#42/#47) —runEditAndPropose's document branch callsdiffToBlockHunksinstead ofdiffToHunks, and tags each minted proposalgranularity: "block";editDocumentresolves the tab's document (URI arg) when invoked fromeditor/title/context, not onlyactiveTextEditor. Adds theacceptAllinbound-message case (#46) →proposals.acceptAllProposals.proposalController.ts(#47/#46):accept(#47): whenproposal.granularity === "block", instead of one whole-blockapplyAgentEdit, compute the intra-block word sub-diff (diffToHunks(resolvedBlockText, proposal.replacement)) and apply oneapplyAgentEditper changed run (Claude-attributed), unchanged runs left in place — one undo-grouped edit. Non-block proposals keep the existing singleapplyAgentEdit.acceptAllProposals(document)(#46): gather pending proposals, apply viaacceptByIdin 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:
// 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:
// 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). SameEditHunkshape asdiffToHunks.diffToHunksis 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 existingacceptByIdseam, re-anchor-safe order, orphan-skip (INV-42). The privateacceptgains the block-aware attribution branch (INV-40) keyed offproposal.granularity.- Commands:
cowriting.editDocument(existing — extended to resolve a tab URI arg); newcowriting.acceptAllProposals(active doc; reachable outside the webview).
6.5 Per–Product-Use-Case design
- PUC-1/2/3 (#42) — pure
package.jsonmenu wiring + theeditDocumenttab URI resolution.runEditAndProposeis 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) —
diffToBlockHunksreplacesdiffToHunksin the document branch. For eachBlockOpthat ischangedoradded, emit oneEditHunk: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
blockproposal: re-resolve the anchor → the current block text;diffToHunks(currentBlockText, replacement)→ the word-level changed runs within the block; apply each run's replacement viaapplyAgentEdit(Claude-attributed) in one undo-groupedWorkspaceEdit, 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,acceptByIdeach (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 —
diffToBlockHunksis pure and vscode-free (unit-tested likediffToHunks/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 incurrentText. - 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 newMANUAL-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
granularityfield 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.jsonmenus (editor/context+editor/title/context, selection-aware, markdown-gated);editDocumentresolves 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 ofrunEditAndProposeswitches to it and tagsgranularity:"block"; block-aware accept inProposalController(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.acceptAllProposalscommand; 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 inspecs/. This spec restates the F11 invariants it depends on and supersedes INV-37 here, but the F11 design should be graduated tospecs/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
splitBlocksrecognizes: 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
diffToHunksrun 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 emittingEditHunk[].- 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 sessions0035/0040/0039, brainstorming session0041. Lineageben.stull/rfc-app#48.