Hook API additions:
- pi.getTools() / pi.setTools(toolNames) - dynamically enable/disable tools
- pi.registerFlag(name, options) / pi.getFlag(name) - register custom CLI flags
- pi.registerShortcut(shortcut, options) - register keyboard shortcuts
Plan mode hook (examples/hooks/plan-mode.ts):
- /plan command or Shift+P shortcut to toggle
- --plan CLI flag to start in plan mode
- Read-only tools: read, bash, grep, find, ls
- Bash restricted to non-destructive commands (blocks rm, mv, git commit, etc.)
- Interactive prompt after each response: execute, stay, or refine
- Shows plan indicator in footer when active
- State persists across sessions
- Add pi.getTools() and pi.setTools(toolNames) to HookAPI
- Hooks can now enable/disable tools dynamically
- Changes take effect on next agent turn
New example hook: plan-mode.ts
- Claude Code-style read-only exploration mode
- /plan command toggles plan mode on/off
- Plan mode tools: read, bash, grep, find, ls
- Edit/write tools disabled in plan mode
- Injects context telling agent about restrictions
- After each response, prompts to execute/stay/refine
- State persists across sessions
Add new slash commands for gracefully exiting the interactive mode:
- /quit - exit the application
- /exit - alias for /quit (common CLI convention)
Both commands await shutdown() which emits session_shutdown events
to hooks and custom tools before exiting. Unlike Ctrl+C (twice) which
uses void (fire-and-forget), these commands properly wait for hooks
and tools to complete their shutdown handlers.
JPEG/GIF/WebP images were not rendering in terminals using the Kitty
graphics protocol (Kitty, Ghostty, WezTerm) because it requires PNG
format (f=100). Non-PNG images are now converted to PNG using sharp
before being sent to the terminal.
- Hook commands now execute immediately during streaming (they manage their own LLM interaction via pi.sendMessage())
- File-based slash commands are expanded and queued via steer/followUp during streaming
- prompt() accepts new streamingBehavior option ('steer' or 'followUp') for explicit queueing during streaming
- steer() and followUp() now expand file-based slash commands and error on hook commands
- RPC prompt command accepts optional streamingBehavior field
- Updated docs: rpc.md, sdk.md, CHANGELOG.md
fixes#420
The async diff preview computation could race with tool execution,
causing editDiffPreview to contain an error (file already modified)
while the actual tool result had the correct diff in details.diff.
Fix: prioritize result.details.diff over editDiffPreview when the
tool has executed successfully. The preview is only used before
tool execution completes.
Use !!command to execute bash commands that are shown in the TUI and
saved to session history but excluded from LLM context, compaction
summaries, and branch summaries.
- Add excludeFromContext field to BashExecutionMessage
- Filter excluded messages in convertToLlm()
- Parse !! prefix in interactive mode
- Use dim border color for excluded commands
fixes#414
Add doubleEscapeAction setting to choose whether double-escape with an
empty editor opens /tree (default) or /branch.
- Add setting to Settings interface and SettingsManager
- Add to /settings UI for easy toggling
- Update interactive-mode to respect the setting
- Document in README.md settings table
fixes#404
getAvailable() now uses hasAuth() which checks if auth is configured
without triggering OAuth token refresh. Refresh happens later when
the model is actually used.
- Implement google-vertex provider in packages/ai
- Support ADC (Application Default Credentials) via @google/generative-ai
- Add Gemini model catalog for Vertex AI
- Update packages/coding-agent to handle google-vertex provider
Builds on #406 to support simpler proxy use case:
- Override just baseUrl to route built-in provider through proxy
- All built-in models preserved, no need to redefine them
- Full replacement still works when models array is provided
* Allow models.json to override built-in providers
When a provider is defined in models.json with the same name as a
built-in provider (e.g., 'anthropic', 'google'), the built-in models
for that provider are completely replaced by the custom definition.
This enables users to:
- Use custom base URLs (proxies, self-hosted endpoints)
- Define a subset of models they want available
- Customize model configurations for built-in providers
Example usage in ~/.pi/agent/models.json:
{
"providers": {
"anthropic": {
"baseUrl": "https://my-proxy.example.com/v1",
"apiKey": "ANTHROPIC_API_KEY",
"api": "anthropic-messages",
"models": [...]
}
}
}
* Refactor model-registry for readability
- Extract CustomModelsResult type and emptyCustomModelsResult helper
- Extract loadBuiltInModels method with clear skip logic
- Simplify loadModels with destructuring and ternary
- Reduce repetition in error handling paths
* Refactor model-registry tests for readability
- Extract providerConfig() helper to hide irrelevant model fields
- Extract writeModelsJson() helper for file writing
- Extract getModelsForProvider() helper for filtering
- Move modelsJsonPath to beforeEach
Reduces test file from 262 to 130 lines while maintaining same coverage.
- Fix settings-selector descriptions to explain one-at-a-time vs all
- Update README.md message queuing section, settings example, and table
- Update hooks.md: hasPendingMessages, sendMessage options, triggerTurn example
- Add Theme/ThemeColor export and hasPendingMessages rename to CHANGELOG
- pi.sendMessage(msg, options?) now accepts { triggerTurn?, deliverAs? }
- deliverAs: 'steer' (default) or 'followUp' controls delivery timing
- Update all mode handlers to pass options through
- Update file-trigger example to use new API
- Update CHANGELOG
- Update settings-manager with steeringMode/followUpMode (migrates old queueMode)
- Update sdk.ts to use new mode options
- Update settings-selector UI to show both modes
- Add Alt+Enter keybind for follow-up messages
- Update RPC API: steer/follow_up commands, set_steering_mode/set_follow_up_mode
- Update rpc-client with new methods
- Delete dead code: queue-mode-selector.ts
- Update tests for new API
- Update mom/context.ts stubs
- Update web-ui example
- Rename queueMessage to steer(), add followUp()
- Split _pendingMessages into _steeringMessages and _followUpMessages
- Update sendHookMessage to accept deliverAs option
- Rename hasQueuedMessages to hasPendingMessages
- Rename queuedMessageCount to pendingMessageCount
- Update clearQueue() return type to { steering, followUp }
- Update UI to show steering vs follow-up messages differently
WIP: settings-manager, sdk, interactive-mode, rpc-mode still need updates
Agent.prompt() and Agent.continue() now throw if called while already
streaming, preventing race conditions and corrupted state. Use
queueMessage() to queue messages during streaming, or await the
previous call.
AgentSession.prompt() has the same guard with a message directing
users to queueMessage().
Ref #403
Add setTitle() method to Terminal interface for setting window title.
Uses standard OSC escape sequence \x1b]0;...\x07 for broad terminal
compatibility (macOS Terminal, iTerm2, Kitty, WezTerm, Ghostty, etc.).
Changes:
- Add setTitle(title: string) to Terminal interface
- Implement in ProcessTerminal using OSC sequence
- Implement no-op in VirtualTerminal for testing
- Use in interactive mode to set title as "pi - <dirname>"
BorderedLoader receives theme as a parameter but was creating
DynamicBorder() without passing it, causing DynamicBorder to
fall back to the global theme variable which is undefined when
loaded via jiti (separate module cache).
Added doc comment to DynamicBorder explaining the jiti issue.
Move line count from header to footer to avoid changing the first line
during streaming, which was triggering full screen re-renders in the
TUI's differential rendering logic.
CustomToolContext now has:
- isIdle() - check if agent is streaming
- hasQueuedMessages() - check if user has queued messages
- abort() - abort current operation (fire-and-forget)
Changed abort() signature from Promise<void> to void in both
HookContext and CustomToolContext. The abort is fire-and-forget:
it calls session.abort() without awaiting, so the abort signal
is set immediately while waitForIdle() runs in the background.
Fixes#388
HookContext (all events):
- isIdle() - read-only state check
- hasQueuedMessages() - read-only state check
- abort() - fire-and-forget, does not wait
HookCommandContext (slash commands only):
- waitForIdle() - waits for agent to finish
- newSession(options?) - create new session
- branch(entryId) - branch from entry
- navigateTree(targetId, options?) - navigate session tree
Session control methods moved from HookAPI (pi.*) to HookCommandContext (ctx.*)
because they can deadlock when called from event handlers that run inside
the agent loop (tool_call, tool_result, context events).