feat(f11): SLICE-1 — Pin baseline toolbar button + reachability (#43)

Homes the orphaned cowriting.pinDiffBaseline command and gives the writer a
reachable Pin control in the preview toolbar. Per spec §7.2 SLICE-1
(docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md).

- trackChangesPreview: extract onDidReceiveMessage into handleWebviewMessage;
  add the F11 `pinBaseline` intent → DiffViewController.pin(previewedDoc) (the
  bound doc, not activeTextEditor — §6.7); ToolbarMsg union; receiveMessage test
  seam exercising the real message→seam wiring (INV-35).
- webview: ⌖ Pin baseline button in #cw-header posting { type: "pinBaseline" };
  theme-aware toolbar-button CSS (light/dark/high-contrast, disabled state).
- package.json: unhide pinDiffBaseline — commandPalette `when` false →
  editorLangId == markdown (resolves the #34 orphan from the command side).
- host E2E (test/e2e/suite/f11Toolbar.test.ts): pinBaseline message clears the
  change-marks + advances the baseline to `pinned`; palette `when` is reachable.

Also archives the F11 implementation plan to docs/superpowers/plans/.

197 unit + 47 host E2E green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-12 13:40:21 -07:00
parent 5966907089
commit 8b9e61a1da
6 changed files with 260 additions and 15 deletions
+48 -14
View File
@@ -18,6 +18,17 @@ import { renderReview, renderPlain, diffBlocks, type BlockOp } from "./trackChan
const VIEW_TYPE = "cowriting.trackChangesPreview";
const DEBOUNCE_MS = 150;
/**
* Inbound webview→host messages (intent only — the sealed webview never mutates,
* INV-21/35). F10 carried the annotations toggle + ✓/✗ proposal decisions; F11
* adds the toolbar intents (pin baseline / ask Claude).
*/
type ToolbarMsg =
| { type: "setMode"; mode: "on" | "off" }
| { type: "accept"; proposalId: string }
| { type: "reject"; proposalId: string }
| { type: "pinBaseline" };
export class TrackChangesPreviewController implements vscode.Disposable {
private readonly disposables: vscode.Disposable[] = [];
private readonly panels = new Map<string, vscode.WebviewPanel>();
@@ -91,21 +102,10 @@ export class TrackChangesPreviewController implements vscode.Disposable {
null,
this.disposables,
);
// F10: the webview posts the annotations toggle + ✓/✗ proposal decisions back.
// F10/F11: the webview posts the annotations toggle, ✓/✗ proposal decisions,
// and (F11) the toolbar intents (pin baseline / ask Claude) back to the host.
panel.webview.onDidReceiveMessage(
(m: { type?: string; mode?: "on" | "off"; proposalId?: string }) => {
if (m?.type === "setMode" && (m.mode === "on" || m.mode === "off")) {
this.mode.set(key, m.mode);
this.refresh(document);
} else if (m?.type === "accept" && m.proposalId) {
void this.proposals
.acceptById(this.proposals.keyFor(document), m.proposalId)
.then(() => this.refresh(document));
} else if (m?.type === "reject" && m.proposalId) {
this.proposals.rejectById(this.proposals.keyFor(document), m.proposalId);
this.refresh(document);
}
},
(m: ToolbarMsg) => this.handleWebviewMessage(document, m),
null,
this.disposables,
);
@@ -115,6 +115,30 @@ export class TrackChangesPreviewController implements vscode.Disposable {
this.refresh(document);
}
/**
* Route an inbound webview intent through the existing seams (INV-35): the
* annotations toggle + ✓/✗ proposal decisions (F10) and the toolbar gestures
* (F11). Pin targets the PREVIEWED document (`DiffViewController.pin`), not the
* active editor — the preview knows its bound doc (§6.7).
*/
private handleWebviewMessage(document: vscode.TextDocument, m: ToolbarMsg): void {
const key = document.uri.toString();
if (m?.type === "setMode" && (m.mode === "on" || m.mode === "off")) {
this.mode.set(key, m.mode);
this.refresh(document);
} else if (m?.type === "accept" && m.proposalId) {
void this.proposals
.acceptById(this.proposals.keyFor(document), m.proposalId)
.then(() => this.refresh(document));
} else if (m?.type === "reject" && m.proposalId) {
this.proposals.rejectById(this.proposals.keyFor(document), m.proposalId);
this.refresh(document);
} else if (m?.type === "pinBaseline") {
// F6 baseline store re-render arrives via the onDidChangeBaseline subscription.
this.diffView.pin(document);
}
}
private onEdit(document: vscode.TextDocument): void {
const key = document.uri.toString();
if (!this.panels.has(key)) return;
@@ -232,6 +256,7 @@ export class TrackChangesPreviewController implements vscode.Disposable {
<body>
<div id="cw-header">
<label id="cw-toggle"><input type="checkbox" id="cw-annotations" checked /> Annotations</label>
<button id="cw-pin" type="button" title="Pin the review baseline to now (clears the change-marks)">⌖ Pin baseline</button>
<span id="cw-epoch">Review</span>
<span id="cw-summary"></span>
<span id="cw-legend"></span>
@@ -246,6 +271,15 @@ export class TrackChangesPreviewController implements vscode.Disposable {
isOpen(uriString: string): boolean {
return this.panels.has(uriString);
}
/**
* F11 test seam: deliver an inbound webview message to the real routing, as if
* the sealed webview had posted it. Exercises message→seam wiring without a
* live webview DOM (which is manual-smoke only). No-op if no doc/panel.
*/
receiveMessage(uriString: string, m: ToolbarMsg): void {
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uriString);
if (doc && this.panels.has(uriString)) this.handleWebviewMessage(doc, m);
}
getLastModel(uriString: string): BlockOp[] | undefined {
return this.lastModel.get(uriString);
}