fix(coding-agent): restored Windows external editor launch

Passed the cmd.exe /s /c command line through Bun verbatim so configured editors and temporary paths retain their quotes.

Added command-line regression coverage and documented the fix.

Fixes #8544
This commit is contained in:
roboomp
2026-08-14 11:27:13 +00:00
parent ad318c7572
commit a9075ae509
3 changed files with 44 additions and 6 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Ctrl+G external editors failing to launch on Windows because Bun re-quoted the embedded `cmd.exe /c` command line ([#8544](https://github.com/can1357/oh-my-pi/issues/8544)).
## [17.3.3] - 2026-08-14
### Fixed
@@ -33,6 +33,27 @@ export interface OpenInEditorOptions {
trimTrailingNewline?: boolean;
}
/** Subprocess argv and Windows quoting mode used to launch an external editor. */
export interface EditorSpawnCommand {
cmd: string[];
windowsVerbatimArguments: boolean;
}
/** Resolves shell argv without letting the host runtime re-quote the editor command. */
export function resolveEditorSpawnCommand(
editorCmd: string,
tmpFile: string,
platform: NodeJS.Platform = process.platform,
): EditorSpawnCommand {
const windows = platform === "win32";
// cmd.exe strips the outer /s /c quote pair; Bun must pass the embedded
// editor/path quotes verbatim instead of applying argv escaping to them.
const cmd = windows
? ["cmd.exe", "/d", "/s", "/c", `"${editorCmd} "${tmpFile}""`]
: [$which("sh") ?? "sh", "-c", `${editorCmd} "$1"`, "sh", tmpFile];
return { cmd, windowsVerbatimArguments: windows };
}
/**
* Opens `content` in the user's external editor and returns the edited text.
* Returns `null` if the editor exits with a non-zero code.
@@ -50,15 +71,13 @@ export async function openInEditor(
try {
await Bun.write(tmpFile, content);
const spawnCommand = resolveEditorSpawnCommand(editorCmd, tmpFile);
const [stdin, stdout, stderr] = options?.stdio ?? ["inherit", "inherit", "inherit"];
const cmd =
process.platform === "win32"
? ["cmd", "/c", `${editorCmd} "${tmpFile}"`]
: [$which("sh") ?? "sh", "-c", `${editorCmd} "$1"`, "sh", tmpFile];
const child = Bun.spawn(cmd, {
const child = Bun.spawn(spawnCommand.cmd, {
stdin,
stdout,
stderr,
windowsVerbatimArguments: spawnCommand.windowsVerbatimArguments,
});
const exitCode = await child.exited;
if (exitCode === 0) {
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { TempDir } from "@oh-my-pi/pi-utils";
import { getEditorCommand, openInEditor } from "../src/utils/external-editor";
import { getEditorCommand, openInEditor, resolveEditorSpawnCommand } from "../src/utils/external-editor";
interface MutableProcess {
platform: NodeJS.Platform;
@@ -66,6 +66,21 @@ describe("getEditorCommand", () => {
});
describe("openInEditor", () => {
it("passes the cmd.exe command line verbatim on Windows", () => {
const tmpFile = String.raw`C:\Users\Example User\AppData\Local\Temp\omp-editor-123.omp.md`;
expect(resolveEditorSpawnCommand('"C:\\Program Files\\Code.exe" --wait', tmpFile, "win32")).toEqual({
cmd: [
"cmd.exe",
"/d",
"/s",
"/c",
String.raw`""C:\Program Files\Code.exe" --wait "C:\Users\Example User\AppData\Local\Temp\omp-editor-123.omp.md""`,
],
windowsVerbatimArguments: true,
});
});
it.skipIf(process.platform === "win32")("supports quoted editor paths containing spaces", async () => {
const tempDir = TempDir.createSync("@external-editor-");
try {