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:
+11
-10
@@ -70,16 +70,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
// still works — PUC-5.
|
||||
const globalSidecarStore = new GlobalSidecarStore(baselineStorageDir ?? "");
|
||||
|
||||
// --- F7: rendered track-changes preview (Feature #21) — workspace-INDEPENDENT ---
|
||||
// Like F6, F7 works on any markdown doc (incl. untitled / out-of-folder), so it
|
||||
// is constructed regardless of an open folder and its command is always live.
|
||||
// It reuses the F6 baseline (INV-20) and adds no persistence.
|
||||
const trackChangesPreviewController = new TrackChangesPreviewController(
|
||||
diffViewController,
|
||||
context.extensionUri,
|
||||
);
|
||||
context.subscriptions.push(trackChangesPreviewController);
|
||||
|
||||
// --- F8: authoring on ANY document (in-folder, out-of-folder, untitled) ---
|
||||
// The router routes per-document: in-workspace file: → the committable repo
|
||||
// `.threads/` sidecar (CoauthorStore, INV-2); out-of-folder file: + untitled:
|
||||
@@ -99,6 +89,17 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
const attributionController = new AttributionController(sidecarRouter, root, versionGuard);
|
||||
context.subscriptions.push(attributionController);
|
||||
|
||||
// --- F7: rendered track-changes preview (Feature #21) + F9 authorship mode ---
|
||||
// Workspace-INDEPENDENT (works on any markdown doc, reuses the F6 baseline,
|
||||
// INV-20). Constructed AFTER attribution so F9's authorship view can read F3
|
||||
// spans (AttributionController.spansFor).
|
||||
const trackChangesPreviewController = new TrackChangesPreviewController(
|
||||
diffViewController,
|
||||
context.extensionUri,
|
||||
attributionController,
|
||||
);
|
||||
context.subscriptions.push(trackChangesPreviewController);
|
||||
|
||||
// --- F4: propose/accept (Feature #12) ---
|
||||
const proposalController = new ProposalController(sidecarRouter, attributionController, root, versionGuard);
|
||||
context.subscriptions.push(proposalController);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user