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:
+45
-4
@@ -39,11 +39,51 @@ pinEl?.addEventListener("click", () => {
|
||||
vscodeApi.postMessage({ type: "pinBaseline" });
|
||||
});
|
||||
|
||||
// F11 (SLICE-3): Ask Claude to Edit Document — post the document-scope intent; the
|
||||
// host prompts for the instruction + runs the turn (INV-8). SLICE-4 makes this
|
||||
// button adaptive (Edit Selection when text is selected in the preview).
|
||||
// F11 (SLICE-4): the single adaptive Ask-Claude button. Its label flips on
|
||||
// `selectionchange` (Edit Selection when live text is selected in the preview,
|
||||
// Edit Document otherwise), and a click resolves the selection to a SOURCE range
|
||||
// via the nearest `data-src` ancestors (INV-36) — the webview's sole mapping
|
||||
// duty. A selection that resolves to no live block falls back to document scope.
|
||||
|
||||
/** Walk up from a DOM node to the nearest block carrying data-src offsets (INV-36). */
|
||||
function nearestSrc(node: Node | null): HTMLElement | null {
|
||||
let el: HTMLElement | null = node instanceof HTMLElement ? node : (node?.parentElement ?? null);
|
||||
while (el && el !== body) {
|
||||
if (el.dataset.srcStart !== undefined && el.dataset.srcEnd !== undefined) return el;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The source [start,end) union of the live blocks a non-empty body selection touches, or null. */
|
||||
function selectionSrcRange(): { start: number; end: number } | null {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.isCollapsed || sel.rangeCount === 0) return null;
|
||||
const ends = [nearestSrc(sel.anchorNode), nearestSrc(sel.focusNode)].filter(
|
||||
(e): e is HTMLElement => e !== null,
|
||||
);
|
||||
if (ends.length === 0) return null; // selection touches no live-source block
|
||||
const starts = ends.map((e) => Number(e.dataset.srcStart));
|
||||
const stops = ends.map((e) => Number(e.dataset.srcEnd));
|
||||
return { start: Math.min(...starts), end: Math.max(...stops) };
|
||||
}
|
||||
|
||||
function updateAskLabel(): void {
|
||||
if (!askEl) return;
|
||||
askEl.textContent = selectionSrcRange()
|
||||
? "✦ Ask Claude to Edit Selection"
|
||||
: "✦ Ask Claude to Edit Document";
|
||||
}
|
||||
|
||||
document.addEventListener("selectionchange", updateAskLabel);
|
||||
|
||||
askEl?.addEventListener("click", () => {
|
||||
vscodeApi.postMessage({ type: "askClaude", scope: "document" });
|
||||
const range = selectionSrcRange();
|
||||
if (range) {
|
||||
vscodeApi.postMessage({ type: "askClaude", scope: "selection", start: range.start, end: range.end });
|
||||
} else {
|
||||
vscodeApi.postMessage({ type: "askClaude", scope: "document" });
|
||||
}
|
||||
});
|
||||
|
||||
// F10: delegated ✓/✗ accept/reject of pending proposals (routed back to the F4 seam).
|
||||
@@ -83,6 +123,7 @@ window.addEventListener("message", (event: MessageEvent<RenderMessage>) => {
|
||||
const msg = event.data;
|
||||
if (msg?.type !== "render") return;
|
||||
body.innerHTML = msg.html;
|
||||
updateAskLabel(); // new content clears any selection → reset the adaptive label
|
||||
const on = msg.mode === "on";
|
||||
if (annotationsEl) annotationsEl.checked = on;
|
||||
// Off-state is a clean preview: hide the review chrome.
|
||||
|
||||
Reference in New Issue
Block a user