feat(f11): SLICE-4 — single adaptive Ask-Claude button + selection mapping (#43, INV-37)
The one Ask-Claude toolbar button now adapts: its label flips on selectionchange
(Edit Selection when live text is selected in the preview, Edit Document
otherwise), and a click resolves the preview selection to a SOURCE range via the
nearest data-src ancestors (INV-36) — the webview's sole mapping duty. A
selection touching no live-source block falls back to document scope. Per spec
§6.5 PUC-2/3 / §7.2 SLICE-4.
- webview (media/preview.ts): nearestSrc() DOM walk + selectionSrcRange()
block-union; updateAskLabel() on selectionchange + after each render; the
adaptive click posts { askClaude, scope:"selection", start, end } or falls back
to document scope. Sealed (INV-21): reads data-src only, posts intent.
- host: runEditAndPropose's range branch (already shared from SLICE-3) records one
single-range F4 proposal over the resolved block-union.
- host E2E: stubbed selection turn → exactly one proposal over the resolved range
(turn receives exactly the selected source; replaced == the range; doc
untouched); an unchanged replacement proposes nothing. (The webview DOM
selection→data-src lookup is sealed-sandbox → manual smoke, spec §6.8.)
205 unit + 51 host E2E green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,49 @@ suite("F11 preview toolbar (host E2E — message → seam wiring, no LLM)", () =
|
||||
void key;
|
||||
});
|
||||
|
||||
// SLICE-4: Edit Selection → one single-range proposal over the resolved block-union.
|
||||
test("runEditAndPropose(range) with a stubbed turn → exactly one proposal over the resolved range (PUC-3, INV-37)", async () => {
|
||||
const body = "# F11 sel\n\nThe target paragraph Claude will rewrite.\n\nAnother untouched paragraph.\n";
|
||||
const { doc, key } = await freshDoc("docs/f11sel.md", body);
|
||||
const api = await getApi();
|
||||
const ctl = api.trackChangesPreviewController;
|
||||
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
||||
await settle();
|
||||
|
||||
const target = "The target paragraph Claude will rewrite.";
|
||||
const start = doc.getText().indexOf(target);
|
||||
const end = start + target.length;
|
||||
ctl.setEditTurnForTest(async (_instruction, text) => {
|
||||
assert.strictEqual(text, target, "the turn receives exactly the selected source range");
|
||||
return { replacement: "The REWRITTEN paragraph from Claude.", model: "sonnet", sessionId: "e2e-f11-sel" };
|
||||
});
|
||||
|
||||
const ids = await ctl.runEditAndPropose(doc, { kind: "range", start, end }, "rewrite this paragraph");
|
||||
await settle();
|
||||
assert.strictEqual(ids.length, 1, "a selection yields exactly one proposal");
|
||||
const views = api.proposalController.listProposals(doc);
|
||||
const view = views.find((v) => v.id === ids[0]);
|
||||
assert.ok(view, "the proposal is live");
|
||||
assert.strictEqual(view!.replacement, "The REWRITTEN paragraph from Claude.", "carries the turn replacement");
|
||||
assert.strictEqual(view!.replaced, target, "replaces exactly the selected range");
|
||||
assert.ok(doc.getText().includes(target), "document unchanged by propose (INV-10)");
|
||||
void key;
|
||||
});
|
||||
|
||||
// SLICE-4: a no-op turn (Claude returns the input unchanged) produces no proposal.
|
||||
test("runEditAndPropose(range) where Claude returns the selection unchanged → no proposal", async () => {
|
||||
const { doc } = await freshDoc("docs/f11noop.md", "# noop\n\nLeave me exactly as I am.\n");
|
||||
const api = await getApi();
|
||||
const ctl = api.trackChangesPreviewController;
|
||||
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
||||
await settle();
|
||||
const target = "Leave me exactly as I am.";
|
||||
const start = doc.getText().indexOf(target);
|
||||
ctl.setEditTurnForTest(async (_i, text) => ({ replacement: text, model: "sonnet", sessionId: "e2e-noop" }));
|
||||
const ids = await ctl.runEditAndPropose(doc, { kind: "range", start, end: start + target.length }, "no change");
|
||||
assert.strictEqual(ids.length, 0, "an unchanged replacement proposes nothing");
|
||||
});
|
||||
|
||||
// SLICE-3: the document-scoped command exists for #42 reuse, guarded on markdown.
|
||||
test("cowriting.editDocument is a registered command, palette-guarded on markdown", async () => {
|
||||
const all = await vscode.commands.getCommands(true);
|
||||
|
||||
Reference in New Issue
Block a user