feat!: sunset the review-panel webview — built-in preview + native diff are the review surfaces (D3/D17, spec §6.10); Keep/Reject lens copy (PUC-2)

Deletes the last bespoke UI surface (TrackChangesPreviewController + its
sealed webview client assets); every entry point re-points at native VS
Code chrome per spec §5: the #41 right-click entries become
cowriting.openReviewPreview (enter coediting if needed -> "Open Preview to
the Side"), Ctrl+Alt+R/Cmd+Alt+R moves to cowriting.reviewChanges (native
diff), and the F12 CodeLens per-proposal titles read "Keep"/"Reject" with
a top-of-file "Keep all (N)"/"Reject all" pair once >=2 proposals are
pending. EditFlow drops its own askClaude/askEditInstruction (the
webview's only caller) and its now-unused constructor params.

Coverage that lived only in the webview's test seams (renderHtmlFor,
receiveMessage, isOpen, ...) moves to direct calls against the surviving
controllers/pure renderReview (test/e2e/suite/helpers.ts gains a shared
renderHtmlFor probe); a genuine gap (pending-proposal + unchanged-block
rendering) is backfilled in test/previewAnnotations.test.ts. README's "how
it works" is rewritten as the native-surface map; the superseded F6/F7/F9/
F10/F11 sections are kept as a marked historical record rather than
deleted outright.

292 unit + 91 E2E green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-07-02 14:51:18 -07:00
parent 17fc01e8d8
commit 2170a0d282
23 changed files with 412 additions and 1475 deletions
+82 -37
View File
@@ -10,7 +10,7 @@ const settle = () => new Promise((r) => setTimeout(r, 400));
async function getApi(): Promise<CowritingApi> {
const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!;
const api = (await ext.activate()) as CowritingApi;
assert.ok(api?.trackChangesPreviewController, "exports preview controller");
assert.ok(api?.coeditingRegistry, "exports coeditingRegistry");
return api;
}
@@ -26,14 +26,19 @@ function pkg(): any {
return JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8"));
}
// #41 (story): "Open Cowriting Review Panel" reachable from the markdown
// file/tab right-click menu. The review surface (showTrackChangesPreview) is the
// plugin's central affordance; this adds explorer/context + editor/title/context
// entry points and makes the command open the *clicked* document — not merely
// the active editor — so the explorer right-click works even when the file is
// not already open.
suite("#41 review-panel right-click entry (host E2E — menu wiring + clicked-doc resolution)", () => {
test("command with a Uri previews the CLICKED doc, not the active editor", async () => {
// Task 8 (native-surfaces migration, spec §6.10): the review-webview's #41
// right-click entries ("Open Cowriting Review Panel") are replaced by
// `cowriting.openReviewPreview` — a minimal wrapper that resolves the clicked
// document (or falls back to the active editor, mirroring #41's original
// behavior), enters coediting if not already (so there is a baseline to review
// against), and hands off to VS Code's OWN "Open Preview to the Side"
// (`markdown.showPreviewToSide`) — the built-in preview IS the review surface
// now (Task 7's annotations render inside it). There is no controller-level
// `isOpen` seam anymore (no bespoke webview to introspect); these tests observe
// the command's OWN side effects instead — which document it focused/entered
// coediting on — the same signal #41's original suite checked via `isOpen`.
suite("Open Cowriting Review Preview (host E2E — menu wiring + clicked-doc resolution, Task 8)", () => {
test("command with a Uri targets the CLICKED doc, not the active editor", async () => {
// Two markdown docs; A is the active editor, B is the right-clicked target.
const aUri = writeFile("docs/menu-active.md", "# Active\n\nThe active editor's document.\n");
const bUri = writeFile("docs/menu-clicked.md", "# Clicked\n\nThe right-clicked document.\n");
@@ -43,19 +48,20 @@ suite("#41 review-panel right-click entry (host E2E — menu wiring + clicked-do
await settle();
const api = await getApi();
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview", bUri);
await vscode.commands.executeCommand("cowriting.openReviewPreview", bUri);
await settle();
assert.strictEqual(api.trackChangesPreviewController.isOpen(bUri.toString()), true, "preview opened for the clicked doc");
assert.strictEqual(
api.trackChangesPreviewController.isOpen(aUri.toString()),
false,
"the active editor's doc did NOT get a preview",
);
// `markdown.showPreviewToSide` itself ends with the preview WEBVIEW focused
// (not a text editor), so `activeTextEditor` is not a stable post-command
// signal — `isCoediting` (set by the command's OWN resolve-and-enter step,
// before it hands off to the preview) is: the clicked doc B entered, the
// still-open-but-not-clicked doc A did not.
assert.strictEqual(api.coeditingRegistry.isCoediting(bUri), true, "the clicked doc B entered coediting (a baseline to review against)");
assert.strictEqual(api.coeditingRegistry.isCoediting(aUri), false, "the active-but-not-clicked doc A did NOT enter coediting");
assert.ok(bDoc, "clicked doc handle held");
});
test("command with a Uri for a not-yet-open file opens it and previews (explorer case)", async () => {
test("command with a Uri for a not-yet-open file opens it and focuses it (explorer case)", async () => {
// Write a file but do not openTextDocument it — mimics an Explorer right-click.
const uri = writeFile("docs/menu-unopened.md", "# Unopened\n\nNever opened before the right-click.\n");
const key = uri.toString();
@@ -65,10 +71,14 @@ suite("#41 review-panel right-click entry (host E2E — menu wiring + clicked-do
"precondition: the file is not an open document",
);
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview", uri);
await vscode.commands.executeCommand("cowriting.openReviewPreview", uri);
await settle();
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "the clicked-but-unopened file got a preview");
assert.ok(
vscode.workspace.textDocuments.some((d) => d.uri.toString() === key),
"the clicked-but-unopened file was opened",
);
assert.strictEqual(api.coeditingRegistry.isCoediting(uri), true, "it entered coediting");
});
test("no-arg invocation still falls back to the active editor (palette / keybinding unchanged)", async () => {
@@ -78,39 +88,74 @@ suite("#41 review-panel right-click entry (host E2E — menu wiring + clicked-do
await settle();
const api = await getApi();
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
await vscode.commands.executeCommand("cowriting.openReviewPreview");
await settle();
assert.strictEqual(api.trackChangesPreviewController.isOpen(uri.toString()), true, "no-arg previews the active editor");
assert.strictEqual(api.coeditingRegistry.isCoediting(uri), true, "no-arg targets the active editor");
});
test("explorer/context contributes the review-panel item for markdown only", () => {
test("a non-markdown doc: command warns, no coediting entered", async () => {
const uri = writeFile("docs/menu-notmd.txt", "Not markdown.\n");
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);
await settle();
const api = await getApi();
assert.notStrictEqual(doc.languageId, "markdown", "fixture is not markdown");
await vscode.commands.executeCommand("cowriting.openReviewPreview");
await settle();
assert.strictEqual(api.coeditingRegistry.isCoediting(uri), false, "a non-markdown doc never enters coediting via this gateway");
});
test("explorer/context contributes the review-preview item for markdown only", () => {
const items = (pkg().contributes.menus["explorer/context"] ?? []) as Array<{ command: string; when?: string }>;
const entry = items.find((m) => m.command === "cowriting.showTrackChangesPreview");
assert.ok(entry, "explorer/context has a showTrackChangesPreview entry");
const entry = items.find((m) => m.command === "cowriting.openReviewPreview");
assert.ok(entry, "explorer/context has an openReviewPreview entry");
assert.match(entry!.when ?? "", /resourceLangId == markdown/, "gated on markdown resources");
});
test("editor/title/context (tab right-click) contributes the review-panel item for markdown only", () => {
test("editor/title/context (tab right-click) contributes the review-preview item for markdown only", () => {
const items = (pkg().contributes.menus["editor/title/context"] ?? []) as Array<{ command: string; when?: string }>;
const entry = items.find((m) => m.command === "cowriting.showTrackChangesPreview");
assert.ok(entry, "editor/title/context has a showTrackChangesPreview entry");
const entry = items.find((m) => m.command === "cowriting.openReviewPreview");
assert.ok(entry, "editor/title/context has an openReviewPreview entry");
assert.match(entry!.when ?? "", /resourceLangId == markdown/, "gated on markdown tabs");
});
test("the command title reads 'Open Cowriting Review Panel'", () => {
const cmd = (pkg().contributes.commands as Array<{ command: string; title: string }>).find(
(c) => c.command === "cowriting.showTrackChangesPreview",
);
assert.ok(cmd, "command is contributed");
assert.strictEqual(cmd!.title, "Open Cowriting Review Panel", "menus render this title");
test("editor/title (toolbar icon) contributes the review-preview item for markdown only", () => {
const items = (pkg().contributes.menus["editor/title"] ?? []) as Array<{ command: string; when?: string }>;
const entry = items.find((m) => m.command === "cowriting.openReviewPreview");
assert.ok(entry, "editor/title has an openReviewPreview entry");
assert.match(entry!.when ?? "", /editorLangId == markdown/, "gated on markdown");
});
test("the ctrl+alt+r keybinding still targets the command (unchanged)", () => {
const kb = (pkg().contributes.keybindings as Array<{ command: string; key: string; when?: string }>).find(
test("the command title reads 'Open Cowriting Review Preview'", () => {
const cmd = (pkg().contributes.commands as Array<{ command: string; title: string }>).find(
(c) => c.command === "cowriting.openReviewPreview",
);
assert.ok(cmd, "command is contributed");
assert.strictEqual(cmd!.title, "Open Cowriting Review Preview", "menus render this title");
});
test("the old cowriting.showTrackChangesPreview command/menus/keybinding are fully gone (Task 8 exit criterion)", () => {
const p = pkg();
const commands = p.contributes.commands as Array<{ command: string }>;
assert.ok(!commands.some((c) => c.command === "cowriting.showTrackChangesPreview"), "command contribution removed");
for (const menuId of ["commandPalette", "editor/title", "editor/title/context", "explorer/context"]) {
const items = (p.contributes.menus[menuId] ?? []) as Array<{ command: string }>;
assert.ok(!items.some((m) => m.command === "cowriting.showTrackChangesPreview"), `${menuId} no longer references it`);
}
const kb = (p.contributes.keybindings as Array<{ command: string }>).find(
(k) => k.command === "cowriting.showTrackChangesPreview",
);
assert.ok(kb, "keybinding still present");
assert.strictEqual(kb!.key, "ctrl+alt+r", "key unchanged");
assert.ok(!kb, "keybinding removed");
});
test("ctrl+alt+r now targets cowriting.reviewChanges (the native diff), not the retired preview command", () => {
const kb = (pkg().contributes.keybindings as Array<{ command: string; key: string; when?: string }>).find(
(k) => k.key === "ctrl+alt+r",
);
assert.ok(kb, "ctrl+alt+r keybinding still present");
assert.strictEqual(kb!.command, "cowriting.reviewChanges", "the chord now opens the native diff (Step 1 re-point)");
});
});