F10: interactive track-changes review in the markdown preview (#29) #30

Merged
benstull merged 17 commits from f10-interactive-review into main 2026-06-12 07:39:51 +00:00
Showing only changes of commit 923dcc09d9 - Show all commits
+6 -30
View File
@@ -2,9 +2,11 @@
* AttributionController — the thin editor-facing layer for F3 (spec §6.2). * AttributionController — the thin editor-facing layer for F3 (spec §6.2).
* Wires the pure AttributionTracker + PendingEditRegistry + Store/Anchorer to * Wires the pure AttributionTracker + PendingEditRegistry + Store/Anchorer to
* the editor: human typing → human spans, seam edits → agent spans (INV-9), * the editor: human typing → human spans, seam edits → agent spans (INV-9),
* decorations (Claude tint / human gutter border / toggle), save-time * save-time persistence, load/external-change resolve-or-orphan
* persistence, load/external-change resolve-or-orphan (INV-1/INV-6), and the * (INV-1/INV-6), and the orphan status-bar count. The editor carries no
* orphan status-bar count. Sidecar self-write suppression and the shared * in-editor attribution decorations — the rendered preview is the single
* review surface (F10/INV-32); spansFor() feeds it the live attribution.
* Sidecar self-write suppression and the shared
* FileSystemWatcher live in CoauthorStore / extension.ts (the sidecar is * FileSystemWatcher live in CoauthorStore / extension.ts (the sidecar is
* co-owned with ThreadController). * co-owned with ThreadController).
*/ */
@@ -44,23 +46,10 @@ interface DocAttribution {
hadAttributions: boolean; hadAttributions: boolean;
} }
const AGENT_DECO: vscode.DecorationRenderOptions = {
backgroundColor: "rgba(99, 102, 241, 0.18)",
overviewRulerColor: "rgba(99, 102, 241, 0.8)",
overviewRulerLane: vscode.OverviewRulerLane.Right,
};
const HUMAN_DECO: vscode.DecorationRenderOptions = {
borderColor: "rgba(16, 185, 129, 0.8)",
borderStyle: "solid",
borderWidth: "0 0 0 2px",
};
export class AttributionController implements vscode.Disposable { export class AttributionController implements vscode.Disposable {
private readonly disposables: vscode.Disposable[] = []; private readonly disposables: vscode.Disposable[] = [];
private readonly docs = new Map<string, DocAttribution>(); private readonly docs = new Map<string, DocAttribution>();
private readonly pending = new PendingEditRegistry(); private readonly pending = new PendingEditRegistry();
private readonly agentType = vscode.window.createTextEditorDecorationType(AGENT_DECO);
private readonly humanType = vscode.window.createTextEditorDecorationType(HUMAN_DECO);
private readonly statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90); private readonly statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
private readonly output = vscode.window.createOutputChannel("Cowriting Attribution"); private readonly output = vscode.window.createOutputChannel("Cowriting Attribution");
private visible = true; private visible = true;
@@ -79,7 +68,7 @@ export class AttributionController implements vscode.Disposable {
private readonly rootDir: string | undefined, private readonly rootDir: string | undefined,
private readonly guard: VersionGuard, private readonly guard: VersionGuard,
) { ) {
this.disposables.push(this.agentType, this.humanType, this.statusItem, this.output, this.applyEmitter); this.disposables.push(this.statusItem, this.output, this.applyEmitter);
this.disposables.push( this.disposables.push(
vscode.commands.registerCommand("cowriting.toggleAttribution", () => this.toggle()), vscode.commands.registerCommand("cowriting.toggleAttribution", () => this.toggle()),
vscode.workspace.onDidChangeTextDocument((e) => this.onDidChange(e)), vscode.workspace.onDidChangeTextDocument((e) => this.onDidChange(e)),
@@ -354,19 +343,6 @@ export class AttributionController implements vscode.Disposable {
private render(document: vscode.TextDocument): void { private render(document: vscode.TextDocument): void {
if (!this.isTracked(document)) return; if (!this.isTracked(document)) return;
const s = this.docs.get(this.keyOf(document)); const s = this.docs.get(this.keyOf(document));
const spans = this.visible && s ? s.spans : [];
const toRange = (sp: LiveSpan) =>
new vscode.Range(document.positionAt(sp.start), document.positionAt(sp.end));
const agentRanges = spans.filter((x) => x.author.kind === "agent").map(toRange);
const humanRanges = spans.filter((x) => x.author.kind === "human").map(toRange);
// Apply decorations to ALL visible split-editors showing this document, not
// just the first match — each editor pane has its own decoration layer.
for (const editor of vscode.window.visibleTextEditors) {
if (editor.document === document) {
editor.setDecorations(this.agentType, agentRanges);
editor.setDecorations(this.humanType, humanRanges);
}
}
if (document === vscode.window.activeTextEditor?.document) { if (document === vscode.window.activeTextEditor?.document) {
this.renderStatus(s); this.renderStatus(s);
} }