From afedf1707aa21c9b3cc55d31e665ad3afbe71fc8 Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Sun, 21 Dec 2025 12:06:57 +0100 Subject: [PATCH 1/6] feat(tui): add isCtrlG key detection --- packages/tui/src/index.ts | 1 + packages/tui/src/keys.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index fdf04af8..e09b5255 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -33,6 +33,7 @@ export { isCtrlC, isCtrlD, isCtrlE, + isCtrlG, isCtrlK, isCtrlLeft, isCtrlO, diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index d82a01ca..faa5e4d7 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -28,6 +28,7 @@ const CODEPOINTS = { c: 99, d: 100, e: 101, + g: 103, k: 107, o: 111, p: 112, @@ -160,6 +161,7 @@ export const Keys = { CTRL_C: kittySequence(CODEPOINTS.c, MODIFIERS.ctrl), CTRL_D: kittySequence(CODEPOINTS.d, MODIFIERS.ctrl), CTRL_E: kittySequence(CODEPOINTS.e, MODIFIERS.ctrl), + CTRL_G: kittySequence(CODEPOINTS.g, MODIFIERS.ctrl), CTRL_K: kittySequence(CODEPOINTS.k, MODIFIERS.ctrl), CTRL_O: kittySequence(CODEPOINTS.o, MODIFIERS.ctrl), CTRL_P: kittySequence(CODEPOINTS.p, MODIFIERS.ctrl), @@ -214,6 +216,7 @@ const RAW = { CTRL_C: "\x03", CTRL_D: "\x04", CTRL_E: "\x05", + CTRL_G: "\x07", CTRL_K: "\x0b", CTRL_O: "\x0f", CTRL_P: "\x10", @@ -256,6 +259,14 @@ export function isCtrlE(data: string): boolean { return data === RAW.CTRL_E || data === Keys.CTRL_E || matchesKittySequence(data, CODEPOINTS.e, MODIFIERS.ctrl); } +/** + * Check if input matches Ctrl+G (raw byte or Kitty protocol). + * Ignores lock key bits. + */ +export function isCtrlG(data: string): boolean { + return data === RAW.CTRL_G || data === Keys.CTRL_G || matchesKittySequence(data, CODEPOINTS.g, MODIFIERS.ctrl); +} + /** * Check if input matches Ctrl+K (raw byte or Kitty protocol). * Ignores lock key bits. From fe853ed869cc1acd7d1018980f9f763c25a3217e Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Sun, 21 Dec 2025 12:07:05 +0100 Subject: [PATCH 2/6] feat(coding-agent): add Ctrl+G to open external editor --- .../interactive/components/custom-editor.ts | 19 ++++++- .../src/modes/interactive/interactive-mode.ts | 51 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 98bbfd19..62171ca1 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -1,4 +1,14 @@ -import { Editor, isCtrlC, isCtrlD, isCtrlO, isCtrlP, isCtrlT, isEscape, isShiftTab } from "@mariozechner/pi-tui"; +import { + Editor, + isCtrlC, + isCtrlD, + isCtrlG, + isCtrlO, + isCtrlP, + isCtrlT, + isEscape, + isShiftTab, +} from "@mariozechner/pi-tui"; /** * Custom editor that handles Escape and Ctrl+C keys for coding-agent @@ -11,8 +21,15 @@ export class CustomEditor extends Editor { public onCtrlP?: () => void; public onCtrlO?: () => void; public onCtrlT?: () => void; + public onCtrlG?: () => void; handleInput(data: string): void { + // Intercept Ctrl+G for external editor + if (isCtrlG(data) && this.onCtrlG) { + this.onCtrlG(); + return; + } + // Intercept Ctrl+T for thinking block visibility toggle if (isCtrlT(data) && this.onCtrlT) { this.onCtrlT(); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 9675adc8..0c5ebf9d 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -4,6 +4,7 @@ */ import * as fs from "node:fs"; +import * as os from "node:os"; import * as path from "node:path"; import type { AgentState, AppMessage, Attachment } from "@mariozechner/pi-agent-core"; import type { AssistantMessage, Message } from "@mariozechner/pi-ai"; @@ -23,7 +24,7 @@ import { TUI, visibleWidth, } from "@mariozechner/pi-tui"; -import { exec } from "child_process"; +import { exec, spawnSync } from "child_process"; import { APP_NAME, getDebugLogPath, getOAuthPath } from "../../config.js"; import type { AgentSession, AgentSessionEvent } from "../../core/agent-session.js"; import type { LoadedCustomTool, SessionEvent as ToolSessionEvent } from "../../core/custom-tools/index.js"; @@ -579,6 +580,7 @@ export class InteractiveMode { this.editor.onCtrlP = () => this.cycleModel(); this.editor.onCtrlO = () => this.toggleToolOutputExpansion(); this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility(); + this.editor.onCtrlG = () => this.openExternalEditor(); this.editor.onChange = (text: string) => { const wasBashMode = this.isBashMode; @@ -1225,6 +1227,52 @@ export class InteractiveMode { this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`); } + private openExternalEditor(): void { + // Determine editor (respect $VISUAL, then $EDITOR) + const editorCmd = process.env.VISUAL || process.env.EDITOR; + if (!editorCmd) { + this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable."); + return; + } + + const currentText = this.editor.getText(); + const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`); + + try { + // Write current content to temp file + fs.writeFileSync(tmpFile, currentText, "utf-8"); + + // Stop TUI to release terminal + this.ui.stop(); + + // 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", + }); + + // On successful exit (status 0), replace editor content + if (result.status === 0) { + const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); + this.editor.setText(newContent); + } + // On non-zero exit, keep original text (no action needed) + } finally { + // Clean up temp file + try { + fs.unlinkSync(tmpFile); + } catch { + // Ignore cleanup errors + } + + // Restart TUI + this.ui.start(); + this.ui.requestRender(); + } + } + // ========================================================================= // UI helpers // ========================================================================= @@ -1703,6 +1751,7 @@ export class InteractiveMode { | \`Ctrl+P\` | Cycle models | | \`Ctrl+O\` | Toggle tool output expansion | | \`Ctrl+T\` | Toggle thinking block visibility | +| \`Ctrl+G\` | Edit message in external editor | | \`/\` | Slash commands | | \`!\` | Run bash command | `; From 0f6cb1183e4b107027449448f201138a1ee30431 Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Sun, 21 Dec 2025 12:07:11 +0100 Subject: [PATCH 3/6] docs(coding-agent): document Ctrl+G external editor shortcut --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/README.md | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 47b461ea..c62f03e8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- **External editor support**: Press `Ctrl+G` to edit your message in an external editor. Uses `$VISUAL` or `$EDITOR` environment variable. On successful save, the message is replaced; on cancel, the original is kept. ([#266](https://github.com/badlogic/pi-mono/pull/266) by [@aliou](https://github.com/aliou)) + ## [0.25.2] - 2025-12-21 ### Fixed diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index db3f4c25..69c6a717 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -230,6 +230,7 @@ The agent reads, writes, and edits files, and executes commands via bash. | Ctrl+P | Cycle models (scoped by `--models`) | | Ctrl+O | Toggle tool output expansion | | Ctrl+T | Toggle thinking block visibility | +| Ctrl+G | Edit message in external editor (`$VISUAL` or `$EDITOR`) | ### Bash Mode From 55ca650a40d6f1f2fb020af88f6bbbff6a80e565 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 21 Dec 2025 20:17:01 +0100 Subject: [PATCH 4/6] Update README to include github-copilot, google-gemini-cli, google-antigravity providers --- packages/ai/src/models.generated.ts | 46 ++++++++++++++--------------- packages/coding-agent/README.md | 2 +- pi-mono.code-workspace | 9 ------ 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 00f631dc..a859bd21 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -6019,9 +6019,9 @@ export const MODELS = { contextWindow: 32768, maxTokens: 4096, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", + "anthropic/claude-3.5-haiku-20241022": { + id: "anthropic/claude-3.5-haiku-20241022", + name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -6036,9 +6036,9 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku-20241022": { - id: "anthropic/claude-3.5-haiku-20241022", - name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", + "anthropic/claude-3.5-haiku": { + id: "anthropic/claude-3.5-haiku", + name: "Anthropic: Claude 3.5 Haiku", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -6274,23 +6274,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 16384, } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-405b-instruct": { - id: "meta-llama/llama-3.1-405b-instruct", - name: "Meta: Llama 3.1 405B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3.5, - output: 3.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 130815, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "meta-llama/llama-3.1-8b-instruct": { id: "meta-llama/llama-3.1-8b-instruct", name: "Meta: Llama 3.1 8B Instruct", @@ -6308,6 +6291,23 @@ export const MODELS = { contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, + "meta-llama/llama-3.1-405b-instruct": { + id: "meta-llama/llama-3.1-405b-instruct", + name: "Meta: Llama 3.1 405B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 3.5, + output: 3.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 130815, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "meta-llama/llama-3.1-70b-instruct": { id: "meta-llama/llama-3.1-70b-instruct", name: "Meta: Llama 3.1 70B Instruct", diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 69c6a717..5c4e815f 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -716,7 +716,7 @@ pi [options] [@files...] [messages...] | Option | Description | |--------|-------------| -| `--provider ` | Provider: `anthropic`, `openai`, `google`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`, or custom | +| `--provider ` | Provider: `anthropic`, `openai`, `google`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`, `github-copilot`, `google-gemini-cli`, `google-antigravity`, or custom | | `--model ` | Model ID | | `--api-key ` | API key (overrides environment) | | `--system-prompt ` | Custom system prompt (text or file path) | diff --git a/pi-mono.code-workspace b/pi-mono.code-workspace index 8b54d2ba..7cfaae8b 100644 --- a/pi-mono.code-workspace +++ b/pi-mono.code-workspace @@ -6,15 +6,6 @@ }, { "path": "../../moms" - }, - { - "path": "../../.pi/agent" - }, - { - "path": "../pi-skills" - }, - { - "path": "../sitegeist" } ], "settings": {} From 8868d623fc60732d9310f79336547cd854998f64 Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Sun, 21 Dec 2025 20:19:32 +0100 Subject: [PATCH 5/6] feat(coding-agent): add Ctrl+Z to suspend process (#267) * feat(tui): add isCtrlZ key detection and resetRenderState method * feat(coding-agent): add Ctrl+Z handler to suspend process * docs(coding-agent): add Ctrl+Z to keyboard shortcuts documentation * feat(tui): add force parameter to requestRender --- packages/coding-agent/CHANGELOG.md | 2 ++ packages/coding-agent/README.md | 1 + .../interactive/components/custom-editor.ts | 8 ++++++++ .../src/modes/interactive/interactive-mode.ts | 19 +++++++++++++++++++ packages/tui/src/index.ts | 1 + packages/tui/src/keys.ts | 11 +++++++++++ packages/tui/src/tui.ts | 7 ++++++- 7 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c62f03e8..83d29cf2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,8 @@ - **External editor support**: Press `Ctrl+G` to edit your message in an external editor. Uses `$VISUAL` or `$EDITOR` environment variable. On successful save, the message is replaced; on cancel, the original is kept. ([#266](https://github.com/badlogic/pi-mono/pull/266) by [@aliou](https://github.com/aliou)) +- **Process suspension**: Press `Ctrl+Z` to suspend pi and return to the shell. Resume with `fg` as usual. ([#267](https://github.com/badlogic/pi-mono/pull/267) by [@aliou](https://github.com/aliou)) + ## [0.25.2] - 2025-12-21 ### Fixed diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 5c4e815f..e646b6a4 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -226,6 +226,7 @@ The agent reads, writes, and edits files, and executes commands via bash. | Escape | Cancel autocomplete / abort streaming | | Ctrl+C | Clear editor (first) / exit (second) | | Ctrl+D | Exit (when editor is empty) | +| Ctrl+Z | Suspend to background | | Shift+Tab | Cycle thinking level | | Ctrl+P | Cycle models (scoped by `--models`) | | Ctrl+O | Toggle tool output expansion | diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 62171ca1..a485b1fc 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -6,6 +6,7 @@ import { isCtrlO, isCtrlP, isCtrlT, + isCtrlZ, isEscape, isShiftTab, } from "@mariozechner/pi-tui"; @@ -22,6 +23,7 @@ export class CustomEditor extends Editor { public onCtrlO?: () => void; public onCtrlT?: () => void; public onCtrlG?: () => void; + public onCtrlZ?: () => void; handleInput(data: string): void { // Intercept Ctrl+G for external editor @@ -30,6 +32,12 @@ export class CustomEditor extends Editor { return; } + // Intercept Ctrl+Z for suspend + if (isCtrlZ(data) && this.onCtrlZ) { + this.onCtrlZ(); + return; + } + // Intercept Ctrl+T for thinking block visibility toggle if (isCtrlT(data) && this.onCtrlT) { this.onCtrlT(); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 0c5ebf9d..a8a7fe59 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -213,6 +213,9 @@ export class InteractiveMode { theme.fg("dim", "ctrl+d") + theme.fg("muted", " to exit (empty)") + "\n" + + theme.fg("dim", "ctrl+z") + + theme.fg("muted", " to suspend") + + "\n" + theme.fg("dim", "ctrl+k") + theme.fg("muted", " to delete line") + "\n" + @@ -576,6 +579,7 @@ export class InteractiveMode { this.editor.onCtrlC = () => this.handleCtrlC(); this.editor.onCtrlD = () => this.handleCtrlD(); + this.editor.onCtrlZ = () => this.handleCtrlZ(); this.editor.onShiftTab = () => this.cycleThinkingLevel(); this.editor.onCtrlP = () => this.cycleModel(); this.editor.onCtrlO = () => this.toggleToolOutputExpansion(); @@ -1159,6 +1163,20 @@ export class InteractiveMode { process.exit(0); } + private handleCtrlZ(): void { + // Set up handler to restore TUI when resumed + process.once("SIGCONT", () => { + this.ui.start(); + this.ui.requestRender(true); + }); + + // Stop the TUI (restore terminal to normal mode) + this.ui.stop(); + + // Send SIGTSTP to process group (pid=0 means all processes in group) + process.kill(0, "SIGTSTP"); + } + private updateEditorBorderColor(): void { if (this.isBashMode) { this.editor.borderColor = theme.getBashModeBorderColor(); @@ -1747,6 +1765,7 @@ export class InteractiveMode { | \`Escape\` | Cancel autocomplete / abort streaming | | \`Ctrl+C\` | Clear editor (first) / exit (second) | | \`Ctrl+D\` | Exit (when editor is empty) | +| \`Ctrl+Z\` | Suspend to background | | \`Shift+Tab\` | Cycle thinking level | | \`Ctrl+P\` | Cycle models | | \`Ctrl+O\` | Toggle tool output expansion | diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index e09b5255..1e8bb605 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -42,6 +42,7 @@ export { isCtrlT, isCtrlU, isCtrlW, + isCtrlZ, isDelete, isEnd, isEnter, diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index faa5e4d7..d7a82cd4 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -35,6 +35,7 @@ const CODEPOINTS = { t: 116, u: 117, w: 119, + z: 122, // Special keys escape: 27, @@ -168,6 +169,7 @@ export const Keys = { CTRL_T: kittySequence(CODEPOINTS.t, MODIFIERS.ctrl), CTRL_U: kittySequence(CODEPOINTS.u, MODIFIERS.ctrl), CTRL_W: kittySequence(CODEPOINTS.w, MODIFIERS.ctrl), + CTRL_Z: kittySequence(CODEPOINTS.z, MODIFIERS.ctrl), // Enter combinations SHIFT_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.shift), @@ -223,6 +225,7 @@ const RAW = { CTRL_T: "\x14", CTRL_U: "\x15", CTRL_W: "\x17", + CTRL_Z: "\x1a", ALT_BACKSPACE: "\x1b\x7f", SHIFT_TAB: "\x1b[Z", } as const; @@ -322,6 +325,14 @@ export function isCtrlW(data: string): boolean { return data === RAW.CTRL_W || data === Keys.CTRL_W || matchesKittySequence(data, CODEPOINTS.w, MODIFIERS.ctrl); } +/** + * Check if input matches Ctrl+Z (raw byte or Kitty protocol). + * Ignores lock key bits. + */ +export function isCtrlZ(data: string): boolean { + return data === RAW.CTRL_Z || data === Keys.CTRL_Z || matchesKittySequence(data, CODEPOINTS.z, MODIFIERS.ctrl); +} + /** * Check if input matches Alt+Backspace (legacy or Kitty protocol). * Ignores lock key bits. diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 61474f8a..1baf5e4d 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -118,7 +118,12 @@ export class TUI extends Container { this.terminal.stop(); } - requestRender(): void { + requestRender(force = false): void { + if (force) { + this.previousLines = []; + this.previousWidth = 0; + this.cursorRow = 0; + } if (this.renderRequested) return; this.renderRequested = true; process.nextTick(() => { From 6683e06376de2319fe3008cacbc5a8de94fe6c2f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 21 Dec 2025 20:22:29 +0100 Subject: [PATCH 6/6] Clarify Ctrl+Z docs to mention fg for resume --- packages/coding-agent/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index e646b6a4..d9443a42 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -226,7 +226,7 @@ The agent reads, writes, and edits files, and executes commands via bash. | Escape | Cancel autocomplete / abort streaming | | Ctrl+C | Clear editor (first) / exit (second) | | Ctrl+D | Exit (when editor is empty) | -| Ctrl+Z | Suspend to background | +| Ctrl+Z | Suspend to background (use `fg` in shell to resume) | | Shift+Tab | Cycle thinking level | | Ctrl+P | Cycle models (scoped by `--models`) | | Ctrl+O | Toggle tool output expansion |