Previously, users had to wait up to 60 seconds for the browser callback
to timeout before being prompted to paste the authorization code. This
was problematic for SSH/VPS sessions where the callback cannot work.
Now the paste input is shown immediately alongside the browser flow:
- Browser callback and manual paste race - whichever completes first wins
- Desktop users: browser callback succeeds, input is cleaned up
- SSH/VPS users: paste code immediately without waiting
Changes:
- Add cancelWait() to OAuth server for early termination of polling loop
- Add onManualCodeInput callback that races with browser callback
- Show paste input immediately in TUI for openai-codex provider
- Clean up input on success, error, or when browser callback wins
Co-authored-by: cc-vps <crcatala+vps@gmail.com>
Breaking changes:
- Settings: 'hooks' and 'customTools' arrays replaced with 'extensions'
- CLI: '--hook' and '--tool' flags replaced with '--extension' / '-e'
- API: HookMessage renamed to CustomMessage, role 'hookMessage' to 'custom'
- API: FileSlashCommand renamed to PromptTemplate
- API: discoverSlashCommands() renamed to discoverPromptTemplates()
- Directories: commands/ renamed to prompts/ for prompt templates
Migration:
- Session version bumped to 3 (auto-migrates v2 sessions)
- Old 'hookMessage' role entries converted to 'custom'
Structural changes:
- src/core/hooks/ and src/core/custom-tools/ merged into src/core/extensions/
- src/core/slash-commands.ts renamed to src/core/prompt-templates.ts
- examples/hooks/ and examples/custom-tools/ merged into examples/extensions/
- docs/hooks.md and docs/custom-tools.md merged into docs/extensions.md
New test coverage:
- test/extensions-runner.test.ts (10 tests)
- test/extensions-discovery.test.ts (26 tests)
- test/prompt-templates.test.ts
- HookError now includes optional stack field
- Hook error display shows stack trace in dim color below error message
- tools.ts: create SettingsListTheme using the theme passed to ctx.ui.custom()
instead of using getSettingsListTheme() which depends on global theme
1. Merge setWidget and setWidgetComponent into single overloaded method
- Accepts either string[] or component factory function
- Uses single Map<string, Component> internally
- String arrays wrapped in Container with Text components
2. Use KeyId type for registerShortcut instead of plain string
- Import Key from @mariozechner/pi-tui
- Update plan-mode example to use Key.shift('p')
- Type-safe shortcut registration
3. Fix tool API docs
- Both built-in and custom tools can be enabled/disabled
- Removed incorrect 'custom tools always active' statement
4. Use matchesKey instead of matchShortcut (already done in rebase)
- New ctx.ui.setWidgetComponent(key, factory) method
- Allows custom Component to render as widget without taking focus
- Unlike custom(), widget components render inline above editor
- Components are disposed when cleared or replaced
- Falls back to no-op in RPC/print modes
- Limit total widget lines to 10 to prevent viewport overflow/flicker
- Show '... (widget truncated)' when limit exceeded
- Document that multiple hooks stack widgets vertically
- Add caution about keeping widgets small
- ctx.ui.setWidget(key, lines) for multi-line displays above editor
- Widgets appear below 'Working...' indicator, above editor
- Supports ANSI styling including strikethrough
- Added theme.strikethrough() method
- Plan-mode hook now shows todo list with checkboxes
- Completed items show checked box and strikethrough text
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
* fix(tui): expand paste markers when opening external editor
Add getExpandedText() method to Editor that substitutes paste markers
with actual content. Use it in Ctrl-G external editor flow so users
see full pasted content instead of [paste #N ...] placeholders.
* docs: add changelog entries for #444
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.
- 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
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
- 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
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>"
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
HookAPI additions:
- pi.newSession(options?) - create new session with optional setup callback
- pi.branch(entryId) - branch from a specific entry
- pi.navigateTree(targetId, options?) - navigate the session tree
HookContext additions:
- ctx.isIdle() - check if agent is streaming
- ctx.waitForIdle() - wait for agent to finish
- ctx.abort() - abort current operation
- ctx.hasQueuedMessages() - check for queued user messages
These enable hooks to programmatically manage sessions (handoff, templates)
and check agent state before showing interactive UI.
Fixes#388
- Remove session_before_new and session_new hook events
- Add reason: 'new' | 'resume' to session_before_switch and session_switch events
- Remove 'new' reason from custom tool onSession (use 'switch' for both /new and /resume)
- Rename reset() to newSession(options?) in AgentSession
- Add NewSessionOptions with optional parentSession for lineage tracking
- Rename branchedFrom to parentSession in SessionHeader
- Rename RPC reset command to new_session with optional parentSession
- Update example hooks to use new event structure
- Update documentation and changelog
Based on discussion in #293
- Add theme property to HookUIContext interface
- Implement in interactive, RPC, and no-op contexts
- Add status-line.ts example hook
- Document styling with theme colors in hooks.md
* Add ctx.ui.setStatus(key, text) API for hooks to display status in footer
- Add setStatus to HookUIContext interface
- Implement in interactive mode (FooterComponent)
- Implement in RPC mode (fire-and-forget)
- Add no-op implementations for headless contexts
- Multiple statuses displayed on single line, sorted by key
- Supports ANSI styling (hooks handle their own colors)
* Remove setStatus from changelog for now
* Fix hook status API to follow TUI rules
- Sanitize status text: replace newlines, tabs, carriage returns with spaces
- Truncate combined status line to terminal width using truncateToWidth
- Update JSDoc to document sanitization and truncation behavior
- Remove unused createHookUIContext method
- Add missing setStatus to test mock
* Add setStatus to changelog
* Use dim ellipsis for hook status truncation for consistency with footer style
---------
Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
- Use BorderedLoader for /share command so Escape cancels gist creation
- Add CHANGELOG entries for /share command and HTML export improvements
- Add todo.md with remaining export-html issues
- Split template into separate files: template.html, template.css, template.js
- Add tree visualization sidebar for session navigation
- Fix HTML sanitization to prevent <style> tags breaking DOM
- Add DOM node caching for faster re-renders
- Fix tree indentation to match tree-selector.ts
- Add /share command to upload session as GitHub gist
- Support shittycodingagent.ai/session?{gistId} URLs
Closes#375, closes#380
- Extract diff computation from edit.ts into shared edit-diff.ts
- ToolExecutionComponent computes and caches diff when args are complete
- Diff is visible while permission hooks block, before tool executes
- Add configurable thinkingText color for thinking blocks (defaults to muted)
- Make 'Thinking...' label italic when collapsed
- Fix Ctrl+T during streaming hiding the current message
- Track streamingMessage to properly re-render on toggle
Based on #366 by @paulbettner
- Add setEditorText() and getEditorText() to HookUIContext for prompt generator pattern
- custom() now accepts async factories for fire-and-forget work
- Add CancellableLoader component to tui package
- Add BorderedLoader component for hooks with cancel UI
- Export HookAPI, HookContext, HookFactory from main package
- Update all examples to import from packages instead of relative paths
- Update hooks.md and custom-tools.md documentation
fixes#350
- CustomAgentTool renamed to CustomTool
- ToolAPI renamed to CustomToolAPI
- ToolContext renamed to CustomToolContext
- ToolSessionEvent renamed to CustomToolSessionEvent
- Added CustomToolContext parameter to execute() and onSession()
- CustomToolFactory now returns CustomTool<any, any> for type compatibility
- dispose() replaced with onSession({ reason: 'shutdown' })
- Added wrapCustomTool() to convert CustomTool to AgentTool
- Session exposes setToolUIContext() instead of leaking internals
- Fix ToolExecutionComponent to sync with toolOutputExpanded state
- Update all custom tool examples for new API
Breaking change: CustomAgentTool.dispose() removed. Use onSession with
reason 'shutdown' instead for cleanup.
- Add 'shutdown' to SessionEvent.reason for custom tools
- Remove dispose() method from CustomAgentTool interface
- Make emitToolSessionEvent() public on AgentSession
- Emit shutdown event to tools in InteractiveMode.shutdown()
- Update custom-tools.md with new API and examples
Both auto_compaction_end and executeCompaction were manually adding
CompactionSummaryMessageComponent instead of using addMessageToChat,
which caused missing spacers. Now both use addMessageToChat for
consistent spacing and expansion state handling.
- Check response.stopReason instead of catching errors for abort detection
- Return result object from _generateBranchSummary instead of throwing
- Fix summary attachment: attach to navigation target position, not old branch
- Support root-level summaries (parentId=null) in branchWithSummary
- Remove setTimeout hack for re-showing tree selector on abort