From cc6764398938532f67d320f71cc37682b8407b07 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 11 Jun 2026 07:52:43 -0700 Subject: [PATCH] =?UTF-8?q?spec(f6):=20v0.2.0=20=E2=80=94=20diff-view=20on?= =?UTF-8?q?=20any=20file=20+=20untitled;=20global=20storage=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- specs/coauthoring-diff-view.md | 107 +++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 26 deletions(-) diff --git a/specs/coauthoring-diff-view.md b/specs/coauthoring-diff-view.md index 82b18e9..709454d 100644 --- a/specs/coauthoring-diff-view.md +++ b/specs/coauthoring-diff-view.md @@ -8,14 +8,15 @@ status: graduated | **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` | +| **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 | --- @@ -23,7 +24,7 @@ status: graduated ### 1.1 Executive Summary -F6 gives the human coauthor the review surface the machine already has. F4 +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 @@ -86,7 +87,8 @@ Self-review during coauthoring becomes as cheap as accepting a proposal. **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 +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. @@ -167,10 +169,11 @@ seam owner: `AttributionController.onDidApplyAgentEdit`. 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. +- **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 @@ -203,8 +206,9 @@ Parent invariants INV-1..INV-17 carry over unchanged. F6 adds: 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. + 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 @@ -245,15 +249,22 @@ No sidecar change of any kind (INV-19). The baseline is a private, per-workspace extension artifact: ```jsonc -// /baselines/.json +// 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: +// /baselines/.json { - "docPath": "notes/chapter-1.md", + "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 @@ -314,14 +325,14 @@ per-workspace extension artifact: 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 ` — self-describing — and pin recovers. - External-to-session file changes appear as changes (INV-18's stated - modulo); pin recovers. +- **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 ` + — 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 @@ -341,7 +352,7 @@ maintain. | --- | --- | --- | | **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) | +| 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 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) | @@ -476,8 +487,52 @@ performed once on this machine. 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`. +- 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 F2–F5 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-independently** — `DiffViewController` 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.docPath` → `Baseline.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.