Merge PR #264: Add Gemini 3 preview models to google-gemini-cli provider

This commit is contained in:
Mario Zechner 2025-12-21 20:31:19 +01:00
commit 329b3a0a36
10 changed files with 166 additions and 38 deletions

View file

@ -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

View file

@ -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 <name>` | Provider: `anthropic`, `openai`, `google`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`, or custom |
| `--provider <name>` | Provider: `anthropic`, `openai`, `google`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`, `github-copilot`, `google-gemini-cli`, `google-antigravity`, or custom |
| `--model <id>` | Model ID |
| `--api-key <key>` | API key (overrides environment) |
| `--system-prompt <text\|file>` | Custom system prompt (text or file path) |

View file

@ -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();

View file

@ -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 |
`;