diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9bf59682..ca8c4fa5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,10 @@ - **Model Cycling with Thinking Levels**: The `--models` flag now supports thinking level syntax (e.g., `--models sonnet:high,haiku:low`). When cycling models with `Ctrl+P`, the associated thinking level is automatically applied. The first model in the scope is used as the initial model when starting a new session. Both model and thinking level changes are now saved to session and settings for persistence. ([#47](https://github.com/badlogic/pi-mono/pull/47)) - **`--thinking` Flag**: New CLI flag to set thinking level directly (e.g., `--thinking high`). Valid values: `off`, `minimal`, `low`, `medium`, `high`. Takes highest priority over all other thinking level sources. ([#45](https://github.com/badlogic/pi-mono/issues/45)) +### Breaking + +- **Interactive Mode with Initial Prompt**: Passing a prompt on the command line (e.g., `pi "List files"`) now starts interactive mode with the prompt pre-submitted, instead of exiting after completion. Use `--print` or `-p` to get the previous non-interactive behavior (e.g., `pi -p "List files"`). This matches Claude CLI (`-p`) and Codex (`exec`) behavior. ([#46](https://github.com/badlogic/pi-mono/issues/46)) + ### Fixed - **Slash Command Autocomplete**: Fixed issue where pressing Enter on a highlighted slash command suggestion (e.g., typing `/mod` with `/model` highlighted) would submit the partial text instead of executing the selected command. Now Enter applies the completion and submits in one action. ([#49](https://github.com/badlogic/pi-mono/issues/49)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 93f3e7d5..fa07551f 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -250,10 +250,11 @@ You can add custom HTTP headers to bypass Cloudflare bot detection, add authenti When starting `pi`, models are selected in this order: 1. **CLI args**: `--provider` and `--model` flags -2. **Restored from session**: If using `--continue` or `--resume` -3. **Saved default**: From `~/.pi/agent/settings.json` (set when you select a model with `/model`) -4. **First available**: First model with a valid API key -5. **None**: Allowed in interactive mode (shows error on message submission) +2. **First from `--models` scope**: If `--models` is provided (skipped when using `--continue` or `--resume`) +3. **Restored from session**: If using `--continue` or `--resume` +4. **Saved default**: From `~/.pi/agent/settings.json` (set when you select a model with `/model`) +5. **First available**: First model with a valid API key +6. **None**: Allowed in interactive mode (shows error on message submission) ### Provider Defaults @@ -652,11 +653,14 @@ Custom system prompt. Can be: If the argument is a valid file path, the file contents will be used as the system prompt. Otherwise, the text is used directly. Project context files and datetime are automatically appended. **--mode ** -Output mode for non-interactive usage. Options: +Output mode for non-interactive usage (implies `--print`). Options: - `text` (default): Output only the final assistant message text - `json`: Stream all agent events as JSON (one event per line). Events are emitted by `@mariozechner/pi-agent` and include message updates, tool executions, and completions - `rpc`: JSON mode plus stdin listener for headless operation. Send JSON commands on stdin: `{"type":"prompt","message":"..."}` or `{"type":"abort"}`. See [test/rpc-example.ts](test/rpc-example.ts) for a complete example +**--print, -p** +Non-interactive mode: process the prompt(s) and exit. Without this flag, passing a prompt starts interactive mode with the prompt pre-submitted. Similar to Claude's `-p` flag and Codex's `exec` command. + **--no-session** Don't save session (ephemeral mode) @@ -701,10 +705,13 @@ Show help message # Start interactive mode pi -# Single message mode (text output) +# Interactive mode with initial prompt (stays running after completion) pi "List all .ts files in src/" -# JSON mode - stream all agent events +# Non-interactive mode (process prompt and exit) +pi -p "List all .ts files in src/" + +# JSON mode - stream all agent events (non-interactive) pi --mode json "List all .ts files in src/" # RPC mode - headless operation (see test/rpc-example.ts) diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 31e05f5c..d7376197 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -47,6 +47,7 @@ interface Args { noSession?: boolean; session?: string; models?: string[]; + print?: boolean; messages: string[]; } @@ -94,6 +95,8 @@ function parseArgs(args: string[]): Args { ), ); } + } else if (arg === "--print" || arg === "-p") { + result.print = true; } else if (!arg.startsWith("-")) { result.messages.push(arg); } @@ -114,6 +117,7 @@ ${chalk.bold("Options:")} --api-key API key (defaults to env vars) --system-prompt System prompt (default: coding assistant prompt) --mode Output mode: text (default), json, or rpc + --print, -p Non-interactive mode: process prompt and exit --continue, -c Continue previous session --resume, -r Select a session to resume --session Use specific session file @@ -123,13 +127,16 @@ ${chalk.bold("Options:")} --help, -h Show this help ${chalk.bold("Examples:")} - # Interactive mode (no messages = interactive TUI) + # Interactive mode pi - # Single message + # Interactive mode with initial prompt pi "List all .ts files in src/" - # Multiple messages + # Non-interactive mode (process and exit) + pi -p "List all .ts files in src/" + + # Multiple messages (interactive) pi "Read package.json" "What dependencies do we have?" # Continue previous session @@ -503,6 +510,7 @@ async function runInteractiveMode( modelFallbackMessage: string | null = null, newVersion: string | null = null, scopedModels: Array<{ model: Model; thinkingLevel: ThinkingLevel }> = [], + initialMessages: string[] = [], ): Promise { const renderer = new TuiRenderer( agent, @@ -525,6 +533,16 @@ async function runInteractiveMode( renderer.showWarning(modelFallbackMessage); } + // Process initial messages if provided (from CLI args) + for (const message of initialMessages) { + try { + await agent.prompt(message); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + renderer.showError(errorMessage); + } + } + // Interactive loop while (true) { const userInput = await renderer.getUserInput(); @@ -532,9 +550,10 @@ async function runInteractiveMode( // Process the message - agent.prompt will add user message and trigger state updates try { await agent.prompt(userInput); - } catch (error: any) { + } catch (error: unknown) { // Display error in the TUI by adding an error message to the chat - renderer.showError(error.message || "Unknown error occurred"); + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + renderer.showError(errorMessage); } } } @@ -722,7 +741,9 @@ export async function main(args: string[]) { } // Determine mode early to know if we should print messages and fail early - const isInteractive = parsed.messages.length === 0 && parsed.mode === undefined; + // Interactive mode: no --print flag and no --mode flag + // Having initial messages doesn't make it non-interactive anymore + const isInteractive = !parsed.print && parsed.mode === undefined; const mode = parsed.mode || "text"; const shouldPrintMessages = isInteractive || mode === "text"; @@ -967,7 +988,7 @@ export async function main(args: string[]) { console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)); } - // No messages and not RPC - use TUI + // Interactive mode - use TUI (may have initial messages from CLI args) await runInteractiveMode( agent, sessionManager, @@ -977,9 +998,10 @@ export async function main(args: string[]) { modelFallbackMessage, newVersion, scopedModels, + parsed.messages, ); } else { - // CLI mode with messages + // Non-interactive mode (--print flag or --mode flag) await runSingleShotMode(agent, sessionManager, parsed.messages, mode); } }