diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0440adcf..57932dc7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Windows external editor handoff so vim/nvim can receive input after opening from the TUI ([#4612](https://github.com/earendil-works/pi/issues/4612)). - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). - Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). - Fixed Windows pnpm self-updates to resolve pnpm command shims and run through pnpm instead of requiring manual updates ([#4157](https://github.com/earendil-works/pi/issues/4157)). diff --git a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts index e1134ce9..b33ad9f5 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts @@ -3,7 +3,7 @@ * Supports Ctrl+G for external editor. */ -import { spawnSync } from "node:child_process"; +import { spawn } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -110,7 +110,7 @@ export class ExtensionEditorComponent extends Container implements Focusable { this.editor.handleInput(keyData); } - private openExternalEditor(): void { + private async openExternalEditor(): Promise { const editorCmd = process.env.VISUAL || process.env.EDITOR; if (!editorCmd) { return; @@ -124,12 +124,21 @@ export class ExtensionEditorComponent extends Container implements Focusable { this.tui.stop(); const [editor, ...editorArgs] = editorCmd.split(" "); - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", + process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`); + + // Do not use spawnSync here. On Windows, synchronous child_process calls can keep + // Node/libuv's console input read active after tui.stop() pauses stdin, racing + // vim/nvim for the console input buffer until Ctrl+C cancels the pending read. + const status = await new Promise((resolve) => { + const child = spawn(editor, [...editorArgs, tmpFile], { + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", () => resolve(null)); + child.on("close", (code) => resolve(code)); }); - if (result.status === 0) { + if (status === 0) { const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); this.editor.setText(newContent); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index a493bf6f..b3df9eb4 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3497,7 +3497,7 @@ export class InteractiveMode { this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`); } - private openExternalEditor(): void { + private async openExternalEditor(): Promise { // Determine editor (respect $VISUAL, then $EDITOR) const editorCmd = process.env.VISUAL || process.env.EDITOR; if (!editorCmd) { @@ -3518,14 +3518,22 @@ export class InteractiveMode { // Split by space to support editor arguments (e.g., "code --wait") const [editor, ...editorArgs] = editorCmd.split(" "); - // Spawn editor synchronously with inherited stdio for interactive editing - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", + process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`); + + // Do not use spawnSync here. On Windows, synchronous child_process calls can keep + // Node/libuv's console input read active after ui.stop() pauses stdin, racing + // vim/nvim for the console input buffer until Ctrl+C cancels the pending read. + const status = await new Promise((resolve) => { + const child = spawn(editor, [...editorArgs, tmpFile], { + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", () => resolve(null)); + child.on("close", (code) => resolve(code)); }); // On successful exit (status 0), replace editor content - if (result.status === 0) { + if (status === 0) { const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); this.editor.setText(newContent); }