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
- Label editing with 'l' key
- Ctrl+O cycles through filters: default -> user-only -> labeled-only -> all
- Preserve cursor position when toggling filters
- Labels shown at front of node
- Normalize tabs/newlines in content
- extractContent limited to 200 chars for performance
- truncateToWidth on all rendered lines
- Iterative flattenTree to avoid stack overflow
- Linear chain optimization (no indent for single-child chains)
- Add TreeSelectorComponent with ASCII tree visualization
- Add AgentSession.navigateTree() for switching branches
- Add session_before_tree/session_tree hook events
- Add SessionManager.resetLeaf() for navigating to root
- Change leafId from string to string|null for consistency with parentId
- Support optional branch summarization when switching
- Update buildSessionContext() to handle null leafId
- Add /tree to slash commands in interactive mode
Major changes:
- Replace monolithic SessionEvent with reason discriminator with individual
event types: session_start, session_before_switch, session_switch,
session_before_new, session_new, session_before_branch, session_branch,
session_before_compact, session_compact, session_shutdown
- Each event has dedicated result type (SessionBeforeSwitchResult, etc.)
- HookHandler type now allows bare return statements (void in return type)
- HookAPI.on() has proper overloads for each event with correct typing
Additional fixes:
- AgentSession now always subscribes to agent in constructor (was only
subscribing when external subscribe() called, breaking internal handlers)
- Standardize on undefined over null throughout codebase
- HookUIContext methods return undefined instead of null
- SessionManager methods return undefined instead of null
- Simplify hook exports to 'export type * from types.js'
- Add detailed JSDoc for skipConversationRestore vs cancel
- Fix createBranchedSession to rebuild index in persist mode
- newSession() now returns the session file path
Updated all example hooks, tests, and emission sites to use new event types.
Hook commands now always execute immediately, even during streaming.
If a command needs to interact with the LLM, it uses pi.sendMessage()
which handles queueing automatically.
This simplifies the API and eliminates the issue of queued slash
commands being sent to the LLM instead of executing.
- CompactionSummaryMessageComponent now extends Box, uses customMessageBg
- New BranchSummaryMessageComponent for branch summaries
- Both use same background color as HookMessageComponent for consistency
- Added Spacer before compaction/branch components in chat
- addMessageToChat: exhaustive switch for all AgentMessage roles
- renderSessionContext: delegates to addMessageToChat, special handling for assistant tool calls and tool results
- export-html formatMessage: exhaustive switch for all AgentMessage roles
- Removed isHookMessage, isBashExecutionMessage type guards in favor of role checks
- Fixed imports and removed unused getLatestCompactionEntry
Two bugs:
1. createCustomMessage was returning role: 'user' instead of preserving
the hook message structure (role: 'hookMessage', customType, etc.)
2. rebuildChatFromMessages wasn't clearing the container before rebuilding
- Renamed AppMessage to AgentMessage throughout
- New agent-loop.ts with AgentLoopContext, AgentLoopConfig
- Removed transport abstraction, Agent now takes streamFn directly
- Extracted streamProxy to proxy.ts utility
- Removed agent-loop from pi-ai (now in agent package)
- Updated consumers (coding-agent, mom) for AgentMessage rename
- Tests updated but some consumers still need migration
Known issues:
- AgentTool, AgentToolResult not exported from pi-ai
- Attachment not exported from pi-agent-core
- ProviderTransport removed but still referenced
- messageTransformer -> convertToLlm migration incomplete
- CustomMessages declaration merging not working properly
- Rename HookAppMessage to HookMessage, isHookAppMessage to isHookMessage
- Remove entries array from SessionContext (use isHookMessage type guard instead)
- HookMessage.content now accepts string directly (not just array)
- Fix streamMessage type in AgentState (AppMessage, not Message)
- Rename CustomMessageComponent to HookMessageComponent
- Fix test hook to use pi.sendMessage
- Extract execCommand to src/core/exec.ts, shared by hooks and custom-tools
- Rename CustomMessageRenderer -> HookMessageRenderer
- Rename registerCustomMessageRenderer -> registerMessageRenderer
- Renderer now receives HookMessage instead of CustomMessageEntry
- CustomMessageComponent now has setExpanded() and responds to Ctrl+E toggle
- Re-export ExecOptions/ExecResult from exec.ts for backward compatibility
Following the same pattern as BashExecutionMessage:
- HookAppMessage has role: 'hookMessage' with customType, content, display, details
- isHookAppMessage() type guard for checking message type
- messageTransformer converts to user message for LLM context
- TUI checks isHookAppMessage() for rendering as CustomMessageComponent
This makes the API clean for anyone building on AgentSession - they can
use the type guard instead of knowing about internal marker fields.
Hook commands registered via pi.registerCommand() are now handled in
AgentSession.prompt() alongside file-based slash commands. This:
- Removes duplicate tryHandleHookCommand from interactive-mode and rpc-mode
- All modes (interactive, RPC, print) share the same command handling logic
- AgentSession._tryExecuteHookCommand() builds CommandContext using:
- UI context from hookRunner (set by mode)
- sessionManager, modelRegistry from AgentSession
- sendMessage via sendHookMessage
- exec via exported execCommand
- Handler returning string uses it as prompt, undefined returns early
Also:
- Export execCommand from hooks/runner.ts
- Add getUIContext() and getHasUI() to HookRunner
- Make HookRunner.emitError() public for error reporting
Breaking changes to Hook API:
- pi.send(text, attachments?) replaced with pi.sendMessage(message, triggerTurn?)
- Creates CustomMessageEntry instead of user messages
- Properly handles queuing during streaming via agent loop
- Supports optional turn triggering when idle
- New pi.appendEntry(customType, data?) for hook state persistence
- New pi.registerCommand(name, options) for custom slash commands
- Handler types renamed: SendHandler -> SendMessageHandler, new AppendEntryHandler
Implementation:
- AgentSession.sendHookMessage() handles all three cases:
- Streaming: queues message with _hookData marker, agent loop processes it
- Not streaming + triggerTurn: appends to state/session, calls agent.continue()
- Not streaming + no trigger: appends to state/session only
- message_end handler routes based on _hookData presence to correct persistence
- HookRunner gains getRegisteredCommands() and getCommand() methods
New types: HookMessage<T>, RegisteredCommand, CommandContext
- CustomMessageComponent accepts optional CustomMessageRenderer
- If hook provides a renderer, call it and use returned Component inside Box
- Falls back to default rendering (label + Markdown) if no renderer or null returned
- renderSessionContext gets renderer from hookRunner and passes to component
Use OSC 8 hyperlink escape sequence to show 'Click here to login'
as a clickable link instead of displaying the raw URL which spans
multiple lines and is hard to click in terminals (especially WSL).
- Add --session-dir CLI flag to specify custom session directory
- SessionManager API: second param of create(), continueRecent(), list(), open()
changed from agentDir to sessionDir (direct directory, no cwd encoding)
- When omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/)
- --session now derives sessionDir from file's parent if --session-dir not provided
- list() validates session header before processing files
- Closes#313
Co-authored-by: scutifer <scutifer@users.noreply.github.com>