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:
@@ -0,0 +1,123 @@
|
|||||||
|
# Implementation Plan: F11 — Preview Toolbar as the Primary Interaction Surface (#43)
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-06-12-f11-preview-toolbar-interaction-surface.md`
|
||||||
|
**Anchor:** Feature `benstull/vscode-cowriting-plugin#43` (F11, `type/feature`, `priority/P1`)
|
||||||
|
**Session:** vscode-cowriting-plugin-0037
|
||||||
|
|
||||||
|
This plan transcribes the spec's §7.2 slicing plan into concrete, file-level
|
||||||
|
tasks. Each slice is independently green (unit + host E2E) before the next. Host
|
||||||
|
E2E is this app's required tier (no browser/deploy stage — a VS Code extension);
|
||||||
|
no LLM in CI (edit turns stubbed). The webview's visual rendering, the adaptive
|
||||||
|
label, and the selection→source DOM lookup are manual-smoke only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SLICE-1 — Pin baseline button + reachability *(the immediate win)*
|
||||||
|
|
||||||
|
Homes the orphaned `cowriting.pinDiffBaseline` command and gives the writer a
|
||||||
|
reachable Pin control in the preview toolbar.
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
|
||||||
|
1. **Host message routing** (`src/trackChangesPreview.ts`): extract the inline
|
||||||
|
`onDidReceiveMessage` body into a private `handleWebviewMessage(document, m)`
|
||||||
|
method; add a `pinBaseline` branch that calls `this.diffView.pin(document)`
|
||||||
|
(the *previewed* document — not `activeTextEditor`). The existing
|
||||||
|
`onDidChangeBaseline` subscription already re-renders with cleared marks.
|
||||||
|
2. **Test seam**: add `receiveMessage(uriString, m)` that resolves the doc and
|
||||||
|
calls `handleWebviewMessage`, so host E2E can simulate the raw webview message
|
||||||
|
and exercise the real routing.
|
||||||
|
3. **Webview** (`media/preview.ts` + `.css`): add a `⌖ Pin baseline` button in
|
||||||
|
`#cw-header`; click → `postMessage({ type: "pinBaseline" })`; theme-aware CSS.
|
||||||
|
4. **Shell HTML** (`shellHtml`): add the `<button id="cw-pin">` to the header row.
|
||||||
|
5. **Reachability** (`package.json`): flip `cowriting.pinDiffBaseline`'s
|
||||||
|
`commandPalette` `when` from `false` to `editorLangId == markdown`.
|
||||||
|
|
||||||
|
**Host E2E** (`test/e2e/suite/trackChangesPreview.test.ts` or new f11 suite):
|
||||||
|
open a markdown fixture with a divergent baseline → some block marked; simulate
|
||||||
|
`{type:"pinBaseline"}` via `receiveMessage` → `getLastModel` shows every block
|
||||||
|
`unchanged` (marks cleared) and `getBaseline(key).reason === "pinned"`.
|
||||||
|
|
||||||
|
**Unit:** none new (pure layer untouched this slice).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SLICE-2 — Block-offset emission *(INV-36 data layer)*
|
||||||
|
|
||||||
|
Shared pure helper wrapping each rendered block with `data-src-start`/`-end`
|
||||||
|
(source char offsets from `BlockWithRange`) in **both** `renderReview` and
|
||||||
|
`renderPlain`. vscode-free, DOM-free, deterministic (extends INV-22/33). No UI.
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
|
||||||
|
1. `src/trackChangesModel.ts`: a shared internal helper that prepends
|
||||||
|
`data-src-start="N" data-src-end="M"` to each block's wrapping element, routed
|
||||||
|
from both render paths using the existing `splitBlocksWithRanges` offsets.
|
||||||
|
2. Skip blocks with no live-source range (deletion-only / proposal blocks).
|
||||||
|
|
||||||
|
**Unit** (`test/trackChangesModel.test.ts`): `data-src-start/end` present and
|
||||||
|
correct on every block for both modes (offsets equal `BlockWithRange` ranges);
|
||||||
|
determinism (same inputs → identical HTML).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SLICE-3 — Edit Document button + hunk path *(INV-37 document half)*
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
|
||||||
|
1. `src/trackChangesModel.ts`: pure `diffToHunks(currentText, rewrittenText):
|
||||||
|
Array<{ start; end; replacement }>` — vscode-free, deterministic.
|
||||||
|
2. `src/trackChangesPreview.ts`: `runEditAndPropose(document, target, instruction)`
|
||||||
|
private routine; `askClaude`/`document` branch → host `showInputBox` →
|
||||||
|
`runEditTurn` over full text → `diffToHunks` → one F4 `propose()` per hunk.
|
||||||
|
3. `package.json`: register `cowriting.editDocument` (document-scoped), routed
|
||||||
|
through `runEditAndPropose({kind:"document"})`; for `#42` reuse.
|
||||||
|
4. Webview: `✦ Ask Claude to Edit Document` button (no-selection state) →
|
||||||
|
`postMessage({ type: "askClaude", scope: "document" })`.
|
||||||
|
|
||||||
|
**Unit:** `diffToHunks` over fixtures (single hunk → one range; multi-hunk →
|
||||||
|
disjoint ranges + correct replacements; unchanged → zero hunks; whole-doc
|
||||||
|
replacement → one full-range hunk).
|
||||||
|
|
||||||
|
**Host E2E:** simulate `{askClaude, scope:"document"}` with a stubbed multi-hunk
|
||||||
|
rewrite → **N** proposals matching the hunks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SLICE-4 — Adaptive Edit Selection *(INV-37 selection half; INV-36 consumer)*
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
|
||||||
|
1. Webview: `selectionchange` listener flips the Ask-Claude label (Edit Selection
|
||||||
|
⇆ Edit Document); selection→nearest-`data-src` ancestor resolution →
|
||||||
|
`postMessage({ type:"askClaude", scope:"selection", start, end })`.
|
||||||
|
2. Host: `askClaude`/`selection` branch → `runEditAndPropose({kind:"range",
|
||||||
|
start, end})` → one `runEditTurn` → one F4 `propose()` over the block-union.
|
||||||
|
3. Edge: selection resolving to no live block → fall back to document scope.
|
||||||
|
|
||||||
|
**Host E2E:** simulate `{askClaude, scope:"selection", start, end}` with a stubbed
|
||||||
|
edit turn → exactly **one** proposal over the resolved range, anchored inline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SLICE-5 — Gateway, edges, tests & docs
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
|
||||||
|
1. `package.json`: add `cowriting.showTrackChangesPreview` to `editor/title` with
|
||||||
|
`when: editorLangId == markdown` (minimal right-click gateway).
|
||||||
|
2. Non-authorable disabling: Pin + Ask-Claude controls render disabled when
|
||||||
|
`!isAuthorable(document)`; annotations toggle stays active.
|
||||||
|
3. Host E2E: gateway command opens the panel; controls inert on non-authorable.
|
||||||
|
4. `docs/MANUAL-SMOKE-F11.md` (live smoke script per spec §6.8).
|
||||||
|
5. README F11 section.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Done = #43 acceptance (spec §7.3)
|
||||||
|
|
||||||
|
Preview toolbar hosts annotations checkbox + Pin baseline + single adaptive
|
||||||
|
Ask-Claude (Edit Selection ⇆ Edit Document) routing through existing F4/F3/F6;
|
||||||
|
edits surface as proposals (one for a selection, per-hunk for a document
|
||||||
|
rewrite); a right-click entry opens the preview; the pin command is no longer
|
||||||
|
orphaned; unit + host E2E green; live smoke performed once.
|
||||||
@@ -59,6 +59,19 @@ pre.mermaid[data-cw-error] { color: var(--vscode-errorForeground); }
|
|||||||
#cw-legend .cw-swatch { padding: 0 0.4em; border-radius: 3px; }
|
#cw-legend .cw-swatch { padding: 0 0.4em; border-radius: 3px; }
|
||||||
#cw-summary .cw-prop { opacity: 0.85; }
|
#cw-summary .cw-prop { opacity: 0.85; }
|
||||||
|
|
||||||
|
/* F11 — preview toolbar buttons (Pin baseline; adaptive Ask Claude). Theme-aware. */
|
||||||
|
#cw-header button {
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
border: 1px solid var(--vscode-button-border, transparent);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.1em 0.55em;
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
}
|
||||||
|
#cw-header button:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground); }
|
||||||
|
#cw-header button:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
|
||||||
/* F10 interactive review — annotations toggle + ✓/✗ proposal blocks. */
|
/* F10 interactive review — annotations toggle + ✓/✗ proposal blocks. */
|
||||||
#cw-toggle { display: inline-flex; align-items: center; gap: 0.35em; cursor: pointer; }
|
#cw-toggle { display: inline-flex; align-items: center; gap: 0.35em; cursor: pointer; }
|
||||||
.cw-proposal {
|
.cw-proposal {
|
||||||
|
|||||||
@@ -26,12 +26,18 @@ const header = document.getElementById("cw-epoch")!;
|
|||||||
const summary = document.getElementById("cw-summary")!;
|
const summary = document.getElementById("cw-summary")!;
|
||||||
const legend = document.getElementById("cw-legend")!;
|
const legend = document.getElementById("cw-legend")!;
|
||||||
const annotationsEl = document.getElementById("cw-annotations") as HTMLInputElement | null;
|
const annotationsEl = document.getElementById("cw-annotations") as HTMLInputElement | null;
|
||||||
|
const pinEl = document.getElementById("cw-pin") as HTMLButtonElement | null;
|
||||||
|
|
||||||
// F10: the annotations on/off toggle.
|
// F10: the annotations on/off toggle.
|
||||||
annotationsEl?.addEventListener("change", () => {
|
annotationsEl?.addEventListener("change", () => {
|
||||||
vscodeApi.postMessage({ type: "setMode", mode: annotationsEl.checked ? "on" : "off" });
|
vscodeApi.postMessage({ type: "setMode", mode: annotationsEl.checked ? "on" : "off" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// F11 (SLICE-1): Pin baseline — post intent; the host pins via the F6 store (INV-35).
|
||||||
|
pinEl?.addEventListener("click", () => {
|
||||||
|
vscodeApi.postMessage({ type: "pinBaseline" });
|
||||||
|
});
|
||||||
|
|
||||||
// F10: delegated ✓/✗ accept/reject of pending proposals (routed back to the F4 seam).
|
// F10: delegated ✓/✗ accept/reject of pending proposals (routed back to the F4 seam).
|
||||||
body.addEventListener("click", (e) => {
|
body.addEventListener("click", (e) => {
|
||||||
const btn = (e.target as HTMLElement)?.closest<HTMLElement>(".cw-actions button");
|
const btn = (e.target as HTMLElement)?.closest<HTMLElement>(".cw-actions button");
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cowriting.pinDiffBaseline",
|
"command": "cowriting.pinDiffBaseline",
|
||||||
"when": "false"
|
"when": "editorLangId == markdown"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor/context": [
|
"editor/context": [
|
||||||
|
|||||||
+44
-10
@@ -18,6 +18,17 @@ import { renderReview, renderPlain, diffBlocks, type BlockOp } from "./trackChan
|
|||||||
const VIEW_TYPE = "cowriting.trackChangesPreview";
|
const VIEW_TYPE = "cowriting.trackChangesPreview";
|
||||||
const DEBOUNCE_MS = 150;
|
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 {
|
export class TrackChangesPreviewController implements vscode.Disposable {
|
||||||
private readonly disposables: vscode.Disposable[] = [];
|
private readonly disposables: vscode.Disposable[] = [];
|
||||||
private readonly panels = new Map<string, vscode.WebviewPanel>();
|
private readonly panels = new Map<string, vscode.WebviewPanel>();
|
||||||
@@ -91,9 +102,27 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
null,
|
null,
|
||||||
this.disposables,
|
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(
|
panel.webview.onDidReceiveMessage(
|
||||||
(m: { type?: string; mode?: "on" | "off"; proposalId?: string }) => {
|
(m: ToolbarMsg) => this.handleWebviewMessage(document, m),
|
||||||
|
null,
|
||||||
|
this.disposables,
|
||||||
|
);
|
||||||
|
this.panels.set(key, panel);
|
||||||
|
// A panel is now open for this doc — the off-panel indicator is redundant.
|
||||||
|
this.hideStatus();
|
||||||
|
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")) {
|
if (m?.type === "setMode" && (m.mode === "on" || m.mode === "off")) {
|
||||||
this.mode.set(key, m.mode);
|
this.mode.set(key, m.mode);
|
||||||
this.refresh(document);
|
this.refresh(document);
|
||||||
@@ -104,15 +133,10 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
} else if (m?.type === "reject" && m.proposalId) {
|
} else if (m?.type === "reject" && m.proposalId) {
|
||||||
this.proposals.rejectById(this.proposals.keyFor(document), m.proposalId);
|
this.proposals.rejectById(this.proposals.keyFor(document), m.proposalId);
|
||||||
this.refresh(document);
|
this.refresh(document);
|
||||||
|
} else if (m?.type === "pinBaseline") {
|
||||||
|
// F6 baseline store re-render arrives via the onDidChangeBaseline subscription.
|
||||||
|
this.diffView.pin(document);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
null,
|
|
||||||
this.disposables,
|
|
||||||
);
|
|
||||||
this.panels.set(key, panel);
|
|
||||||
// A panel is now open for this doc — the off-panel indicator is redundant.
|
|
||||||
this.hideStatus();
|
|
||||||
this.refresh(document);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEdit(document: vscode.TextDocument): void {
|
private onEdit(document: vscode.TextDocument): void {
|
||||||
@@ -232,6 +256,7 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
<body>
|
<body>
|
||||||
<div id="cw-header">
|
<div id="cw-header">
|
||||||
<label id="cw-toggle"><input type="checkbox" id="cw-annotations" checked /> Annotations</label>
|
<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-epoch">Review</span>
|
||||||
<span id="cw-summary"></span>
|
<span id="cw-summary"></span>
|
||||||
<span id="cw-legend"></span>
|
<span id="cw-legend"></span>
|
||||||
@@ -246,6 +271,15 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
isOpen(uriString: string): boolean {
|
isOpen(uriString: string): boolean {
|
||||||
return this.panels.has(uriString);
|
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 {
|
getLastModel(uriString: string): BlockOp[] | undefined {
|
||||||
return this.lastModel.get(uriString);
|
return this.lastModel.get(uriString);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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 && api?.diffViewController, "exports preview + diffView");
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create + open a fresh markdown doc under WS, returning the doc + its uri key. */
|
||||||
|
async function freshDoc(rel: string, body: string): Promise<{ doc: vscode.TextDocument; key: string }> {
|
||||||
|
const abs = path.join(WS, rel);
|
||||||
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||||
|
fs.writeFileSync(abs, body, "utf8");
|
||||||
|
const uri = vscode.Uri.file(abs);
|
||||||
|
const doc = await vscode.workspace.openTextDocument(uri);
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
await settle();
|
||||||
|
return { doc, key: uri.toString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// F11 host E2E (no LLM): the preview toolbar is the primary interaction surface.
|
||||||
|
// The webview posts intent messages; the host routes them through the existing
|
||||||
|
// F4/F6/F3 seams (INV-35). The webview DOM (real button clicks) is sealed and
|
||||||
|
// manual-smoke only; here we simulate the inbound messages via `receiveMessage`.
|
||||||
|
suite("F11 preview toolbar (host E2E — message → seam wiring, no LLM)", () => {
|
||||||
|
// SLICE-1: the Pin baseline button.
|
||||||
|
test("pinBaseline message pins the PREVIEWED doc → marks clear, baseline reason is pinned (PUC-5, INV-35)", async () => {
|
||||||
|
const { doc, key } = await freshDoc("docs/f11pin.md", "# F11 pin\n\nA baseline paragraph that will diverge.\n");
|
||||||
|
const api = await getApi();
|
||||||
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
||||||
|
await settle();
|
||||||
|
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "panel open");
|
||||||
|
|
||||||
|
// Diverge from the opened baseline so the preview carries a real change-mark.
|
||||||
|
const edit = new vscode.WorkspaceEdit();
|
||||||
|
edit.insert(doc.uri, doc.positionAt(doc.getText().length), "\n\nA freshly typed paragraph that diverges.\n");
|
||||||
|
assert.ok(await vscode.workspace.applyEdit(edit), "operator edit applied");
|
||||||
|
await settle();
|
||||||
|
const marked = (api.trackChangesPreviewController.getLastModel(key) ?? []).some((o) => o.kind !== "unchanged");
|
||||||
|
assert.ok(marked, "the typed paragraph shows as a change before pinning");
|
||||||
|
|
||||||
|
// Simulate the webview's Pin baseline button posting its intent.
|
||||||
|
api.trackChangesPreviewController.receiveMessage(key, { type: "pinBaseline" });
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
const model = api.trackChangesPreviewController.getLastModel(key) ?? [];
|
||||||
|
assert.ok(model.length > 0 && model.every((o) => o.kind === "unchanged"), "after pin, every block is unchanged");
|
||||||
|
assert.strictEqual(api.diffViewController.getBaseline(key)?.reason, "pinned", "baseline reason advanced to pinned");
|
||||||
|
});
|
||||||
|
|
||||||
|
// SLICE-1 reachability: the orphaned pin command gets a real palette `when`.
|
||||||
|
test("pinDiffBaseline is reachable from the command palette (when: editorLangId == markdown)", async () => {
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8"));
|
||||||
|
const entry = (pkg.contributes.menus.commandPalette as Array<{ command: string; when?: string }>).find(
|
||||||
|
(m) => m.command === "cowriting.pinDiffBaseline",
|
||||||
|
);
|
||||||
|
assert.ok(entry, "pinDiffBaseline has a commandPalette entry");
|
||||||
|
assert.notStrictEqual(entry!.when, "false", "it is no longer hidden (when:false)");
|
||||||
|
assert.match(entry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user