Block-granularity proposals: one change per paragraph/header/bullet, not per word #47

Closed
opened 2026-06-13 00:21:09 +00:00 by benstull · 1 comment
Owner

Summary

When Claude edits a document, each suggested change is one proposal per changed
block
— a whole paragraph, header, or bullet — rather than one proposal per
changed word. A reviewer sees a handful of block-sized decisions, not a
scatter of word-level ones.

Problem / pain

F11's Ask Claude to Edit Document diffs a whole-document rewrite into
proposals at word granularity: diffToHunks (src/trackChangesModel.ts) runs
diffWordsWithSpace and flushes a hunk on every unchanged token, so each changed
word (or short run) becomes its own independent ✓/✗ proposal (INV-37, "per-hunk").

A single paragraph where Claude changed three words scattered across it surfaces
as three separate proposals; a light copy-edit pass over a document explodes
into dozens of tiny blue blocks. It's too much to review — the reviewer is
forced to adjudicate word-fragments out of the context of the sentence they live
in, instead of deciding "do I want Claude's version of this paragraph?"

Who feels it

Writers reviewing Claude's document-level edits — anyone using Ask Claude to
Edit Document
, where one rewrite currently fans out into many word-level
proposals. The finer the edit, the worse the review tax.

Desired outcome / value

A reviewer adjudicates Claude's changes at the unit they actually think in
the paragraph / header / bullet — so a document edit produces a small number of
meaningful, in-context decisions instead of a flood of word-fragment ones. Makes
the document-edit flow genuinely reviewable, and pairs with bulk-accept (#46) to
make "take Claude's pass" tractable.

Acceptance — "done right"

  • A whole-document Claude edit produces one proposal per changed block, where a
    block is a paragraph, header, or bullet / list item (the unit the existing block
    splitter already recognizes).
  • A changed block's proposal spans the entire block and its replacement is
    Claude's whole version of that block — accepting it replaces the block, not just
    the changed words inside it.
  • Unchanged blocks produce no proposal (only blocks Claude actually touched
    surface).
  • Code / mermaid fences stay atomic — a changed fenced block is one proposal
    for the whole fence, never word-refined or split (consistent with INV-23 in the
    preview renderer).
  • A newly inserted block (a paragraph/bullet Claude added) surfaces as its own
    proposal, anchored so it can be accepted (no un-resolvable zero-width proposal).
  • Accept / reject of a block proposal routes through the existing F4 seam
    unchanged (the accepted block lands Claude-attributed, baseline advances,
    proposal clears); this changes only how the rewrite is cut into proposals,
    not the accept mechanism.
  • The preview's inline word-level <ins>/<del> rendering within a block is
    unaffected
    — the reviewer still sees exactly what changed inside the block;
    only the proposal/decision unit becomes the block.
  • E2E browser coverage asserts: a rewrite changing N words across M blocks yields
    M proposals (one per changed block), each spanning its block; an unchanged
    block yields none; a changed fenced block stays one atomic proposal.

Scope / non-goals

  • In scope: changing document-edit proposal granularity from word-level
    hunks to block-level (the diffToHunks fan-out path, F11/#42); reusing the
    existing block model; preserving the F4 accept seam and the intra-block
    <ins>/<del> rendering.
  • Non-goals: selection edits are unchanged — a selection edit is already a
    single proposal (no fan-out to coarsen); no change to the inline diff
    rendering granularity (words still shown inside the block); no new accept
    mechanism; no "accept all" (that's #46); no configurable/toggleable granularity
    (block is the chosen unit, not a setting) unless the design surfaces a need.

Assumptions · constraints · dependencies

  • Supersedes the F11 INV-37 "per-hunk" (word-level) proposal-granularity
    decision with per-block for document edits — the spec/invariant is updated as
    part of this work.
  • The plugin already splits markdown into blocks for the preview (diffBlocks,
    paragraph/header/bullet, code & mermaid fences atomic) — block-level hunking
    should reuse that block model rather than invent a second notion of "block".
  • Attribution tradeoff (a real design fork): a block proposal replaces the
    whole block, so accepting it attributes the entire block to Claude via the
    seam — including words that didn't change. The design step decides whether that
    is acceptable (simplest, matches "Claude's version of this block") or whether
    unchanged spans within an accepted block should retain prior attribution.
  • Open questions for the design step: precise block taxonomy (list items vs.
    whole lists; tables; blockquotes; nested blocks); how adjacent
    changed+inserted
    blocks group; anchoring of inserted/blank-bordered blocks
    (the current anchorInsertion trick is word-oriented — block insertions need an
    equivalent); whether very large blocks warrant any fallback.
  • Related lineage: F4 (#12) propose/accept seam, F7/F10 block-diff rendering, F11
    (#43, INV-37) document edit → per-hunk proposals (the behavior this changes),
    #42 (Ask Claude to Edit Document — the N-proposal source), #46 (accept-all —
    complementary: fewer proposals and one-gesture accept).

Source / signal

Self-originated — capture session vscode-cowriting-plugin-0040 (operator ask:
"When Claude suggests changes, a single change should be for a paragraph (or
header, or bulletpoint), not individual words. It's too much to review").

Priority (WSJF)

Value (high — makes Claude's document edits actually reviewable; directly removes a
"too much to review" pain) · Time-criticality (low–med) · Opportunity-enablement
(med — unlocks the document-edit flow alongside #42/#46) ÷ Size (med — reuses the
block model, but block hunking + insertion anchoring + the attribution decision
are real work) → favorable score. priority/P1.

Decomposition

  • Block-level document diff: a diffToBlockHunks (or reworked diffToHunks) that emits one hunk spanning each changed block, reusing the existing block splitter; code/mermaid fences atomic.
  • Anchor inserted / blank-bordered blocks so each block proposal resolves and is acceptable (block-level equivalent of anchorInsertion).
  • Wire the document-edit path (runEditAndPropose document scope) to mint per-block proposals via the F4 seam; selection path untouched.
  • Settle + apply the attribution-of-unchanged-words-in-an-accepted-block decision.
  • Update spec/invariant: INV-37 per-hunk → per-block for document edits.
  • E2E: N words across M blocks → M proposals; unchanged block → none; changed fence → one atomic proposal; inserted block accepts cleanly.

Solution notes (optional — NOT a design)

The likely shape is a block-aware replacement for diffToHunks: split both
baseline-vs-rewrite into the existing block units, and for each block that differs
emit a single hunk covering the whole block (its source range → Claude's block
text), keeping fenced blocks atomic. Intra-block <ins>/<del> rendering stays as
it is — only the proposal cut changes. Inserted blocks need a block-level anchoring
analogue to today's word-oriented anchorInsertion. The accept seam and the
preview are otherwise untouched. Non-binding — the design step settles the block
taxonomy, insertion anchoring, and the attribution semantics.

<!-- Set the type/* label via capture-issues.sh --type feature --> ## Summary When Claude edits a document, each suggested change is **one proposal per changed block** — a whole paragraph, header, or bullet — rather than one proposal per changed **word**. A reviewer sees a handful of block-sized decisions, not a scatter of word-level ones. ## Problem / pain F11's **Ask Claude to Edit Document** diffs a whole-document rewrite into proposals at **word granularity**: `diffToHunks` (src/trackChangesModel.ts) runs `diffWordsWithSpace` and flushes a hunk on every unchanged token, so each changed word (or short run) becomes its own independent ✓/✗ proposal (INV-37, "per-hunk"). A single paragraph where Claude changed three words scattered across it surfaces as **three separate proposals**; a light copy-edit pass over a document explodes into dozens of tiny blue blocks. **It's too much to review** — the reviewer is forced to adjudicate word-fragments out of the context of the sentence they live in, instead of deciding "do I want Claude's version of this paragraph?" ## Who feels it Writers reviewing Claude's document-level edits — anyone using **Ask Claude to Edit Document**, where one rewrite currently fans out into many word-level proposals. The finer the edit, the worse the review tax. ## Desired outcome / value A reviewer adjudicates Claude's changes at the **unit they actually think in** — the paragraph / header / bullet — so a document edit produces a small number of meaningful, in-context decisions instead of a flood of word-fragment ones. Makes the document-edit flow genuinely reviewable, and pairs with bulk-accept (#46) to make "take Claude's pass" tractable. ## Acceptance — "done right" - A whole-document Claude edit produces **one proposal per changed block**, where a block is a paragraph, header, or bullet / list item (the unit the existing block splitter already recognizes). - A changed block's proposal spans the **entire block** and its replacement is Claude's whole version of that block — accepting it replaces the block, not just the changed words inside it. - **Unchanged blocks produce no proposal** (only blocks Claude actually touched surface). - **Code / mermaid fences stay atomic** — a changed fenced block is one proposal for the whole fence, never word-refined or split (consistent with INV-23 in the preview renderer). - A **newly inserted block** (a paragraph/bullet Claude added) surfaces as its own proposal, anchored so it can be accepted (no un-resolvable zero-width proposal). - Accept / reject of a block proposal routes through the **existing F4 seam** unchanged (the accepted block lands Claude-attributed, baseline advances, proposal clears); this changes only **how the rewrite is cut into proposals**, not the accept mechanism. - The preview's inline word-level `<ins>`/`<del>` **rendering within a block is unaffected** — the reviewer still sees exactly what changed inside the block; only the **proposal/decision unit** becomes the block. - E2E browser coverage asserts: a rewrite changing N words across M blocks yields **M proposals (one per changed block)**, each spanning its block; an unchanged block yields none; a changed fenced block stays one atomic proposal. ## Scope / non-goals - **In scope:** changing **document-edit** proposal granularity from word-level hunks to block-level (the `diffToHunks` fan-out path, F11/#42); reusing the existing block model; preserving the F4 accept seam and the intra-block `<ins>`/`<del>` rendering. - **Non-goals:** **selection edits** are unchanged — a selection edit is already a single proposal (no fan-out to coarsen); no change to the inline diff *rendering* granularity (words still shown inside the block); no new accept mechanism; no "accept all" (that's #46); no configurable/toggleable granularity (block is the chosen unit, not a setting) unless the design surfaces a need. ## Assumptions · constraints · dependencies - Supersedes the F11 **INV-37 "per-hunk" (word-level)** proposal-granularity decision with **per-block** for document edits — the spec/invariant is updated as part of this work. - The plugin already splits markdown into blocks for the preview (`diffBlocks`, paragraph/header/bullet, code & mermaid fences atomic) — block-level hunking should **reuse that block model** rather than invent a second notion of "block". - **Attribution tradeoff (a real design fork):** a block proposal replaces the whole block, so accepting it attributes the **entire block** to Claude via the seam — including words that didn't change. The design step decides whether that is acceptable (simplest, matches "Claude's version of this block") or whether unchanged spans within an accepted block should retain prior attribution. - Open questions for the design step: precise **block taxonomy** (list items vs. whole lists; tables; blockquotes; nested blocks); how **adjacent changed+inserted** blocks group; **anchoring** of inserted/blank-bordered blocks (the current `anchorInsertion` trick is word-oriented — block insertions need an equivalent); whether very large blocks warrant any fallback. - Related lineage: F4 (#12) propose/accept seam, F7/F10 block-diff rendering, F11 (#43, INV-37) document edit → per-hunk proposals (the behavior this changes), #42 (Ask Claude to Edit Document — the N-proposal source), #46 (accept-all — complementary: fewer proposals *and* one-gesture accept). ## Source / signal Self-originated — capture session **vscode-cowriting-plugin-0040** (operator ask: "When Claude suggests changes, a single change should be for a paragraph (or header, or bulletpoint), not individual words. It's too much to review"). ## Priority (WSJF) Value (high — makes Claude's document edits actually reviewable; directly removes a "too much to review" pain) · Time-criticality (low–med) · Opportunity-enablement (med — unlocks the document-edit flow alongside #42/#46) ÷ Size (med — reuses the block model, but block hunking + insertion anchoring + the attribution decision are real work) → favorable score. `priority/P1`. ## Decomposition - [ ] Block-level document diff: a `diffToBlockHunks` (or reworked `diffToHunks`) that emits one hunk spanning each changed block, reusing the existing block splitter; code/mermaid fences atomic. - [ ] Anchor inserted / blank-bordered blocks so each block proposal resolves and is acceptable (block-level equivalent of `anchorInsertion`). - [ ] Wire the document-edit path (`runEditAndPropose` document scope) to mint per-block proposals via the F4 seam; selection path untouched. - [ ] Settle + apply the attribution-of-unchanged-words-in-an-accepted-block decision. - [ ] Update spec/invariant: INV-37 per-hunk → per-block for document edits. - [ ] E2E: N words across M blocks → M proposals; unchanged block → none; changed fence → one atomic proposal; inserted block accepts cleanly. ## Solution notes (optional — NOT a design) The likely shape is a block-aware replacement for `diffToHunks`: split both baseline-vs-rewrite into the existing block units, and for each block that differs emit a single hunk covering the whole block (its source range → Claude's block text), keeping fenced blocks atomic. Intra-block `<ins>`/`<del>` rendering stays as it is — only the proposal cut changes. Inserted blocks need a block-level anchoring analogue to today's word-oriented `anchorInsertion`. The accept seam and the preview are otherwise untouched. Non-binding — the design step settles the block taxonomy, insertion anchoring, and the attribution semantics.
benstull added the type/featurepriority/P1 labels 2026-06-13 00:21:09 +00:00
Author
Owner

Shipped as SLICE-2 of the document-edit flow (#50, session 0044). Document edits now propose one F4 proposal per changed block (INV-39 supersedes INV-37); accepting a block attributes only the words Claude changed, unchanged spans keep prior authorship (INV-40); inserted blocks anchor + accept (INV-41); fences atomic (INV-23). 214 unit + 69/5 host E2E green.

Shipped as SLICE-2 of the document-edit flow (#50, session 0044). Document edits now propose **one F4 proposal per changed block** (INV-39 supersedes INV-37); accepting a block attributes only the words Claude changed, unchanged spans keep prior authorship (INV-40); inserted blocks anchor + accept (INV-41); fences atomic (INV-23). 214 unit + 69/5 host E2E green.
Sign in to join this conversation.