F3 SLICE-5 review fixes: fence-aware replacement extraction + editSelection guards (#6)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-10 10:35:16 -07:00
parent 258d1fa914
commit fe23ffa100
5 changed files with 41 additions and 11 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ const options = {
bundle: true, bundle: true,
platform: "node", platform: "node",
format: "cjs", format: "cjs",
target: "node20", target: "node22",
sourcemap: true, sourcemap: true,
// vscode is provided by the host; @cline/sdk is ESM-only and is loaded at // vscode is provided by the host; @cline/sdk is ESM-only and is loaded at
// runtime via dynamic import() from node_modules, so keep both external. // runtime via dynamic import() from node_modules, so keep both external.
+1 -1
View File
@@ -17,6 +17,6 @@ try {
console.log(`elapsed: ${((Date.now() - t0) / 1000).toFixed(1)}s`); console.log(`elapsed: ${((Date.now() - t0) / 1000).toFixed(1)}s`);
process.exit(0); process.exit(0);
} catch (err) { } catch (err) {
console.error(`live turn failed (expected when Claude Code is absent/signed out): ${err.message}`); console.error(`live turn failed (expected when Claude Code is absent/signed out): ${err instanceof Error ? err.message : String(err)}`);
process.exit(1); process.exit(1);
} }
+17 -2
View File
@@ -86,7 +86,12 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
context.subscriptions.push( context.subscriptions.push(
vscode.commands.registerCommand("cowriting.editSelection", async () => { vscode.commands.registerCommand("cowriting.editSelection", async () => {
const editor = vscode.window.activeTextEditor; const editor = vscode.window.activeTextEditor;
if (!editor || editor.selection.isEmpty || !editor.document.uri.fsPath.startsWith(root)) { if (
!editor ||
editor.selection.isEmpty ||
editor.document.uri.scheme !== "file" ||
!editor.document.uri.fsPath.startsWith(root)
) {
void vscode.window.showWarningMessage("Cowriting: select some text in a workspace document first."); void vscode.window.showWarningMessage("Cowriting: select some text in a workspace document first.");
return; return;
} }
@@ -95,6 +100,10 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
placeHolder: "e.g. tighten this paragraph", placeHolder: "e.g. tighten this paragraph",
}); });
if (!instruction) return; if (!instruction) return;
if (editor.selection.isEmpty) {
void vscode.window.showWarningMessage("Cowriting: select some text in a workspace document first.");
return;
}
const document = editor.document; const document = editor.document;
const selection = editor.selection; const selection = editor.selection;
const expectedVersion = document.version; const expectedVersion = document.version;
@@ -106,6 +115,12 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
async () => { async () => {
const { runEditTurn } = await import("./liveTurn"); const { runEditTurn } = await import("./liveTurn");
const turn = await runEditTurn(instruction, selectedText); const turn = await runEditTurn(instruction, selectedText);
if (turn.replacement === "") {
void vscode.window.showWarningMessage(
"Cowriting: Claude returned an empty replacement — nothing was applied.",
);
return;
}
const ok = await attributionController.applyAgentEdit( const ok = await attributionController.applyAgentEdit(
document, document,
new vscode.Range(selection.start, selection.end), new vscode.Range(selection.start, selection.end),
@@ -119,7 +134,7 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
); );
if (!ok) { if (!ok) {
void vscode.window.showWarningMessage( void vscode.window.showWarningMessage(
"Cowriting: the document changed while Claude was editing — nothing was applied.", "Cowriting: the edit could not be applied (the document changed during the turn, or the editor rejected the edit) — nothing was changed.",
); );
} }
}, },
+10 -3
View File
@@ -24,8 +24,15 @@ const SYSTEM_PROMPT = [
"unless the instruction says otherwise.", "unless the instruction says otherwise.",
].join(" "); ].join(" ");
/** Strip a wrapping markdown code fence if the model added one anyway. */ /**
export function extractReplacement(outputText: string): string { * Strip a wrapping markdown code fence if the model added one anyway.
* If `selectedText` itself was fence-wrapped (i.e. the user's selection was a
* fenced block), the model mirroring that format is correct return
* `outputText` unchanged so the fence is preserved.
*/
export function extractReplacement(outputText: string, selectedText: string): string {
const fencePattern = /^\s*```[^\n]*\n[\s\S]*?\n?```\s*$/;
if (fencePattern.test(selectedText)) return outputText;
const m = outputText.match(/^\s*```[^\n]*\n([\s\S]*?)\n?```\s*$/); const m = outputText.match(/^\s*```[^\n]*\n([\s\S]*?)\n?```\s*$/);
return m ? m[1] : outputText; return m ? m[1] : outputText;
} }
@@ -53,5 +60,5 @@ export async function runEditTurn(
"(is Claude Code installed and signed in?)", "(is Claude Code installed and signed in?)",
); );
} }
return { replacement: extractReplacement(result.outputText), model: modelId, sessionId: result.runId }; return { replacement: extractReplacement(result.outputText, selectedText), model: modelId, sessionId: result.runId };
} }
+12 -4
View File
@@ -3,16 +3,24 @@ import { extractReplacement } from "../src/liveTurn";
describe("extractReplacement", () => { describe("extractReplacement", () => {
it("returns plain text untouched", () => { it("returns plain text untouched", () => {
expect(extractReplacement("Hello world.")).toBe("Hello world."); expect(extractReplacement("Hello world.", "x")).toBe("Hello world.");
}); });
it("strips a wrapping fence", () => { it("strips a wrapping fence", () => {
expect(extractReplacement("```\nHello world.\n```")).toBe("Hello world."); expect(extractReplacement("```\nHello world.\n```", "x")).toBe("Hello world.");
}); });
it("strips a language-tagged fence", () => { it("strips a language-tagged fence", () => {
expect(extractReplacement("```markdown\nHello.\n```\n")).toBe("Hello."); expect(extractReplacement("```markdown\nHello.\n```\n", "x")).toBe("Hello.");
}); });
it("leaves inner fences alone", () => { it("leaves inner fences alone", () => {
const inner = "before\n```js\ncode\n```\nafter"; const inner = "before\n```js\ncode\n```\nafter";
expect(extractReplacement(inner)).toBe(inner); expect(extractReplacement(inner, "x")).toBe(inner);
});
it("keeps the fence when the original selection was itself fence-wrapped", () => {
const sel = "```python\nold()\n```";
const out = "```python\nnew()\n```";
expect(extractReplacement(out, sel)).toBe(out);
});
it("still strips when the selection was plain but the model added a fence", () => {
expect(extractReplacement("```\nplain\n```", "plain old text")).toBe("plain");
}); });
}); });