feat(f11): SLICE-5 — gateway, non-authorable disabling, docs (#43)
Closes the F11 feature: makes the toolbar surface reachable end to end and guards the edit controls. Per spec §6.4/§6.5/§7.2 SLICE-5. - package.json: cowriting.showTrackChangesPreview added to editor/title (when: editorLangId == markdown) — the minimal right-click → Open Review Preview gateway (#41/#42 expand it later). - trackChangesPreview: the gateway command accepts the tab's resource Uri (palette/keybinding still fall back to the active editor); refresh() sends an `authorable` flag on both render messages; `editControlsEnabled` test seam. - webview: disable Pin + Ask-Claude on a non-authorable doc (Annotations stays active — reading is always allowed); RenderMessage.authorable. - host E2E: the editor/title gateway opens the preview + is markdown-guarded; edit controls disabled on a non-authorable (read-only-scheme) markdown doc. - docs: docs/MANUAL-SMOKE-F11.md (live smoke, 10 steps) + README F11 section + intro line. 205 unit + 53 host E2E green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,9 @@ catalog (a pure, key-free SDK call) in a notification and the
|
|||||||
Features shipped so far: F2 region-anchored threads (Feature #4), F3 live
|
Features shipped so far: F2 region-anchored threads (Feature #4), F3 live
|
||||||
human/Claude attribution (Feature #6), F4 propose/accept diff flow
|
human/Claude attribution (Feature #6), F4 propose/accept diff flow
|
||||||
(Feature #12), F5 cross-rung sidecar contract (Feature #14), F6 diff-view
|
(Feature #12), F5 cross-rung sidecar contract (Feature #14), F6 diff-view
|
||||||
toggle (Feature #17), and F10 interactive review — **write left / review
|
toggle (Feature #17), F10 interactive review — **write left / review
|
||||||
right** (Feature #29).
|
right** (Feature #29), and F11 — the **preview toolbar as the primary
|
||||||
|
interaction surface** (Feature #43).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -262,6 +263,33 @@ they are no longer separate user surfaces.
|
|||||||
Design: `vscode-cowriting-plugin-content/specs/coauthoring-interactive-review.md`.
|
Design: `vscode-cowriting-plugin-content/specs/coauthoring-interactive-review.md`.
|
||||||
Live smoke: [`docs/MANUAL-SMOKE-F10.md`](docs/MANUAL-SMOKE-F10.md).
|
Live smoke: [`docs/MANUAL-SMOKE-F10.md`](docs/MANUAL-SMOKE-F10.md).
|
||||||
|
|
||||||
|
## F11 — Preview toolbar as the primary interaction surface (Feature #43)
|
||||||
|
|
||||||
|
The review preview's **header toolbar** becomes the cockpit for the inner loop.
|
||||||
|
Beside the existing **Annotations** switch it gains two controls:
|
||||||
|
|
||||||
|
- **⌖ Pin baseline** — pins the previewed document's review baseline to now and
|
||||||
|
clears the change-marks (homes the previously-orphaned `pinDiffBaseline`
|
||||||
|
command, which is reachable from the palette again too).
|
||||||
|
- **✦ Ask Claude…** — one **adaptive** button. Its label flips on the preview's
|
||||||
|
selection: **Edit Selection** when text is selected in the rendered preview,
|
||||||
|
**Edit Document** otherwise. Clicking it opens a host input box for the
|
||||||
|
instruction (the LLM turn and prompt stay host-side — the sealed webview gains
|
||||||
|
no LLM/credential surface), then surfaces the result as F4 proposals — **one**
|
||||||
|
for a selection (mapped back to its source block-union), or **one per changed
|
||||||
|
hunk** for a whole-document rewrite (`diffToHunks`), each independently ✓/✗-able.
|
||||||
|
|
||||||
|
The pure render layer emits `data-src-start`/`data-src-end` on every block in
|
||||||
|
**both** modes (INV-36); the webview's only mapping duty is walking a selection
|
||||||
|
to its nearest `data-src` ancestor. Right-clicking a markdown tab → **Open Review
|
||||||
|
Preview** is the minimal gateway making the surface reachable end to end (#41/#42
|
||||||
|
expand it). Edit controls are disabled on a non-authorable doc (reading stays
|
||||||
|
allowed). No new model, no new persistence — pin via the F6 store, edits via the
|
||||||
|
F4 propose/accept seam with F3 attribution (INV-35..37).
|
||||||
|
|
||||||
|
Design: [`docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md`](docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md).
|
||||||
|
Live smoke: [`docs/MANUAL-SMOKE-F11.md`](docs/MANUAL-SMOKE-F11.md).
|
||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
- `npm run watch` — rebuild on change.
|
- `npm run watch` — rebuild on change.
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Manual smoke — F11 preview toolbar as the primary interaction surface (#43)
|
||||||
|
|
||||||
|
F11 makes the rendered preview's **header toolbar** the primary interaction
|
||||||
|
surface: beside the existing **Annotations** switch it gains a **Pin baseline**
|
||||||
|
button and a **single adaptive Ask-Claude button** (Edit Selection when text is
|
||||||
|
selected in the preview, Edit Document otherwise). The webview's *visual*
|
||||||
|
behavior — the buttons, the live label flip, and the selection→source mapping —
|
||||||
|
is verified **here**, not in the automated host E2E (the webview is a sealed
|
||||||
|
sandbox; the host's message→seam wiring is covered by the F11 E2E suite). Run
|
||||||
|
once per change that touches F11. Live turns hit the SDK; use a trivial
|
||||||
|
instruction to keep them quick.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. `npm run build`
|
||||||
|
2. Launch the Extension Development Host (F5 in VS Code, or the Run panel) with
|
||||||
|
`sandbox/` open.
|
||||||
|
3. Open a markdown document with a few paragraphs (e.g. copy
|
||||||
|
`test/e2e/fixtures/workspace/docs/preview.md`).
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Right-click gateway (PUC-6).** Right-click the markdown editor **tab** (or
|
||||||
|
use the editor title bar) → **"Cowriting: Open Review Preview"**. The preview
|
||||||
|
opens beside the editor. (The palette entry and `Ctrl+Alt+R` still work too.)
|
||||||
|
2. **Three header controls (PUC-1).** The header row shows, left to right: the
|
||||||
|
**☑ Annotations** checkbox (on), a **⌖ Pin baseline** button, and a **✦ Ask
|
||||||
|
Claude to Edit Document** button. They restyle with the theme.
|
||||||
|
3. **Adaptive label — selection (PUC-2).** Select a paragraph **in the preview**
|
||||||
|
(drag across rendered text). The Ask-Claude button label flips live to **"✦
|
||||||
|
Ask Claude to Edit Selection"**. Click elsewhere to clear the selection → it
|
||||||
|
flips back to **"✦ Ask Claude to Edit Document"**.
|
||||||
|
4. **Edit Selection (PUC-3).** With a paragraph selected in the preview, click
|
||||||
|
**Ask Claude to Edit Selection** → a host input box appears → type an
|
||||||
|
instruction (e.g. "tighten this") → submit. A **blue `cw-proposal` block**
|
||||||
|
appears **inline at that paragraph** with **✓ / ✗** (the selection mapped back
|
||||||
|
to its source block; the edit ran host-side). The editor does **not** change
|
||||||
|
(INV-10). Accept with **✓**: the replacement lands; the block clears.
|
||||||
|
5. **Edit Document (PUC-4).** Clear the selection (button reads "Edit Document").
|
||||||
|
Click it → instruct (e.g. "fix any typos and tighten") → submit. Claude's
|
||||||
|
whole-document rewrite surfaces as **one or more** independent blue proposal
|
||||||
|
blocks (one per changed hunk), each independently **✓ / ✗**-able. Accept some,
|
||||||
|
reject others — each behaves as an ordinary F10 proposal.
|
||||||
|
6. **Pin baseline (PUC-5).** Make (or accept) a few changes so the preview shows
|
||||||
|
change-marks and a non-zero summary. Click **⌖ Pin baseline**. Expect: the
|
||||||
|
change-marks **clear**, the summary resets to `+0 −0`, and the header epoch
|
||||||
|
reads **`pinned <time>`**. "What changed" now counts from this moment.
|
||||||
|
7. **Off-state still maps (INV-36).** Toggle **Annotations** off (clean preview).
|
||||||
|
Selecting a paragraph still flips the button to **Edit Selection** and an edit
|
||||||
|
still works — the off/clean preview is also a selection→source surface.
|
||||||
|
8. **Non-authorable doc (PUC-1/7).** Open a markdown doc from a read-only/virtual
|
||||||
|
source (e.g. a Git diff view, or an Output channel rendered as markdown) and
|
||||||
|
open the preview. Expect: **Pin baseline** and **Ask-Claude** render
|
||||||
|
**disabled**; the **Annotations** toggle stays active (reading is allowed).
|
||||||
|
9. **Theme.** Toggle light / dark / high-contrast (`Ctrl+K Ctrl+T`). The two
|
||||||
|
toolbar buttons (enabled, hover, disabled states) restyle and stay legible.
|
||||||
|
10. **Cleanliness.** `git status` shows nothing written to the document, sidecar,
|
||||||
|
or repo by the toolbar gestures except the edits you explicitly accepted
|
||||||
|
(INV-20/35).
|
||||||
|
|
||||||
|
## Pass criteria
|
||||||
|
|
||||||
|
All ten steps behave as described; the adaptive label tracks the live selection;
|
||||||
|
the selection-scoped edit anchors at the right paragraph; the document edit
|
||||||
|
produces per-hunk proposals; pin clears the marks; edit controls are inert on a
|
||||||
|
non-authorable doc; no console errors in the webview devtools; nothing is
|
||||||
|
persisted by the toolbar beyond the edits you accepted.
|
||||||
@@ -18,6 +18,8 @@ interface RenderMessage {
|
|||||||
html: string;
|
html: string;
|
||||||
epoch?: string;
|
epoch?: string;
|
||||||
summary?: { added: number; removed: number; proposals: number };
|
summary?: { added: number; removed: number; proposals: number };
|
||||||
|
/** F11: false on a non-authorable doc → Pin + Ask-Claude controls disabled. */
|
||||||
|
authorable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vscodeApi = acquireVsCodeApi();
|
const vscodeApi = acquireVsCodeApi();
|
||||||
@@ -124,6 +126,10 @@ window.addEventListener("message", (event: MessageEvent<RenderMessage>) => {
|
|||||||
if (msg?.type !== "render") return;
|
if (msg?.type !== "render") return;
|
||||||
body.innerHTML = msg.html;
|
body.innerHTML = msg.html;
|
||||||
updateAskLabel(); // new content clears any selection → reset the adaptive label
|
updateAskLabel(); // new content clears any selection → reset the adaptive label
|
||||||
|
// F11 (PUC-1/7): disable edit controls on a non-authorable doc (reading stays on).
|
||||||
|
const authorable = msg.authorable !== false;
|
||||||
|
if (pinEl) pinEl.disabled = !authorable;
|
||||||
|
if (askEl) askEl.disabled = !authorable;
|
||||||
const on = msg.mode === "on";
|
const on = msg.mode === "on";
|
||||||
if (annotationsEl) annotationsEl.checked = on;
|
if (annotationsEl) annotationsEl.checked = on;
|
||||||
// Off-state is a clean preview: hide the review chrome.
|
// Off-state is a clean preview: hide the review chrome.
|
||||||
|
|||||||
@@ -112,6 +112,13 @@
|
|||||||
"when": "editorLangId == markdown"
|
"when": "editorLangId == markdown"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"editor/title": [
|
||||||
|
{
|
||||||
|
"command": "cowriting.showTrackChangesPreview",
|
||||||
|
"when": "editorLangId == markdown",
|
||||||
|
"group": "navigation@9"
|
||||||
|
}
|
||||||
|
],
|
||||||
"editor/context": [
|
"editor/context": [
|
||||||
{
|
{
|
||||||
"command": "cowriting.editSelection",
|
"command": "cowriting.editSelection",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { AttributionController } from "./attributionController";
|
|||||||
import type { ProposalController } from "./proposalController";
|
import type { ProposalController } from "./proposalController";
|
||||||
import { renderReview, renderPlain, diffBlocks, diffToHunks, type BlockOp } from "./trackChangesModel";
|
import { renderReview, renderPlain, diffBlocks, diffToHunks, type BlockOp } from "./trackChangesModel";
|
||||||
import { buildFingerprint } from "./anchorer";
|
import { buildFingerprint } from "./anchorer";
|
||||||
|
import { isAuthorable } from "./workspacePath";
|
||||||
import type { EditTurnResult } from "./liveTurn";
|
import type { EditTurnResult } from "./liveTurn";
|
||||||
|
|
||||||
/** F11: a host edit turn (selection/document text + instruction → rewrite). Injectable for tests. */
|
/** F11: a host edit turn (selection/document text + instruction → rewrite). Injectable for tests. */
|
||||||
@@ -63,9 +64,12 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
private readonly proposals: ProposalController,
|
private readonly proposals: ProposalController,
|
||||||
) {
|
) {
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
vscode.commands.registerCommand("cowriting.showTrackChangesPreview", () =>
|
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
|
||||||
this.show(vscode.window.activeTextEditor?.document),
|
// 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);
|
||||||
|
}),
|
||||||
// F11: document-scoped Ask-Claude (also reused by #42's gateway). Edits the
|
// 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.
|
// active markdown doc; the rewrite is diffed into per-hunk F4 proposals.
|
||||||
vscode.commands.registerCommand("cowriting.editDocument", () => {
|
vscode.commands.registerCommand("cowriting.editDocument", () => {
|
||||||
@@ -265,8 +269,10 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
const baselineText = baseline?.text ?? current; // no baseline → no change-marks
|
const baselineText = baseline?.text ?? current; // no baseline → no change-marks
|
||||||
const ops = diffBlocks(baselineText, current);
|
const ops = diffBlocks(baselineText, current);
|
||||||
this.lastModel.set(key, ops);
|
this.lastModel.set(key, ops);
|
||||||
|
// F11 (PUC-1/7): edit controls are inert on a non-authorable doc (reading stays allowed).
|
||||||
|
const authorable = isAuthorable(document.uri.scheme);
|
||||||
if (mode === "off") {
|
if (mode === "off") {
|
||||||
void panel.webview.postMessage({ type: "render", mode, html: renderPlain(current) });
|
void panel.webview.postMessage({ type: "render", mode, html: renderPlain(current), authorable });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const spans = this.attribution.spansFor(document);
|
const spans = this.attribution.spansFor(document);
|
||||||
@@ -282,6 +288,7 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
html: renderReview(baselineText, current, spans, proposals),
|
html: renderReview(baselineText, current, spans, proposals),
|
||||||
epoch: this.epochLabel(baseline),
|
epoch: this.epochLabel(baseline),
|
||||||
summary,
|
summary,
|
||||||
|
authorable,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,6 +388,15 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
setEditTurnForTest(fn: EditTurn): void {
|
setEditTurnForTest(fn: EditTurn): void {
|
||||||
this.editTurn = fn;
|
this.editTurn = fn;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* F11 (PUC-1/7): whether the previewed doc's edit controls (Pin + Ask-Claude)
|
||||||
|
* are enabled — true only for an authorable doc. The annotations toggle is
|
||||||
|
* always active (reading is always allowed). False if no panel/doc.
|
||||||
|
*/
|
||||||
|
editControlsEnabled(uriString: string): boolean {
|
||||||
|
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uriString);
|
||||||
|
return doc ? isAuthorable(doc.uri.scheme) : false;
|
||||||
|
}
|
||||||
getLastModel(uriString: string): BlockOp[] | undefined {
|
getLastModel(uriString: string): BlockOp[] | undefined {
|
||||||
return this.lastModel.get(uriString);
|
return this.lastModel.get(uriString);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,4 +155,52 @@ suite("F11 preview toolbar (host E2E — message → seam wiring, no LLM)", () =
|
|||||||
assert.ok(entry, "editDocument has a commandPalette entry");
|
assert.ok(entry, "editDocument has a commandPalette entry");
|
||||||
assert.match(entry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
assert.match(entry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SLICE-5: the minimal right-click gateway lives in editor/title (markdown only).
|
||||||
|
test("the editor/title gateway opens the preview, and the menu entry is markdown-guarded (PUC-6)", async () => {
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8"));
|
||||||
|
const entry = (pkg.contributes.menus["editor/title"] as Array<{ command: string; when?: string }>).find(
|
||||||
|
(m) => m.command === "cowriting.showTrackChangesPreview",
|
||||||
|
);
|
||||||
|
assert.ok(entry, "showTrackChangesPreview is in editor/title");
|
||||||
|
assert.match(entry!.when ?? "", /editorLangId == markdown/, "gateway guarded on markdown");
|
||||||
|
// the command it invokes opens the panel.
|
||||||
|
const { key } = await freshDoc("docs/f11gw.md", "# F11 gateway\n\nReachable end to end.\n");
|
||||||
|
const api = await getApi();
|
||||||
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
||||||
|
await settle();
|
||||||
|
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "gateway command opens the preview");
|
||||||
|
});
|
||||||
|
|
||||||
|
// SLICE-5: edit controls are inert on a non-authorable (read-only scheme) doc.
|
||||||
|
test("toolbar edit controls are disabled for a non-authorable document (PUC-1/7)", async () => {
|
||||||
|
const SCHEME = "cwf11ro";
|
||||||
|
const provider = new (class implements vscode.TextDocumentContentProvider {
|
||||||
|
onDidChange = undefined;
|
||||||
|
provideTextDocumentContent(): string {
|
||||||
|
return "# Read only\n\nThis markdown doc is not authorable.\n";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const reg = vscode.workspace.registerTextDocumentContentProvider(SCHEME, provider);
|
||||||
|
try {
|
||||||
|
const uri = vscode.Uri.parse(`${SCHEME}:/readonly.md`);
|
||||||
|
const doc = await vscode.workspace.openTextDocument(uri);
|
||||||
|
await vscode.languages.setTextDocumentLanguage(doc, "markdown");
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
await settle();
|
||||||
|
const api = await getApi();
|
||||||
|
const key = uri.toString();
|
||||||
|
assert.strictEqual(doc.languageId, "markdown", "fixture is markdown");
|
||||||
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
||||||
|
await settle();
|
||||||
|
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "preview opens (reading is always allowed)");
|
||||||
|
assert.strictEqual(
|
||||||
|
api.trackChangesPreviewController.editControlsEnabled(key),
|
||||||
|
false,
|
||||||
|
"Pin + Ask-Claude controls are disabled on a non-authorable doc",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
reg.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user