feat(#41): Open Cowriting Review Panel from markdown file/tab right-click

The review preview (`cowriting.showTrackChangesPreview`) was only reachable from
the command palette and `ctrl+alt+r` — neither where a writer's hand naturally
goes. Add the obvious right-click entry points to the plugin's central surface:

- `explorer/context` + `editor/title/context` menu items gated on
  `resourceLangId == markdown`, both invoking `showTrackChangesPreview`.
- The command now resolves the *clicked* document: it opens the passed Uri if
  it isn't already an open document (the Explorer case), instead of falling back
  to the active editor. No-arg invocation (palette / keybinding) is unchanged.
- Retitle the command to "Open Cowriting Review Panel" so the menus read the
  operator's wording (palette shows "Cowriting: Open Cowriting Review Panel").

E2E: clicked-doc resolution (open + not-yet-open + no-arg fallback), both menu
contributions present + markdown-gated, title, and keybinding unchanged.

Closes #41.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-12 16:04:02 -07:00
parent de53305a08
commit 695b51f903
4 changed files with 145 additions and 5 deletions
+4 -1
View File
@@ -245,7 +245,10 @@ A clean, **zero-annotation editor** on the left; the rendered preview on the
right as the **single interactive review surface**. The editor carries no
attribution tint, no in-editor proposal threads, and no diff — all review lives
in the preview, toggled by the **Annotations** switch in its header (on by
default). `Ctrl+Alt+R` opens **"Open Review Preview"**.
default). Open it via `Ctrl+Alt+R`, the editor title-bar button, or
**right-click a markdown file in the Explorer / its editor tab →
"Open Cowriting Review Panel"** (#41) — the right-click acts on the clicked
document, opening it first if needed.
In the on-state the preview shows **green = human / blue = Claude /
strikethrough = deleted**, and surfaces each of Claude's pending F4 proposals as
+15 -1
View File
@@ -76,7 +76,7 @@
},
{
"command": "cowriting.showTrackChangesPreview",
"title": "Cowriting: Open Review Preview",
"title": "Open Cowriting Review Panel",
"category": "Cowriting"
},
{
@@ -119,6 +119,20 @@
"group": "navigation@9"
}
],
"editor/title/context": [
{
"command": "cowriting.showTrackChangesPreview",
"when": "resourceLangId == markdown",
"group": "1_cowriting"
}
],
"explorer/context": [
{
"command": "cowriting.showTrackChangesPreview",
"when": "resourceLangId == markdown",
"group": "navigation@9"
}
],
"editor/context": [
{
"command": "cowriting.editSelection",
+10 -3
View File
@@ -71,9 +71,16 @@ export class TrackChangesPreviewController implements vscode.Disposable {
this.disposables.push(
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
// the palette / keybinding pass nothing → fall back to the active editor.
vscode.commands.registerCommand("cowriting.showTrackChangesPreview", (uri?: vscode.Uri) => {
const byUri = uri && vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri.toString());
this.show(byUri || vscode.window.activeTextEditor?.document);
// #41: the explorer/tab right-click also passes the clicked Uri, which may
// not be an open document yet — open it so we preview the clicked file, not
// whatever happens to be the active editor.
vscode.commands.registerCommand("cowriting.showTrackChangesPreview", async (uri?: vscode.Uri) => {
if (uri) {
const open = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri.toString());
this.show(open ?? (await vscode.workspace.openTextDocument(uri)));
return;
}
this.show(vscode.window.activeTextEditor?.document);
}),
// F11: document-scoped Ask-Claude (also reused by #42's gateway). Edits the
// active markdown doc; the rewrite is diffed into per-hunk F4 proposals.
+116
View File
@@ -0,0 +1,116 @@
import * as assert from "assert";
import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
import type { CowritingApi } from "../../../src/extension";
const WS = process.env.E2E_WORKSPACE!;
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");
return api;
}
/** Write a markdown file under WS and return its uri (without opening it). */
function writeFile(rel: string, body: string): vscode.Uri {
const abs = path.join(WS, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, body, "utf8");
return vscode.Uri.file(abs);
}
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 () => {
// 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");
const aDoc = await vscode.workspace.openTextDocument(aUri);
await vscode.window.showTextDocument(aDoc);
const bDoc = await vscode.workspace.openTextDocument(bUri); // open but NOT active
await settle();
const api = await getApi();
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview", 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",
);
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 () => {
// 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();
const api = await getApi();
assert.ok(
!vscode.workspace.textDocuments.some((d) => d.uri.toString() === key),
"precondition: the file is not an open document",
);
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview", uri);
await settle();
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "the clicked-but-unopened file got a preview");
});
test("no-arg invocation still falls back to the active editor (palette / keybinding unchanged)", async () => {
const uri = writeFile("docs/menu-noarg.md", "# No-arg\n\nActive editor for the no-arg path.\n");
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);
await settle();
const api = await getApi();
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
await settle();
assert.strictEqual(api.trackChangesPreviewController.isOpen(uri.toString()), true, "no-arg previews the active editor");
});
test("explorer/context contributes the review-panel 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");
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", () => {
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");
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("the ctrl+alt+r keybinding still targets the command (unchanged)", () => {
const kb = (pkg().contributes.keybindings as Array<{ command: string; key: string; when?: string }>).find(
(k) => k.command === "cowriting.showTrackChangesPreview",
);
assert.ok(kb, "keybinding still present");
assert.strictEqual(kb!.key, "ctrl+alt+r", "key unchanged");
});
});