add spec ./specs/coauthoring-diff-view.md (status: graduated)
This commit is contained in:
@@ -0,0 +1,483 @@
|
|||||||
|
---
|
||||||
|
status: graduated
|
||||||
|
---
|
||||||
|
# Solution Design: Diff-View Toggle (F6)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **Author(s)** | Ben Stull (with Claude) |
|
||||||
|
| **Reviewers / approvers** | Ben Stull |
|
||||||
|
| **Status** | `draft` |
|
||||||
|
| **Version** | v0.1.0 |
|
||||||
|
| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#17` (F6) · Epic `#1` (closed — F6 is the first post-epic UX feature) · Parent specs: `vscode-cowriting-plugin-content/specs/coauthoring-inner-loop.md`, `specs/coauthoring-attribution.md`, `specs/coauthoring-propose-accept.md`, `specs/coauthoring-cross-rung-format.md` + `specs/coauthoring-sidecar-contract.md` (all graduated) · F5 shipped: `#14` (PR #15, session 0015) · Lineage: `ben.stull/rfc-app#48` |
|
||||||
|
|
||||||
|
**Change log**
|
||||||
|
|
||||||
|
| Date | Version | Change | By |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 2026-06-11 | v0.1.0 | Initial draft — brainstorming session 0017 | Ben Stull + Claude |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Business Context
|
||||||
|
|
||||||
|
### 1.1 Executive Summary
|
||||||
|
|
||||||
|
F6 gives the human coauthor the review surface the machine already has. F4
|
||||||
|
made every machine change reviewable-before-landing (proposal diffs); F3
|
||||||
|
paints who owns the current text; but the operator's **own** contribution is
|
||||||
|
visible only as final text. F6 adds a one-gesture **diff-view toggle**: flip
|
||||||
|
the editor into a diff of the document against a **coauthoring baseline** —
|
||||||
|
and back — so "what did I actually change?" is answered in one keystroke
|
||||||
|
instead of git archaeology. The load-bearing design move is the baseline
|
||||||
|
itself: it **advances automatically at every machine landing** (every
|
||||||
|
successful `applyAgentEdit` seam application — the only way machine text ever
|
||||||
|
enters the document, INV-9/INV-10), so everything the diff shows is
|
||||||
|
operator-authored *by construction*. A single **pin** gesture gives the
|
||||||
|
operator an arbitrary epoch when they want one. The baseline lives in
|
||||||
|
extension workspace storage — never in the repo — so the sidecar, the
|
||||||
|
cross-rung contract (INV-14..17), and `SCHEMA_VERSION` are untouched.
|
||||||
|
|
||||||
|
### 1.2 Background
|
||||||
|
|
||||||
|
The inner-loop triad shipped across F2–F4 (threads · attribution ·
|
||||||
|
propose/accept) and F5 made its sidecar a cross-rung contract. Review parity
|
||||||
|
is now asymmetric in the human's favor toward the *machine's* changes:
|
||||||
|
machine edits arrive as anchored diffs (F4) and land tinted (F3), while the
|
||||||
|
operator's own work between machine landings has no diff perspective at all.
|
||||||
|
Git's diff is against HEAD — the wrong baseline mid-session, where the
|
||||||
|
meaningful "before" is a coauthoring moment (a machine landing, a chosen
|
||||||
|
instant), not the last commit. Issue #17 parked the design forks —
|
||||||
|
**baseline choice** (the load-bearing one), snapshot home, whose-changes,
|
||||||
|
and the relationship to git's own features — for this session; all are
|
||||||
|
resolved here (§6.7, §9).
|
||||||
|
|
||||||
|
### 1.3 Business Actors / Roles
|
||||||
|
|
||||||
|
- **Coauthor (human)** — the writer/engineer (PP-1); F6's sole user: the
|
||||||
|
diff view exists to make their own contribution self-reviewable.
|
||||||
|
- **Coauthor (machine)** — Claude via `@cline/sdk`; not a user of F6, but
|
||||||
|
its landings define the default baseline epoch.
|
||||||
|
|
||||||
|
### 1.4 Problem Statement
|
||||||
|
|
||||||
|
While coauthoring, the operator cannot see their own changes as a diff. The
|
||||||
|
editor shows ownership of current text (F3) and machine intent before it
|
||||||
|
lands (F4), but the human's contribution since the last meaningful
|
||||||
|
coauthoring moment is only visible as final prose. The only diff available
|
||||||
|
— git's — is anchored to the wrong moment (HEAD).
|
||||||
|
|
||||||
|
### 1.5 Pain Points
|
||||||
|
|
||||||
|
- "What did I actually change?" requires git archaeology or memory.
|
||||||
|
- Self-review during coauthoring is expensive exactly where machine-review
|
||||||
|
is cheap — an inverted trust asymmetry.
|
||||||
|
- No way to mark "I'm about to do a risky pass — let me review it as a diff
|
||||||
|
afterwards" (no operator-chosen baseline moment).
|
||||||
|
|
||||||
|
### 1.6 Targeted Business Outcomes
|
||||||
|
|
||||||
|
One gesture flips the active document into a diff of the operator's own
|
||||||
|
changes against a coauthoring-meaningful baseline; the same gesture flips
|
||||||
|
back; nothing about the document, sidecar, or editor state is disturbed.
|
||||||
|
Self-review during coauthoring becomes as cheap as accepting a proposal.
|
||||||
|
|
||||||
|
### 1.7 Scope (business)
|
||||||
|
|
||||||
|
**In scope:** the baseline lifecycle (initialize at first track, advance at
|
||||||
|
machine landings, pin on demand); persistence of the baseline across window
|
||||||
|
reloads (extension workspace storage); the toggle command + keybinding; a
|
||||||
|
native `vscode.diff` view (readonly baseline left, the **live document**
|
||||||
|
right — editing continues inside the diff view); test-facing surface; host
|
||||||
|
E2E + unit coverage; manual smoke.
|
||||||
|
|
||||||
|
**Out of scope (deferred, not forgotten):** baseline **splicing** (keeping
|
||||||
|
pre-accept operator edits visible across a baseline advance — §6.7, §9);
|
||||||
|
multiple named/selectable baselines; any cross-rung sharing of baselines;
|
||||||
|
diffing against arbitrary git refs; changed-line gutter indicators in the
|
||||||
|
normal (non-diff) editor view.
|
||||||
|
|
||||||
|
**Non-goals (firm, from #17):** replacing git history; a general-purpose
|
||||||
|
diff tool; changing how proposals render (F4 owns machine diffs).
|
||||||
|
|
||||||
|
### 1.8 Assumptions · Constraints · Dependencies
|
||||||
|
|
||||||
|
- **Anchor:** Feature #17. Builds on the shipped inner loop (F2 `#4` · F3
|
||||||
|
`#6` · F4 `#12` · F5 `#14`); Epic #1 is closed.
|
||||||
|
- The whole design leans on two shipped invariants: **INV-9** (the seam
|
||||||
|
`applyAgentEdit` is the single machine-edit ingress) and **INV-10**
|
||||||
|
(proposals never mutate the document) — together: *between machine
|
||||||
|
landings, every buffer change is operator-authored.*
|
||||||
|
- F3 attribution is **state, not history** (cannot reconstruct before-text)
|
||||||
|
— confirmed: F6 needs a snapshot, and takes one; it does not consume
|
||||||
|
attribution records at all.
|
||||||
|
- Nothing persisted touches `.threads/`: the cross-rung contract
|
||||||
|
(INV-14..17) and `SCHEMA_VERSION = 1` are untouched by construction
|
||||||
|
(INV-19).
|
||||||
|
- No new credential surface (INV-8 untouched); no LLM anywhere in F6 or its
|
||||||
|
tests.
|
||||||
|
|
||||||
|
### 1.9 Business Use Cases
|
||||||
|
|
||||||
|
- **BUC-1 (self-review)** Mid-session, the operator flips into the diff
|
||||||
|
view and sees exactly what *they* changed since Claude last landed text —
|
||||||
|
then flips back and keeps writing.
|
||||||
|
- **BUC-2 (deliberate pass)** Before a risky rewrite pass, the operator
|
||||||
|
pins the baseline; afterwards they review their whole pass as one diff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Solution Proposal
|
||||||
|
|
||||||
|
Keep one **baseline snapshot** per tracked document: the full document text
|
||||||
|
captured at an epoch. The epoch is initialized when the document is first
|
||||||
|
tracked, **advances automatically on every successful seam application**
|
||||||
|
(`applyAgentEdit` — the moment machine text lands), and can be **pinned to
|
||||||
|
now** by the operator. Because the seam is the only machine ingress
|
||||||
|
(INV-9/10), the diff `baseline → current buffer` shows operator-authored
|
||||||
|
changes by construction (INV-18) — no attribution filtering needed. A
|
||||||
|
`cowriting.toggleDiffView` command (with default keybinding) opens a native
|
||||||
|
`vscode.diff` editor: left side a readonly virtual document
|
||||||
|
(`cowriting-baseline:` scheme) serving the baseline text, right side **the
|
||||||
|
live document itself** — so the operator keeps editing inside the diff
|
||||||
|
view, and toggling back simply closes the diff tab. Baselines persist in
|
||||||
|
**extension workspace storage** via a vscode-free `BaselineStore` (the
|
||||||
|
`CoauthorStore` pattern), surviving window reloads without ever entering
|
||||||
|
the repo (INV-19). The machine-landing signal is one additive event on the
|
||||||
|
seam owner: `AttributionController.onDidApplyAgentEdit`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Product Personas
|
||||||
|
|
||||||
|
- **PP-1 Inner-loop coauthor** — the human writer/engineer (as F2–F5); the
|
||||||
|
only persona F6 serves.
|
||||||
|
|
||||||
|
## 4. Product Use Cases
|
||||||
|
|
||||||
|
- **PUC-1 (toggle)** In a tracked document, run the toggle (command or
|
||||||
|
keybinding) → a diff view opens: baseline left, live document right. Run
|
||||||
|
it again → the diff closes and the normal editor is back. The document
|
||||||
|
text, sidecar, threads, attributions, and proposals are untouched either
|
||||||
|
way.
|
||||||
|
- **PUC-2 (epoch follows the machine)** Accept a pending proposal → the
|
||||||
|
baseline advances past it: the accepted text does **not** appear as a
|
||||||
|
change in the diff; the operator's subsequent edits do.
|
||||||
|
- **PUC-3 (pin)** Run "pin diff baseline" → the diff goes empty (baseline =
|
||||||
|
now); everything the operator changes from here shows against the pin.
|
||||||
|
- **PUC-4 (survive reload)** Reload the window mid-session → the baseline
|
||||||
|
(and therefore the diff) is the same as before the reload.
|
||||||
|
- **PUC-5 (graceful edges)** An untracked document refuses the toggle with
|
||||||
|
a warning; a missing/ancient baseline is visible in the diff title
|
||||||
|
(epoch + when) and one pin gesture recovers; external file changes (e.g.
|
||||||
|
a git pull) show up as changes — pin recovers there too.
|
||||||
|
|
||||||
|
## 5. UX Layout
|
||||||
|
|
||||||
|
No bespoke UI surface — native VS Code primitives only (the F2–F5
|
||||||
|
Claude-Code-rubric precedent). The diff view **is** VS Code's diff editor:
|
||||||
|
readonly baseline on the left, the live, editable document on the right;
|
||||||
|
all existing decorations and comment threads keep working on the right side
|
||||||
|
because it is the same `TextDocument`. The diff tab title carries the epoch
|
||||||
|
story: `<basename> — my changes since <epoch>` where epoch reads `opened
|
||||||
|
<time>` / `Claude landed <time>` / `pinned <time>`. Two contributed
|
||||||
|
commands: **"Cowriting: Toggle Diff View"** (`cowriting.toggleDiffView`,
|
||||||
|
default keybinding `ctrl+alt+d`, `when: editorTextFocus`) and **"Cowriting:
|
||||||
|
Pin Diff Baseline to Now"** (`cowriting.pinDiffBaseline`). No status-bar
|
||||||
|
item: the diff tab itself is the indicator.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Technical Design
|
||||||
|
|
||||||
|
### 6.1 Invariants
|
||||||
|
|
||||||
|
Parent invariants INV-1..INV-17 carry over unchanged. F6 adds:
|
||||||
|
|
||||||
|
- **INV-18 (machine-factored baseline)** The baseline advances on every
|
||||||
|
successful seam application, so **text the machine landed through the
|
||||||
|
seam never appears as a change in the diff view**. With INV-9/INV-10,
|
||||||
|
the corollary: everything the diff view shows changed is
|
||||||
|
operator-authored — modulo changes made to the file from outside the
|
||||||
|
session (git operations), which are surfaced as changes and recoverable
|
||||||
|
by pin.
|
||||||
|
- **INV-19 (pure view, repo-free)** The diff view never mutates the
|
||||||
|
document, the sidecar, or attribution state; the baseline snapshot lives
|
||||||
|
in extension workspace storage, never in the repo — `.threads/`, the
|
||||||
|
cross-rung contract (INV-14..17), and `SCHEMA_VERSION` are untouched.
|
||||||
|
|
||||||
|
### 6.2 High-level architecture
|
||||||
|
|
||||||
|
One new vscode-free store plus one new controller (the F2/F3/F4 unit
|
||||||
|
pattern), and one additive event on an existing unit:
|
||||||
|
|
||||||
|
- **`BaselineStore`** (`src/baselineStore.ts`, vscode-free) — load/save one
|
||||||
|
baseline JSON per docPath under a given storage directory (the
|
||||||
|
`CoauthorStore` shape: Node `fs`, `mkdir -p`, `<dir>/baselines/<docPath>.json`).
|
||||||
|
- **`DiffViewController`** (`src/diffViewController.ts`, vscode layer) —
|
||||||
|
owns the baseline lifecycle (initialize / advance / pin), registers the
|
||||||
|
`cowriting-baseline:` `TextDocumentContentProvider` (with an `onDidChange`
|
||||||
|
emitter so an open diff refreshes when the baseline advances), implements
|
||||||
|
toggle open/close via the tab-groups API, and exposes the test-facing
|
||||||
|
surface.
|
||||||
|
- **`AttributionController.onDidApplyAgentEdit`** — a new
|
||||||
|
`vscode.EventEmitter` fired after a successful, non-no-op seam apply
|
||||||
|
(`src/attributionController.ts:232`); the single machine-landing signal
|
||||||
|
(INV-9 makes the seam the single source). Carries the document.
|
||||||
|
- **Everything else reused unchanged:** seam semantics, ProposalController,
|
||||||
|
ThreadController, CoauthorStore, Anchorer, VersionGuard (F6 never writes
|
||||||
|
the sidecar, so the guard is not consulted).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
open["tracked doc first seen"] --> init["baseline := buffer text\n(reason: opened)"]
|
||||||
|
seam["applyAgentEdit succeeds\n(accept / E2E ingress, INV-9)"] -- onDidApplyAgentEdit --> adv["baseline := buffer text\n(reason: machine-landing)"]
|
||||||
|
pin["cowriting.pinDiffBaseline\n(human)"] --> pinned["baseline := buffer text\n(reason: pinned)"]
|
||||||
|
init & adv & pinned --> bs["BaselineStore\n(extension workspace storage,\nINV-19: never the repo)"]
|
||||||
|
toggle["cowriting.toggleDiffView"] --> diff["vscode.diff\nleft: cowriting-baseline (readonly)\nright: the LIVE document"]
|
||||||
|
bs --> diff
|
||||||
|
diff -- "toggle again" --> closed["diff tab closed,\nnormal editor back"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Data model & ownership
|
||||||
|
|
||||||
|
No sidecar change of any kind (INV-19). The baseline is a private,
|
||||||
|
per-workspace extension artifact:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// <context.storageUri>/baselines/<repo-relative-docPath>.json
|
||||||
|
{
|
||||||
|
"docPath": "notes/chapter-1.md",
|
||||||
|
"text": "…full document text at the epoch…",
|
||||||
|
"capturedAt": "ISO-8601",
|
||||||
|
"reason": "opened" | "machine-landing" | "pinned"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **One baseline per document** — the newest epoch wins; no history of past
|
||||||
|
baselines (state, not history — the F3/F4 precedent).
|
||||||
|
- **Owner:** the extension alone; the file lives outside the repo (VS
|
||||||
|
Code's per-workspace storage directory), so it can never be committed,
|
||||||
|
merged, or read by another rung. Unknown-field preservation is moot.
|
||||||
|
- **Capture source is the buffer** (`document.getText()`), not the disk —
|
||||||
|
the baseline must include unsaved text, since seam landings apply to the
|
||||||
|
buffer.
|
||||||
|
|
||||||
|
### 6.4 Interfaces & contracts
|
||||||
|
|
||||||
|
- **`BaselineStore`** (vscode-free):
|
||||||
|
`load(docPath): Baseline | null` · `save(docPath, baseline): void` ·
|
||||||
|
`baselinePath(docPath): string`.
|
||||||
|
- **`DiffViewController`**:
|
||||||
|
`ensureBaseline(document)` — on first sight of a tracked doc (wired into
|
||||||
|
`extension.ts`'s existing `renderIfOpen` hook); loads the stored baseline
|
||||||
|
or captures `reason: "opened"`.
|
||||||
|
`advance(document)` — captures `reason: "machine-landing"`; fires the
|
||||||
|
provider's `onDidChange` so an open diff refreshes.
|
||||||
|
`pin(document)` — captures `reason: "pinned"`; same refresh.
|
||||||
|
`toggle(editorOrTab)` — if the active tab is this doc's baseline diff →
|
||||||
|
close it and reveal the normal editor; if the active editor is a tracked
|
||||||
|
doc → `vscode.diff(baselineUri, document.uri, title)`. Untracked →
|
||||||
|
warning.
|
||||||
|
Test-facing: `getBaseline(docPath): { text, reason, capturedAt } |
|
||||||
|
undefined` · `isDiffOpen(docPath): boolean`.
|
||||||
|
- **`AttributionController`**: adds
|
||||||
|
`readonly onDidApplyAgentEdit: vscode.Event<{ document: vscode.TextDocument }>`
|
||||||
|
— fired in `applyAgentEdit` after `applyEdit` succeeds for a real
|
||||||
|
(non-no-op) change; the seam's signature and return contract are
|
||||||
|
unchanged.
|
||||||
|
- **Commands/keybindings** (`package.json` contributions):
|
||||||
|
`cowriting.toggleDiffView` (palette + `ctrl+alt+d`) ·
|
||||||
|
`cowriting.pinDiffBaseline` (palette). Both registered as warning stubs
|
||||||
|
in the no-folder activation path (the #8 precedent).
|
||||||
|
- **`CowritingApi`** gains `diffViewController` (the E2E handle).
|
||||||
|
|
||||||
|
### 6.5 Per–Product-Use-Case design
|
||||||
|
|
||||||
|
- **PUC-1 (toggle):** `toggle` inspects `vscode.window.tabGroups` for a
|
||||||
|
`TabInputTextDiff` whose `original.scheme === "cowriting-baseline"` and
|
||||||
|
whose `modified` equals the document's URI. Present → close that tab and
|
||||||
|
`showTextDocument` the file. Absent → `ensureBaseline` then execute
|
||||||
|
`vscode.diff` with `{ preview: false }`. The right side is the same
|
||||||
|
`TextDocument` the normal editor shows — no content is copied, so
|
||||||
|
"without losing editor state" holds trivially: the normal editor tab is
|
||||||
|
left in place; the diff is an additional tab on the same buffer.
|
||||||
|
- **PUC-2 (epoch follows the machine):** `DiffViewController` subscribes to
|
||||||
|
`onDidApplyAgentEdit` at activation. On fire: `advance(document)` —
|
||||||
|
synchronous capture + `BaselineStore.save` + provider `onDidChange`
|
||||||
|
(an open diff's left side updates in place). Both seam drivers are
|
||||||
|
covered by the one event: accept (`ProposalController.accept`) and the
|
||||||
|
hidden E2E command (`cowriting.applyAgentEdit`).
|
||||||
|
- **PUC-3 (pin):** command → guard tracked → `pin(activeEditor.document)`
|
||||||
|
→ if the diff is open it visibly empties (left = right).
|
||||||
|
- **PUC-4 (survive reload):** `ensureBaseline` prefers the stored file; a
|
||||||
|
baseline written at the last landing/pin is what the reloaded window
|
||||||
|
diffs against. (The toggle's *tab* does not need to survive reload — the
|
||||||
|
baseline does.)
|
||||||
|
- **PUC-5 (graceful edges):** untracked / non-file schemes → warning, no
|
||||||
|
diff (the F2 `isTracked` rule). Storage unavailable
|
||||||
|
(`context.storageUri` undefined or write failure) → degrade to in-memory
|
||||||
|
baselines + one warning (the session still works; reload survival is
|
||||||
|
lost). Ancient baseline (workspace reopened weeks later) → the diff
|
||||||
|
title shows `since opened <date>` — self-describing — and pin recovers.
|
||||||
|
External-to-session file changes appear as changes (INV-18's stated
|
||||||
|
modulo); pin recovers.
|
||||||
|
|
||||||
|
### 6.6 Non-functional requirements & cross-cutting concerns
|
||||||
|
|
||||||
|
Capture cost is O(document) at three rare moments (first track, machine
|
||||||
|
landing, pin) — trivial at inner-loop scale. Storage is one document-sized
|
||||||
|
JSON per tracked doc in VS Code's workspace storage (local machine, same
|
||||||
|
trust domain as the working tree; baseline text is operator content, the
|
||||||
|
document's own sensitivity class). The content provider serves from the
|
||||||
|
in-memory copy (disk only on load). No LLM, no network, no new credential
|
||||||
|
surface (INV-8). Skip non-text/oversized files (the F2 rule, via
|
||||||
|
`isTracked`). The diff editor is native — no rendering machinery to
|
||||||
|
maintain.
|
||||||
|
|
||||||
|
### 6.7 Key decisions & alternatives considered
|
||||||
|
|
||||||
|
| Decision | Chosen | Alternatives rejected |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Baseline epoch (the load-bearing fork)** | **Machine-landing–anchored auto-baseline**: initialize at first track, advance on every successful seam application, plus a human **pin** gesture. INV-9/10 make the resulting diff operator-authored by construction (INV-18) | **turn start** (under F4 propose-by-default the document doesn't change at turn start — the epoch collapses into the accept; not a distinct moment anymore); **last save** (duplicates native `compareWithSaved` / local history / timeline — #17's complement-not-duplicate rule); **fixed session-start** (machine landings bleed into the "my changes" diff — breaks INV-18); **multiple selectable baselines** (a selector UI for epochs nobody asked for — YAGNI; pin covers the operator-chosen case; selectable is additive later) |
|
||||||
|
| Pre-accept operator edits absorbed by an advance | Accepted trade-off: the diff reads "my changes since the machine last landed", not "my whole session"; pin gives manual control | **baseline splicing** (apply the accepted replacement *into* the baseline so older operator edits stay visible — needs current→baseline range mapping with murky edge cases when the machine edits operator-authored-since-baseline text; deferred additive refinement, §9) |
|
||||||
|
| Snapshot home | **Extension workspace storage** (`context.storageUri`) via vscode-free `BaselineStore`; survives reload; zero repo/contract footprint (INV-19) | **sidecar `.threads/`** (full document text per accept dwarfs the artifact and churns git diffs — INV-2; drags INV-14..17 into a local view-state concern no other rung needs); **gitignored repo cache** (litters the operator's working tree; needs `.gitignore` management; risks accidental commit); **in-memory only** (baseline dies at reload — the post-reload diff is empty and lies) |
|
||||||
|
| Whose changes the diff shows | Whole-document diff; operator-authorship is INV-18's corollary, **no filtering** | **F3-attribution-filtered diff** (F3 is state-not-history — cannot reconstruct before-text; #17 itself notes this); **per-author diff lenses** (no user story; heavy) |
|
||||||
|
| View primitive | **Native `vscode.diff`**: readonly `cowriting-baseline:` virtual doc left, the **live document** right (editable in place; decorations/threads keep working) | **decoration-based inline diff** in the normal editor (re-implements diff rendering VS Code already has; F4's fenced-diff primitive is for proposals, not whole documents); **webview diff** (bespoke UI against the F2–F5 native-primitives precedent) |
|
||||||
|
| Machine-landing signal | Additive `onDidApplyAgentEdit` event on the seam owner — INV-9's single ingress is the single signal source | **call-site wiring** (ProposalController + the hidden command both call `advance` — two sites today, N tomorrow, and each new seam driver must remember); **watching attribution records** (indirect, save-coupled, reconstructive) |
|
||||||
|
| Toggle keybinding | `ctrl+alt+d` on all platforms (unbound by default in stock VS Code; user-remappable) | `cmd+alt+d` (macOS Dock toggle); `cmd+k d` (taken: compare-with-saved) |
|
||||||
|
|
||||||
|
### 6.8 Testing strategy
|
||||||
|
|
||||||
|
- **Unit (vitest, vscode-free):** `BaselineStore` round-trip (save → load,
|
||||||
|
nested docPaths, overwrite-in-place, missing → null); baseline JSON shape
|
||||||
|
stability; storage-dir path mapping.
|
||||||
|
- **Host E2E (`@vscode/test-electron`, the F2–F5 pattern, no LLM):**
|
||||||
|
toggle on a tracked doc → diff tab open (original scheme
|
||||||
|
`cowriting-baseline`), provider text = open-time text, document + sidecar
|
||||||
|
untouched (INV-19) → type → baseline unchanged, buffer changed →
|
||||||
|
programmatic propose + accept → baseline advanced past the accepted text
|
||||||
|
(INV-18: `getBaseline().text` contains the replacement; `reason:
|
||||||
|
"machine-landing"`) → operator edit after the landing → baseline ≠
|
||||||
|
buffer (the operator delta) → pin → baseline = buffer, `reason:
|
||||||
|
"pinned"` → toggle again → diff tab closed, normal editor active →
|
||||||
|
baseline file present on disk under the storage dir with the expected
|
||||||
|
content (the reload-survival half is the store's unit-tested round-trip)
|
||||||
|
→ untracked doc → toggle warns, no diff tab.
|
||||||
|
- **Live smoke (manual, documented — `docs/MANUAL-SMOKE-F6.md`):** open a
|
||||||
|
tracked doc → edit → toggle → see own changes → ask Claude to edit a
|
||||||
|
selection → accept → toggle: the accepted text is *not* shown as a
|
||||||
|
change → pin → diff empties.
|
||||||
|
|
||||||
|
### 6.9 Failure modes, rollback & flags
|
||||||
|
|
||||||
|
Storage unavailable / write failure → in-memory fallback + one warning
|
||||||
|
(reload survival degrades; nothing else). Crash between a seam landing and
|
||||||
|
the baseline write → the stored baseline is one landing behind: on reload
|
||||||
|
the machine's last text shows as a change (a visible, self-describing
|
||||||
|
stale-epoch state, the F4-husk spirit) — one pin recovers; the capture is
|
||||||
|
synchronous in the event handler to keep this window minimal. Ancient
|
||||||
|
baseline → self-describing title + pin. External file changes → shown as
|
||||||
|
changes (INV-18's modulo), pin recovers. No feature flag: the feature is a
|
||||||
|
pure view (INV-19) — not toggling it is the off state; rollback is
|
||||||
|
reverting the PR with zero data migration (the baseline cache is
|
||||||
|
disposable by design).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Delivery Plan
|
||||||
|
|
||||||
|
### 7.1 Approach / strategy
|
||||||
|
|
||||||
|
One planning-and-executing session (F6 = #17), plan written just-in-time
|
||||||
|
from this spec — the F2–F5 precedent.
|
||||||
|
|
||||||
|
### 7.2 Slicing plan
|
||||||
|
|
||||||
|
- **SLICE-1** `BaselineStore` (vscode-free) + unit tests (round-trip,
|
||||||
|
paths, overwrite).
|
||||||
|
- **SLICE-2** Seam event (`onDidApplyAgentEdit`, additive) +
|
||||||
|
`DiffViewController` lifecycle: ensure/advance/pin, content provider with
|
||||||
|
refresh, storage-unavailable fallback.
|
||||||
|
- **SLICE-3** Toggle UX: `vscode.diff` open/close via tab groups, epoch
|
||||||
|
titles, `package.json` contributions (commands + keybinding + no-folder
|
||||||
|
stubs), `CowritingApi.diffViewController` + test-facing surface.
|
||||||
|
- **SLICE-4** Host E2E per §6.8 + `docs/MANUAL-SMOKE-F6.md` + README
|
||||||
|
pointer.
|
||||||
|
|
||||||
|
E2E are first-class plan tasks (handbook §9/§4; this app's required tier is
|
||||||
|
host E2E — a VS Code extension has no browser surface or deploy stage, the
|
||||||
|
F2–F5 precedent).
|
||||||
|
|
||||||
|
### 7.3 Rollout / launch plan
|
||||||
|
|
||||||
|
Still non-shippable (no marketplace publish). "Done" = issue #17 acceptance
|
||||||
|
met: toggle there-and-back without losing editor state, against a
|
||||||
|
coauthoring-meaningful baseline; unit + host E2E green; live smoke
|
||||||
|
performed once on this machine.
|
||||||
|
|
||||||
|
### 7.4 Risks & mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
| --- | --- |
|
||||||
|
| Baseline advance absorbs pre-accept operator edits (operator surprise) | Diff title names the epoch ("since Claude landed <time>"); pin gives manual control; splicing is the documented additive follow-up (§9) |
|
||||||
|
| `ctrl+alt+d` collides with an operator's keymap | Default keybinding only — remappable; command palette always works |
|
||||||
|
| Storage dir missing/unwritable | In-memory fallback + one warning; feature degrades to session-scoped, never errors out of the toggle |
|
||||||
|
| Diff left side stale while baseline advances | Provider `onDidChange` fired on every advance/pin — the open diff refreshes in place |
|
||||||
|
| Future seam drivers forget the baseline | They can't — the advance subscribes to the seam's own event (INV-9: one ingress, one signal) |
|
||||||
|
| Operator expects "my whole session" semantics | §6.7 records the trade-off; the epoch is visible in the title; pin-at-session-start reproduces session semantics manually |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Traceability matrix
|
||||||
|
|
||||||
|
| Requirement (issue #17 acceptance) | Use case | Design | Slice |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Toggle (command and/or keybinding) editor ↔ diff, and back | PUC-1 | §6.4, §6.5 | SLICE-3 |
|
||||||
|
| Diff is the operator's own changes | PUC-2 | INV-18, §6.2 | SLICE-2 |
|
||||||
|
| Against a coauthoring-meaningful baseline | PUC-2/3 | §6.7 (epoch fork) | SLICE-2 |
|
||||||
|
| Without losing editor state | PUC-1 | §6.5 (live doc is the right side) | SLICE-3 |
|
||||||
|
| Sidecar/contract untouched if persisted artifacts exist | — | INV-19, §6.3 | all |
|
||||||
|
| Complement, not duplicate, git's diff/timeline | — | §6.7 (save rejected) | — |
|
||||||
|
| Unit + host E2E coverage, no LLM in CI | — | §6.8 | SLICE-1/4 |
|
||||||
|
|
||||||
|
## 9. Open Questions & Decisions log
|
||||||
|
|
||||||
|
- **RESOLVED (this session — the forks routed by #17):**
|
||||||
|
(a) **baseline** = machine-landing–anchored auto-baseline (init at first
|
||||||
|
track, advance on every successful seam application) + human pin; turn
|
||||||
|
start collapses into accept under F4, save duplicates native features,
|
||||||
|
fixed session-start breaks INV-18, selectable-multi is YAGNI;
|
||||||
|
(b) **snapshot home** = extension workspace storage via vscode-free
|
||||||
|
`BaselineStore` — never the sidecar, no contract impact (INV-19);
|
||||||
|
(c) **whose changes** = whole-document diff, operator-authored by
|
||||||
|
construction (INV-18) — no attribution filtering;
|
||||||
|
(d) **git relationship** = complement: this diffs against coauthoring
|
||||||
|
moments, git diffs against commits; save-points stay native;
|
||||||
|
(e) **view primitive** = native `vscode.diff`, live document on the
|
||||||
|
right; (f) **landing signal** = `onDidApplyAgentEdit` on the seam owner.
|
||||||
|
- **OPEN → later:** baseline **splicing** (keep pre-accept operator edits
|
||||||
|
visible across an advance — needs current→baseline range mapping);
|
||||||
|
multiple named baselines / epoch selector (additive on the same store);
|
||||||
|
changed-line gutter indicators in the normal view (a decoration consumer
|
||||||
|
of the same baseline); whether a future rung wants to *share* a pinned
|
||||||
|
baseline (would move it into the contract — deliberately not now).
|
||||||
|
- **Deferred decisions (autonomous-mode calls logged for the operator):**
|
||||||
|
the absorb-on-advance semantics (vs. splicing) and the `ctrl+alt+d`
|
||||||
|
default keybinding were decided without operator input — both are
|
||||||
|
cheap to revisit.
|
||||||
|
|
||||||
|
## 10. Glossary & References
|
||||||
|
|
||||||
|
- **Baseline** — the full text of a tracked document captured at an epoch;
|
||||||
|
the diff view's left side. **Epoch** — the moment the baseline was
|
||||||
|
captured: `opened`, `machine-landing`, or `pinned`. **Machine landing** —
|
||||||
|
a successful `applyAgentEdit` seam application (the only way machine text
|
||||||
|
enters the document, INV-9/10). **Pin** — the human gesture resetting the
|
||||||
|
baseline to now. **Advance** — the automatic baseline re-capture at a
|
||||||
|
machine landing (INV-18).
|
||||||
|
- Feature #17 (F6) · Epic #1 (closed) · F5 #14 (PR #15) · F4 #12 (PR #13) ·
|
||||||
|
F3 #6 (PR #7) · F2 #4 (PR #5) · parent specs `coauthoring-inner-loop.md`,
|
||||||
|
`coauthoring-attribution.md`, `coauthoring-propose-accept.md`,
|
||||||
|
`coauthoring-cross-rung-format.md`, `coauthoring-sidecar-contract.md` ·
|
||||||
|
lineage `ben.stull/rfc-app#48`.
|
||||||
Reference in New Issue
Block a user