diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index c51c2b31..087e8ac3 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/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index b9c43109..cbeb60a2 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -23,10 +23,19 @@ import { convertMessages, convertTools, mapStopReasonString, mapToolChoice } fro export interface GoogleGeminiCliOptions extends StreamOptions { toolChoice?: "auto" | "none" | "any"; + /** + * Thinking/reasoning configuration. + * - Gemini 2.x models: use `budgetTokens` to set the thinking budget + * - Gemini 3 models (gemini-3-pro-*, gemini-3-flash-*): use `level` instead + * + * When using `streamSimple`, this is handled automatically based on the model. + */ thinking?: { enabled: boolean; + /** Thinking budget in tokens. Use for Gemini 2.x models. */ budgetTokens?: number; - level?: ThinkingLevel; // For Gemini 3 models + /** Thinking level. Use for Gemini 3 models (LOW/HIGH for Pro, MINIMAL/LOW/MEDIUM/HIGH for Flash). */ + level?: ThinkingLevel; }; projectId?: string; } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 637e44cf..216183e6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,11 @@ ### Added -- **Gemini 3 preview models**: Added `gemini-3-pro-preview` and `gemini-3-flash-preview` to the google-gemini-cli provider. +- **Gemini 3 preview models**: Added `gemini-3-pro-preview` and `gemini-3-flash-preview` to the google-gemini-cli provider. ([#264](https://github.com/badlogic/pi-mono/pull/264) by [@LukeFost](https://github.com/LukeFost)) + +- **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 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index db3f4c25..d9443a42 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -226,10 +226,12 @@ 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 (use `fg` in shell to resume) | | Shift+Tab | Cycle thinking level | | 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 @@ -715,7 +717,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/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 98bbfd19..a485b1fc 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,15 @@ -import { Editor, isCtrlC, isCtrlD, isCtrlO, isCtrlP, isCtrlT, isEscape, isShiftTab } from "@mariozechner/pi-tui"; +import { + Editor, + isCtrlC, + isCtrlD, + isCtrlG, + isCtrlO, + isCtrlP, + isCtrlT, + isCtrlZ, + isEscape, + isShiftTab, +} from "@mariozechner/pi-tui"; /** * Custom editor that handles Escape and Ctrl+C keys for coding-agent @@ -11,8 +22,22 @@ export class CustomEditor extends Editor { public onCtrlP?: () => void; public onCtrlO?: () => void; public onCtrlT?: () => void; + public onCtrlG?: () => void; + public onCtrlZ?: () => void; handleInput(data: string): void { + // Intercept Ctrl+G for external editor + if (isCtrlG(data) && this.onCtrlG) { + this.onCtrlG(); + 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 9675adc8..a8a7fe59 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"; @@ -212,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" + @@ -575,10 +579,12 @@ 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(); this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility(); + this.editor.onCtrlG = () => this.openExternalEditor(); this.editor.onChange = (text: string) => { const wasBashMode = this.isBashMode; @@ -1157,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(); @@ -1225,6 +1245,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 // ========================================================================= @@ -1699,10 +1765,12 @@ 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 | | \`Ctrl+T\` | Toggle thinking block visibility | +| \`Ctrl+G\` | Edit message in external editor | | \`/\` | Slash commands | | \`!\` | Run bash command | `; diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index fdf04af8..1e8bb605 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, @@ -41,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 d82a01ca..d7a82cd4 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -28,12 +28,14 @@ const CODEPOINTS = { c: 99, d: 100, e: 101, + g: 103, k: 107, o: 111, p: 112, t: 116, u: 117, w: 119, + z: 122, // Special keys escape: 27, @@ -160,12 +162,14 @@ 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), 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), @@ -214,12 +218,14 @@ const RAW = { CTRL_C: "\x03", CTRL_D: "\x04", CTRL_E: "\x05", + CTRL_G: "\x07", CTRL_K: "\x0b", CTRL_O: "\x0f", CTRL_P: "\x10", CTRL_T: "\x14", CTRL_U: "\x15", CTRL_W: "\x17", + CTRL_Z: "\x1a", ALT_BACKSPACE: "\x1b\x7f", SHIFT_TAB: "\x1b[Z", } as const; @@ -256,6 +262,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. @@ -311,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(() => { 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": {}