feat(f9): preview gains authorship mode + attribution dep + setMode wiring

Per-panel mode (default changes); refresh branches to renderAuthorship reading
AttributionController.spansFor; webview setMode message; extension.ts reorders the
F7 controller after attribution. getMode/setMode test seams.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 14:38:05 -07:00
parent a84d72e69b
commit 399e3c5f70
2 changed files with 59 additions and 16 deletions
+48 -6
View File
@@ -11,7 +11,8 @@ import * as path from "node:path";
import { randomBytes } from "node:crypto";
import * as vscode from "vscode";
import type { DiffViewController } from "./diffViewController";
import { renderTrackChanges, diffBlocks, type BlockOp } from "./trackChangesModel";
import type { AttributionController } from "./attributionController";
import { renderTrackChanges, renderAuthorship, diffBlocks, type BlockOp } from "./trackChangesModel";
const VIEW_TYPE = "cowriting.trackChangesPreview";
const DEBOUNCE_MS = 150;
@@ -21,10 +22,13 @@ export class TrackChangesPreviewController implements vscode.Disposable {
private readonly panels = new Map<string, vscode.WebviewPanel>();
private readonly lastModel = new Map<string, BlockOp[]>();
private readonly debounces = new Map<string, NodeJS.Timeout>();
/** F9: per-panel view mode — track-changes (default) or authorship. */
private readonly mode = new Map<string, "changes" | "authorship">();
constructor(
private readonly diffView: DiffViewController,
private readonly extensionUri: vscode.Uri,
private readonly attribution: AttributionController,
) {
this.disposables.push(
vscode.commands.registerCommand("cowriting.showTrackChangesPreview", () =>
@@ -70,6 +74,18 @@ export class TrackChangesPreviewController implements vscode.Disposable {
() => {
this.panels.delete(key);
this.lastModel.delete(key);
this.mode.delete(key);
},
null,
this.disposables,
);
// F9: the webview's header toggle posts the chosen mode back.
panel.webview.onDidReceiveMessage(
(m: { type?: string; mode?: "changes" | "authorship" }) => {
if (m?.type === "setMode" && (m.mode === "changes" || m.mode === "authorship")) {
this.mode.set(key, m.mode);
this.refresh(document);
}
},
null,
this.disposables,
@@ -97,14 +113,30 @@ export class TrackChangesPreviewController implements vscode.Disposable {
if (doc) this.refresh(doc);
}
/** Recompute the model + post HTML to the panel (no-op if no panel). */
/** Recompute the model + post HTML to the panel for its current mode (no-op if no panel). */
refresh(document: vscode.TextDocument): void {
const key = document.uri.toString();
const panel = this.panels.get(key);
if (!panel) return;
const baseline = this.diffView.getBaseline(key);
const baselineText = baseline?.text ?? document.getText(); // no baseline → no marks
const mode = this.mode.get(key) ?? "changes";
const current = document.getText();
if (mode === "authorship") {
// F9: render the current buffer colored by F3 author, baseline-independent.
const spans = this.attribution.spansFor(document);
this.lastModel.set(key, diffBlocks(this.diffView.getBaseline(key)?.text ?? current, current));
void panel.webview.postMessage({
type: "render",
mode,
html: renderAuthorship(current, spans),
legend: {
claude: spans.some((s) => s.author === "claude"),
human: spans.some((s) => s.author === "human"),
},
});
return;
}
const baseline = this.diffView.getBaseline(key);
const baselineText = baseline?.text ?? current; // no baseline → no marks
const ops = diffBlocks(baselineText, current);
this.lastModel.set(key, ops);
const summary = {
@@ -112,11 +144,11 @@ export class TrackChangesPreviewController implements vscode.Disposable {
removed: ops.filter((o) => o.kind === "removed").length,
changed: ops.filter((o) => o.kind === "changed").length,
};
const epoch = this.epochLabel(baseline);
void panel.webview.postMessage({
type: "render",
mode,
html: renderTrackChanges(baselineText, current),
epoch,
epoch: this.epochLabel(baseline),
summary,
});
}
@@ -177,6 +209,16 @@ export class TrackChangesPreviewController implements vscode.Disposable {
getLastModel(uriString: string): BlockOp[] | undefined {
return this.lastModel.get(uriString);
}
/** F9: current view mode for a panel (default track-changes). */
getMode(uriString: string): "changes" | "authorship" {
return this.mode.get(uriString) ?? "changes";
}
/** F9: set the view mode and re-render (the programmatic twin of the header toggle). */
setMode(uriString: string, mode: "changes" | "authorship"): void {
this.mode.set(uriString, mode);
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uriString);
if (doc) this.refresh(doc);
}
dispose(): void {
for (const t of this.debounces.values()) clearTimeout(t);