Files

20 KiB
Raw Permalink Blame History

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 the agent provenance 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-code provider ("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 agent provenance.

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-code provider); 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:

  • attributions model — the typed attributions[] shape on the artifact (replaces the unknown[] placeholder; schemaVersion stays 1 — additive).
  • AttributionTracker — pure span algebra over offset ranges: applyChange(spans, edit, author) → spans (shift/split/clip/coalesce).
  • Apply-edit seamapplyAgentEdit(doc, range, newText, provenance): applies a WorkspaceEdit and 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-code provider, building on the POC's vscode-free cline.ts driver) → 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 Provenanceattributions[] 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? }. Reuses anchorer.shift semantics 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 the claude-code provider, runs one SDK turn producing replacement text for the selection, calls the seam with agent provenance (model + sessionId from the turn).
  • AttributionRenderer: render(editor, spans) · setVisible(boolean).
  • Store/Anchorer: existing contracts, now also carrying attributions.

6.5 PerProduct-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 with turnId. 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 attribution resolve(fingerprint) → live span or orphaned (counted, not rendered). On save: refresh fingerprints from live spans (updatedAt bumped 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-code turn (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-code provider 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. SeamapplyAgentEdit, the single programmatic ingress for machine edits (INV-9). Unattributed — text with no span (it predates tracking). Live turn — one @cline/sdk invocation producing a machine edit. claude-code provider — 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).