20 KiB
status
| status |
|---|
| graduated |
Solution Design: Live Human/Claude Attribution in the Buffer (F3)
| Author(s) | Ben Stull (with Claude) |
| Reviewers / approvers | Ben Stull |
| Status | draft |
| Version | v0.1.0 |
| Source artifacts | Epic benstull/vscode-cowriting-plugin#1 · Feature #6 (F3) · Parent spec: vscode-cowriting-plugin-content/specs/coauthoring-inner-loop.md (graduated; §6.3 attributions[]/provenance, §9 OPEN→F3/F4) · F2 shipped: #4 (PR #5, session 0004) · Lineage: ben.stull/rfc-app#48 |
Change log
| Date | Version | Change | By |
|---|---|---|---|
| 2026-06-10 | v0.1.0 | Initial draft — brainstorming session 0006 | Ben Stull + Claude |
1. Business Context
1.1 Executive Summary
F3 makes the buffer an honest record of authorship: as a document is
coauthored, the editor shows — live — which spans were written by the human
and which by Claude, and persists that record git-natively in the same
sidecar F2 shipped, filling the reserved attributions[] extension point on the
shared anchors/Provenance primitives. It also lands the project's first
live @cline/sdk turn — the minimal machine-edit ingress that gives
attribution something real to attribute — behind a programmatic apply-edit
seam that F4's propose/accept will later drive.
1.2 Background
F2 (#4) gave regions durable discussion; nothing yet records who wrote what.
Epic #1's second acceptance pillar is "see, live in the buffer, which spans are
human vs. Claude." The parent spec deliberately reserved attributions: []
(INV-4) and deferred "where Claude runs + provider/auth" to "the first live
@cline/sdk turn, F3/F4" — i.e., to exactly this design. Issue #6 routed the
open fork (what counts as a machine edit before F4 exists; the attributions[]
shape; mixed-edit granularity) to this brainstorming session.
1.3 Business Actors / Roles
- Coauthor (human) — the writer/engineer authoring prose/specs in VS Code.
- Coauthor (machine) — Claude via
@cline/sdk; active for the first time (F2 reserved theagentprovenance author; F3 uses it).
1.4 Problem Statement
Once a machine edit lands in the buffer it is indistinguishable from human typing. There is no basis for trust ("did I write this claim or did it?"), no provenance to carry up the ladder, and no substrate for F4's propose/accept to render against.
1.5 Pain Points
- No visible, live distinction between human-authored and Claude-authored spans.
- No durable authorship record: attribution (if any) dies on reload.
- No machine-edit pathway at all yet — F2 is human-only by design.
1.6 Targeted Business Outcomes
Human and Claude spans visibly distinguished as edits happen; the attribution survives further editing (re-anchor) and reload (sidecar persistence); a real Claude edit can be invoked from the editor; F4 inherits both the attribution substrate and a clean apply-edit seam.
1.7 Scope (business)
In scope: live in-buffer rendering of human/Claude span attribution;
capturing human keystrokes and applied machine edits as attributed anchored
spans; the attributions[] shape; character-precise split/merge of mixed edits;
attribution survival across edits (re-anchor / orphan); git-native persistence +
reload; the programmatic apply-edit seam; a minimal live @cline/sdk turn
(edit-selection command) on the claude-code provider.
Out of scope (deferred, not forgotten): F4 propose/accept (keep/reject UX, inline diff — it will drive this seam); F5 cross-rung format + Gitea round-trip (#46); attribution of edits made outside the session (external editors, git merges) beyond the orphan rule; per-character blame depth (the span is the unit); multi-file orchestration; Anthropic API-key auth (config option later); chat-panel UX; any server.
1.8 Assumptions · Constraints · Dependencies
- Parent: Epic #1. Blocked by: F2 #4 (shipped). Feeds: F4 (propose/accept renders into this substrate and drives the seam); relates to F5.
- Builds on the graduated parent spec's primitives: shared
anchors(INV-4),Provenance(kind: human | agent), the Anchorer's resolve/orphan ladder (INV-1/INV-3), the per-document sidecar (INV-2). - F2's no-credentials invariant (parent INV-5) ends here by design — but
with zero key handling: the live turn uses the SDK's built-in
claude-codeprovider ("Use Claude Code SDK with Claude Pro/Max subscription"), riding the operator's existing local Claude Code login. The extension stores no secret of any kind. - Requires Claude Code installed + signed in for the live turn; everything else (tracking, rendering, persistence, tests) works without it.
- Native primitives:
vscode.window.createTextEditorDecorationType(rendering),onDidChangeTextDocument(edit deltas — the Anchorer already consumes it),WorkspaceEdit(the seam's application path).
1.9 Business Use Cases
- BUC-1 A coauthor watches authorship accumulate honestly as they and Claude write: each author's spans are visibly theirs, live, and the record survives editing and reopening the workspace.
- BUC-2 A coauthor selects a region, asks Claude to edit it, and the resulting machine edit lands visibly Claude-attributed.
2. Solution Proposal
Fill the sidecar's reserved attributions[] with anchored, author-attributed
spans maintained live by a pure span-algebra tracker: human keystrokes produce
human spans; edits applied through a new programmatic apply-edit seam
produce agent spans. Mixed edits split character-precisely; adjacent same-author
spans coalesce. Spans persist via the existing Store/Anchorer (fingerprint on
save; resolve-or-orphan on load — never guess). A minimal live turn — an
edit-selection command on the SDK's claude-code provider — drives the seam so
there is a real machine author from day one. Rendering is the native Decorations
API: Claude spans tinted, human spans gutter-marked, unattributed text plain.
3. Product Personas
- PP-1 Inner-loop coauthor — the human writer/engineer (as in F2).
- PP-2 Machine coauthor (Claude) — now active: authors edits via the live
turn; its spans carry
agentprovenance.
4. Product Use Cases
- PUC-1 Type in a tracked document → the new/changed span renders human-attributed, live.
- PUC-2 Select a region, run "Ask Claude to edit selection," give an instruction → the replacement lands Claude-attributed, live.
- PUC-3 Edit inside a Claude-attributed span → the span splits character-precisely; the human's characters are human-attributed; the remainder stays Claude's.
- PUC-4 Reopen the workspace → attributed spans reload from the sidecar at their re-resolved anchors; un-resolvable spans surface as orphaned, never silently moved.
- PUC-5 Toggle attribution decorations on/off.
5. UX Layout
No new UI surface. Attribution renders as decorations: a subtle background tint on Claude-attributed spans, a thin gutter bar on human-attributed spans, nothing on unattributed text (text predating tracking — absence of a span is the honest record). Two contributed commands: "Cowriting: Ask Claude to edit selection" (selection + instruction input box → machine edit) and "Cowriting: Toggle attribution". Orphaned attributions are dropped from decoration and surfaced as a status-bar count (details in the output channel) — visible, flagged, recoverable, per F2's orphan precedent.
6. Technical Design
6.1 Invariants
Parent-spec invariants INV-1..INV-4 carry over unchanged. F3 adds:
- INV-6 An attribution span is never silently moved or re-authored: its location is re-resolved by fingerprint (or orphaned), and its author only changes by the explicit split/clip rules of the span algebra.
- INV-7 Attribution is char-honest: the span algebra attributes exactly the characters each author produced (split character-precisely; coalesce only adjacent same-author spans). No threshold heuristics, no whole-span flips.
- INV-8 The extension stores no credentials. The live turn delegates
auth entirely to the local Claude Code installation (
claude-codeprovider); if it is absent or signed out, the live turn fails gracefully with guidance — tracking, rendering, and persistence are unaffected. - INV-9 Machine edits enter the buffer only through the seam
(
applyAgentEdit), so attribution can never mistake an agent edit for typing; F4 inherits the same guarantee.
6.2 High-level architecture
Two new vscode-free units (unit-testable, like the POC's cline.ts and F2's
model/anchorer) plus thin editor-facing wiring:
attributionsmodel — the typedattributions[]shape on the artifact (replaces theunknown[]placeholder;schemaVersionstays 1 — additive).- AttributionTracker — pure span algebra over offset ranges:
applyChange(spans, edit, author) → spans(shift/split/clip/coalesce). - Apply-edit seam —
applyAgentEdit(doc, range, newText, provenance): applies aWorkspaceEditand registers it (keyed by document version) so the tracker attributes the resulting change event to the agent. Exposed as an internal API + a command, so tests drive it with no LLM. - LiveTurn — the edit-selection command: instruction →
@cline/sdk(claude-codeprovider, building on the POC's vscode-freecline.tsdriver) → replacement text → the seam. - AttributionRenderer — two
TextEditorDecorationTypes + the toggle; re-renders on tracker change. - CoauthorStore / Anchorer — reused unchanged.
keystroke ─────────────▶ AttributionTracker (author = human)
applyAgentEdit (seam) ─▶ WorkspaceEdit + pending-edit registry ─▶ AttributionTracker (author = agent)
live turn ─▶ selection + instruction ─▶ @cline/sdk (claude-code) ─▶ seam
document change ───────▶ shift/split/clip/coalesce ─▶ AttributionRenderer (decorations)
save ──────────────────▶ buildFingerprint per span ─▶ sidecar attributions[] (CoauthorStore)
load / external change ▶ resolve per span ─▶ render | orphaned (Anchorer ladder, INV-1)
6.3 Data model & ownership
Same sidecar, same anchors map, same Provenance — attributions[] is filled,
not reshaped (parent INV-4; no schemaVersion bump):
"attributions": [
{
"id": "at1",
"anchorId": "a7", // shared anchors map — same Fingerprint primitive
"author": { "kind": "agent", "id": "claude",
"agent": { "sdk": "@cline/sdk", "model": "…", "sessionId": "…" } },
"createdAt": "ISO-8601",
"updatedAt": "ISO-8601", // bumped when the span's extent/fingerprint changes
"turnId": "turn-…" // optional; groups all spans applied by one live turn
}
]
- Orphanhood is not persisted — exactly like F2 threads, it is a resolve-time outcome: fingerprint fails → orphaned (visible, flagged), never guessed.
- Unattributed text is the absence of a span — text predating tracking has no attribution record, honestly.
- Ownership/format rules unchanged: extension is the sole writer; stable key
ordering; ids via
newId().
6.4 Interfaces & contracts
- AttributionTracker (pure):
applyChange(spans, edit, author): spans·coalesce(spans): spans· span ={ range: OffsetRange, author: Provenance, id, turnId? }. Reusesanchorer.shiftsemantics for untouched spans. - Seam:
applyAgentEdit(doc, range, newText, provenance): Promise<boolean>— applies + registers; also contributed as an internal command (E2E entry point). - LiveTurn:
editSelection(doc, selection, instruction): Promise<void>— resolves theclaude-codeprovider, runs one SDK turn producing replacement text for the selection, calls the seam withagentprovenance (model + sessionId from the turn). - AttributionRenderer:
render(editor, spans)·setVisible(boolean). - Store/Anchorer: existing contracts, now also carrying attributions.
6.5 Per–Product-Use-Case design
- PUC-1 (type):
onDidChangeTextDocument→ for each content change, the tracker shifts spans past the edit, clips/deletes spans overlapping a deletion, splits a different-author span around an insertion, attributes the inserted range to human (no pending seam registration for this document version), then coalesces. Render. - PUC-2 (live turn): command guards (selection non-empty, document tracked,
Claude Code available) → input box → SDK turn (replacement text only —
single-range, the selection) →
applyAgentEdit→ the change event arrives with a matching registration → attributed to agent withturnId. Render. - PUC-3 (mixed edit): the insertion-inside-a-span case of PUC-1's algebra: split at the insertion point, new human span between the two agent halves (INV-7). Same rule symmetric for agent edits inside human spans.
- PUC-4 (reload / external change): on open or sidecar/document external
change:
load→ per attributionresolve(fingerprint)→ live span or orphaned (counted, not rendered). On save: refresh fingerprints from live spans (updatedAtbumped only for changed spans), persist. - PUC-5 (toggle): flips renderer visibility; tracking continues regardless.
6.6 Non-functional requirements & cross-cutting concerns
Tracking is O(spans) per change event — fine at human scale; coalescing bounds span growth. Persistence happens on save (not per keystroke), as F2 does. Resolution cost on load matches F2 (O(document) per anchor). No secrets at all (INV-8). Skip non-text/oversized files (F2 rule). Single editor writer. Live-turn latency is user-visible: show progress notification; the buffer is not locked (edits during a turn are handled by the version-keyed registration — a stale turn result fails to apply and reports, rather than corrupting).
6.7 Key decisions & alternatives considered
| Decision | Chosen | Alternatives rejected |
|---|---|---|
| Machine-edit ingress | Programmatic seam + minimal live SDK turn driving it | seam-only (no real machine author; demo is simulated); live-turn-only (couples tracking to the SDK; F4 has no clean ingress to drive) |
| Provider/auth | claude-code provider (local Claude Code Pro/Max login; zero key handling) |
Anthropic API key in SecretStorage (ignores the plan already paid for; adds key-handling surface — deferred as a config option); Cline-account OAuth (couples UX to Cline's auth service); env var (no user story) |
| Mixed-edit granularity | Character-precise split + same-author coalescing (INV-7) | whole-span re-attribution (dishonest at the edges); threshold-based (arbitrary, not strictly honest) |
| Attribution record | Span-list on shared anchors (state, not history) |
event log + replay (truthful history but heavy sidecar churn; re-anchoring a log is hard); hybrid (YAGNI) |
| Rendering | Claude tint + human gutter bar + plain unattributed; toggle | Claude-only (human record invisible); both tinted (visually heavy prose) |
| Live-turn UX | Edit-selection command (selection + instruction → replacement) | whole-document multi-range edits (big bite for "minimal ingress"); chat panel (new surface F4 will reshape) |
| Orphan persistence | Resolve-time (not persisted), per F2 threads | persisted status field (duplicates what resolution derives; risks stale state) |
6.8 Testing strategy
- Unit (vitest, vscode-free): AttributionTracker span algebra exhaustively — insert/delete at/around/inside spans, split, clip, coalesce, multi-edit sequences, both author directions; model round-trip (serialize → reload, stable formatting); resolve→orphan for attribution fingerprints.
- Host E2E (
@vscode/test-electron, F2 pattern): type → human span → seam command edit → agent span → save → reload → spans restored → edit above → re-anchored → mangle anchored text → orphaned. No LLM in CI — E2E drives the seam. - Live-turn smoke (manual, documented): select → instruct → Claude edit lands attributed; run on a signed-in Claude Code machine. Failure-path check: signed-out / missing Claude Code → graceful error (INV-8).
6.9 Failure modes, rollback & flags
Orphaned attributions (INV-1/INV-6) — handled, recoverable (re-resolve if the
text reappears). Live-turn failures (no Claude Code, signed out, model error,
stale document version) — surfaced as notifications; never partial-applied.
Sidecar merge conflicts — plain JSON, human-resolvable (unchanged). No feature
flag: additive; decorations have a toggle; absence of attributions[] entries
is the off state.
7. Delivery Plan
7.1 Approach / strategy
One planning-and-executing session (F3 = #6), plan written just-in-time from this spec, per the F2 precedent.
7.2 Slicing plan
- SLICE-1 Typed
attributions[]model (+ round-trip tests). - SLICE-2 AttributionTracker span algebra (+ exhaustive unit tests).
- SLICE-3 Seam (
applyAgentEdit+ registry) and human-edit wiring + decoration rendering (tint/gutter/toggle). - SLICE-4 Persistence on save; reload / external-change resolve; orphan surfacing.
- SLICE-5 Live
claude-codeturn (edit-selection command) + documented manual smoke + graceful-failure paths. - SLICE-6 Host E2E (type → attribute → seam edit → attribute → reload → re-anchor → orphan).
E2E are first-class plan tasks (handbook §4 / parent §6.8), not a follow-up.
7.3 Rollout / launch plan
Still non-shippable (no marketplace publish). "Done" = issue #6 acceptance met: unit + host E2E green, live smoke performed once on this machine.
7.4 Risks & mitigations
| Risk | Mitigation |
|---|---|
| Span churn from heavy typing (many tiny spans) | coalescing (INV-7's merge half); persistence only on save |
| Misattributing a seam edit to the human (or vice versa) | version-keyed pending-edit registry; seam is the only machine ingress (INV-9); algebra unit-tested both directions |
claude-code provider/SDK surface shifts under us (SDK 0.0.x) |
provider use isolated in vscode-free LiveTurn module; seam keeps everything else LLM-free |
| Buffer edited mid-turn | stale-version application fails gracefully, reports, never partial-applies |
| Fingerprint mis-resolution on duplicated text | unchanged F2 ladder: context + lineHint, orphan over guess |
8. Traceability matrix
| Requirement (issue #6 acceptance) | Use case | Design | Slice |
|---|---|---|---|
| Human spans recorded/rendered live | PUC-1 | §6.2, §6.5, INV-7 | SLICE-2/3 |
| Machine edits recorded/rendered as Claude's | PUC-2 | seam + LiveTurn, INV-9 | SLICE-3/5 |
| Attribution survives edits (re-anchor / orphan) | PUC-4 | Anchorer ladder, INV-1/6 | SLICE-4 |
| Git-native persistence + reload | PUC-4 | §6.3, parent INV-2/4 | SLICE-1/4 |
| Mixed-edit split/merge | PUC-3 | INV-7, §6.5 | SLICE-2 |
| Unit + host E2E coverage | — | §6.8 | SLICE-2/6 |
9. Open Questions & Decisions log
- RESOLVED (this session): ingress = seam + minimal live turn; auth =
claude-codeprovider only (no extension-held credentials; Anthropic-key fallback deferred — not needed for testing since tests drive the seam); granularity = character-precise split + coalesce; record = span-list on shared anchors, orphanhood resolve-time; rendering = Claude tint + human gutter + toggle; live-turn UX = edit-selection command. - OPEN → F4: propose/accept drives the same seam; accepted diffs render into this attribution substrate. The seam's contract (single-range apply) may grow multi-range then.
- OPEN → F5: cross-rung persistence format may re-home
attributions[]alongside the rest of the sidecar. - OPEN → later: attribution of out-of-session edits (git merges, external editors) beyond the orphan rule; span-level → finer blame; Anthropic API-key config option for machines without Claude Code.
10. Glossary & References
- Attribution span — an anchored, author-attributed range of a document; the
unit of provenance. Seam —
applyAgentEdit, the single programmatic ingress for machine edits (INV-9). Unattributed — text with no span (it predates tracking). Live turn — one@cline/sdkinvocation producing a machine edit.claude-codeprovider — the SDK's built-in provider that rides the local Claude Code Pro/Max login. - Epic #1 · Feature #6 (F3) · F2 #4 (PR #5) · parent spec
coauthoring-inner-loop.md· POC #2 · rfc-app#48/#46 (lineage).