Files
vscode-cowriting-plugin-con…/specs/coauthoring-diff-view.md
T
Ben Stull cc67643989 spec(f6): v0.2.0 — diff-view on any file + untitled; global storage (#19)
Broaden F6 beyond workspace files: own diffable predicate (file:/untitled:)
decoupled from F2 isTracked; baseline → global storage keyed by URI hash;
untitled buffers in-memory. Adds §11 amendment + updates §1.7/§6.3/§6.5/§6.7/
INV-19 and the change log. Built in session 0019.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 07:52:45 -07:00

31 KiB
Raw Blame History

status
status
graduated

Solution Design: Diff-View Toggle (F6)

Author(s) Ben Stull (with Claude)
Reviewers / approvers Ben Stull
Status draft
Version v0.2.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/coang-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
2026-06-11 v0.2.0 Broaden to any file + untitled buffers (not just workspace files); baseline → global storage keyed by a URI hash; F6 decoupled from F2 isTracked with its own diffable predicate. #19, session 0019. See §11. Ben Stull + Claude

1. Business Context

1.1 Executive Summary

F6 gives the human coauthor the review surface the 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 F2F4 (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 global storage — v0.2.0, §11); 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 F2F5); 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) The toggle works on any text document — a file in or out of the workspace, or an untitled buffer (v0.2.0, §11); only a non-text-editor focus warns. 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 F2F5 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 global storage (context.globalStorageUri, v0.2.0 — was 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).
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:

// v0.2.0 (#19): GLOBAL storage, keyed by a hash of the document URI so ANY
// file (in- or out-of-folder) maps to a stable filename:
// <context.globalStorageUri>/baselines/<sha256(documentUri)>.json
{
  "uri": "file:///abs/path/notes/chapter-1.md",  // the document URI (identity)
  "text": "…full document text at the epoch…",
  "capturedAt": "ISO-8601",
  "reason": "opened" | "machine-landing" | "pinned"
}

Untitled buffers (v0.2.0): an unsaved buffer (untitled: scheme) has no durable identity to persist against, so its baseline is in-memory only (lost on reload) — the same degrade the storage-unavailable path uses. Saving it makes it a file: doc with a persisted baseline.

  • 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 PerProduct-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): v0.2.0 (§11) — the diffable predicate is any file: or untitled: doc, so only a non-text-editor focus → warning, no diff. Untitled buffers diff in-memory (no persistence). Storage unavailable (context.globalStorageUri write failure) → degrade to in-memory baselines
    • one warning (the session still works; reload survival is lost). Ancient baseline (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-landinganchored 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 global storage (context.globalStorageUri, v0.2.0 — was workspace storage) via vscode-free BaselineStore, keyed by a URI hash so it works for any file; 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 F2F5 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 F2F5 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 F2F5 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 F2F5 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
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-landinganchored 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 · F5 #14 (PR #15) · F4 #12 (PR #13) · F3 #6 (PR #7) · F2 #4 (PR #5) · v0.2.0 follow-up #19 · 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.

11. Amendment v0.2.0 — diff-view on any file (#19, session 0019)

v0.1.0 scoped F6 to tracked documents (a saved file inside the opened workspace folder), reusing F2's isTracked. In manual testing this surfaced as the toggle refusing untitled buffers and out-of-folder files — friction with no real cause, since F6 needs no .threads/ sidecar, only a stable document identity and a storage home. This amendment broadens F6 to any text document; the inner-loop ladder F2F5 is unchanged.

Deltas (supersede the matching v0.1.0 text above):

  • Diffable predicate (replaces the isTracked gate for F6). A document is diffable iff its scheme is file: or untitled:. F6 no longer consults the workspace root. The two commands are therefore registered workspace-independentlyDiffViewController is constructed before the no-folder activation branch, so cowriting.toggleDiffView / cowriting.pinDiffBaseline are live (never stubbed) even with no folder open. Only a non-text-editor focus warns.
  • Snapshot home = global storage (replaces workspace storage in §2/§6.3/§6.7 and INV-19). Baselines live under context.globalStorageUri (machine-wide, always present, independent of any workspace), keyed by sha256(documentUri) — one filesystem-safe key for in-folder, out-of-folder, and (transiently) untitled docs. Still never the repo; INV-19 holds verbatim apart from the storage-root word. Baseline.docPathBaseline.uri.
  • Untitled buffers are in-memory only. An untitled: doc has no durable identity, so its baseline is held in memory and lost on reload (the storage-unavailable degrade); the moment it is saved it becomes a file: doc with a persisted baseline. The diff title carries an (unsaved) marker.
  • Machine-landing advance wiring is unchanged in scope. The seam fires only on workspace files, so onDidApplyAgentEdit → advance stays in the with-root branch; F6's baseline capture-on-open is self-wired in the controller for any diffable doc.

Unchanged: the view primitive (native vscode.diff, live doc on the right), the epoch model (opened/machine-landing/pinned + pin), INV-18, the no-LLM host-E2E tier. Tests: host E2E adds an out-of-workspace-file case (persisted in global storage) and an untitled-buffer case (in-memory, not persisted), plus a no-folder activation case proving the toggle works folder- less; BaselineStore unit tests re-keyed.

Shipped: #19 (session 0019) — see that session's transcript.