import * as vscode from "vscode"; import { randomBytes } from "crypto"; /** * The multi-line instruction input for "Ask Claude to Edit" — BOTH the selection * and the whole-document case. A small focused webview (a tall, resizable * textarea + Send) opened in a split pane BELOW the document — not a comment * thread, and not the top QuickInput (which is single-line only; VS Code has no * multi-line input at the command-palette location). For a selection edit the * document stays in the pane above with the selection still highlighted (VS * Code's inactive-selection style), so the user can see exactly what Claude will * edit; the caller has already captured the selection, so we never touch it. * * The split collapses when the input is submitted or cancelled, and focus is * handed back to the document so the collapsing split doesn't reveal the bottom * panel. `header` names the scope ("Ask Claude to Edit This Selection" / * "…This Document"). * * The webview only collects text and posts it to the host — no SDK or secret * surface lives in it (INV-8/35); the sealed CSP allows no network. Because a * webview CAN read its own textarea, Cancel / Escape confirms ONLY when there is * text to lose (closing the tab is an explicit dismiss, no prompt). Resolves with * the typed instruction, or `undefined` if cancelled / closed / left empty. */ export async function promptEditInstruction(header: string): Promise { // Remember the document editor we came from so we can hand focus back to it // when the input closes — otherwise, when the empty split group below collapses, // focus falls into the bottom panel (Output / Debug Console / …) and it pops // open. Restoring editor focus leaves whatever panel state the user had untouched. const source = vscode.window.activeTextEditor; // Open a split editor group BELOW the current one and host the input there, so // it sits under the document instead of covering it as a tab. The new group is // empty, so disposing the panel on submit/cancel leaves it empty and VS Code // collapses the split (the `workbench.editor.closeEmptyGroups` default). Falls // back to a tab in the active group if the split command is unavailable. try { await vscode.commands.executeCommand("workbench.action.newGroupBelow"); } catch { /* no split — the panel opens as a tab in the active group instead */ } return new Promise((resolve) => { const panel = vscode.window.createWebviewPanel( "cowriting.askClaudeInput", header, { viewColumn: vscode.ViewColumn.Active, preserveFocus: false }, { enableScripts: true, retainContextWhenHidden: false }, ); let settled = false; const done = (value: string | undefined): void => { if (settled) return; settled = true; resolve(value); panel.dispose(); // Hand focus back to the originating document so the collapsing split // doesn't leave focus in (and reveal) the bottom panel. if (source) { void vscode.window.showTextDocument(source.document, { viewColumn: source.viewColumn ?? vscode.ViewColumn.One, preserveFocus: false, }); } }; panel.webview.onDidReceiveMessage((m: { type?: string; text?: string }) => { const text = (m?.text ?? "").trim(); if (m?.type === "submit") { done(text ? text : undefined); } else if (m?.type === "cancel") { // Confirm only if there's something to lose (we can read the textarea here). if (!text) { done(undefined); return; } void vscode.window .showWarningMessage("Discard your Ask-Claude instruction?", { modal: true }, "Discard") .then((pick) => { if (pick === "Discard") done(undefined); // else: leave the panel open so the operator can keep editing. }); } }); // Closing the tab is an explicit dismiss — cancel without a prompt. panel.onDidDispose(() => done(undefined)); panel.webview.html = htmlFor(header); }); } function escapeHtml(s: string): string { return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!); } function htmlFor(header: string): string { const nonce = randomBytes(16).toString("base64"); // Sealed CSP: no network; inline style only; the one script is nonce-gated. const csp = `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';`; const title = escapeHtml(header); return ` ${title}

${title}

⌘↵ / Ctrl+↵ to send · Esc to cancel
`; }