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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user