Commit Graph

104 Commits

  • Extensions: add pi.sendUserMessage() for sending user messages
    Adds sendUserMessage() to the extension API, allowing extensions to send
    actual user messages (role: user) rather than custom messages. Unlike
    sendMessage(), this always triggers a turn and behaves as if the user
    typed the message.
    
    - Add SendUserMessageHandler type and sendUserMessage() to ExtensionAPI
    - Wire handler through loader, runner, and all modes
    - Implement via prompt() with expandPromptTemplates: false
    - Add send-user-message.ts example with /ask, /steer, /followup commands
    - Document in extensions.md
    
    fixes #483
  • feat(ai,agent,coding-agent): add sessionId for provider session-based caching
    - Add sessionId to StreamOptions for providers that support session-based caching
    - OpenAI Codex provider uses sessionId for prompt_cache_key and routing headers
    - Agent class now accepts and forwards sessionId to stream functions
    - coding-agent passes session ID from SessionManager and updates on session changes
    - Update ai package README with table of contents, OpenAI Codex OAuth docs, and env vars table
    - Increase Codex instructions cache TTL from 15 minutes to 24 hours
    - Add tests for sessionId forwarding in ai and agent packages
  • fix: clean up Codex thinking level handling
    - Remove per-thinking-level model variants (gpt-5.2-codex-high, etc.)
    - Remove thinkingLevels from Model type
    - Provider clamps reasoning effort internally
    - Omit reasoning field when thinking is off
    
    fixes #472
  • Merge hooks and custom-tools into unified extensions system (#454)
    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
  • Fix event bus async error handling, clear pending messages on session switch, improve SDK docs
    - event-bus.ts: await async handlers to catch errors properly
    - agent-session.ts: clear _pendingNextTurnMessages on newSession/switchSession/branch
    - sdk.ts: make eventBus first (required) param for discoverHooks/discoverCustomTools
    - docs/sdk.md: document eventBus sharing pattern for hook/tool communication
  • feat(coding-agent): add event bus for tool/hook communication (#431)
    * feat(coding-agent): add event bus for tool/hook communication
    
    Adds pi.events API enabling custom tools and hooks to communicate via
    pub/sub. Tools can emit events, hooks can listen. Shared EventBus instance
    created per session in createAgentSession().
    
    - EventBus interface with emit() and on() methods
    - on() returns unsubscribe function
    - Threaded through hook and tool loaders
    - Documented in hooks.md and custom-tools.md
    
    * fix(coding-agent): wrap event handlers to catch errors
    
    * docs: note async handler error handling for event bus
    
    * feat(coding-agent): add sendMessage to tools, nextTurn delivery mode
    
    - Custom tools now have pi.sendMessage() for direct agent notifications
    - New deliverAs: 'nextTurn' queues messages for next user prompt
    - Fix: hooks and tools now share the same eventBus (was isolated before)
    
    * fix(coding-agent): nextTurn delivery should always queue, even when streaming
  • feat(hooks): add systemPromptAppend to before_agent_start, full tool registry
    - before_agent_start handlers can return systemPromptAppend to dynamically
      append text to the system prompt for that turn
    - Multiple hooks' systemPromptAppend strings are concatenated
    - Multiple hooks' messages are now all injected (not just first)
    - Tool registry now contains ALL built-in tools (read, bash, edit, write,
      grep, find, ls) regardless of --tools flag
    - --tools only sets initially active tools, hooks can enable any via
      setActiveTools()
    - System prompt automatically rebuilds when tools change, updating tool
      descriptions and guidelines
    - Add pirate.ts example hook demonstrating systemPromptAppend
    - Update hooks.md with systemPromptAppend documentation
  • refactor(hooks): address PR feedback
    - Rename getTools/setTools to getActiveTools/setActiveTools
    - Add getAllTools to enumerate all configured tools
    - Remove text_delta event (use turn_end/agent_end instead)
    - Add shortcut conflict detection:
      - Skip shortcuts that conflict with built-in shortcuts (with warning)
      - Log warnings when hooks register same shortcut (last wins)
    - Add note about prompt cache invalidation in setActiveTools
    - Update plan-mode hook to use agent_end for [DONE:id] parsing
  • feat(hooks): add text_delta event for streaming text monitoring
    - New text_delta hook event fires for each chunk of streaming text
    - Enables real-time monitoring of agent output
    - Plan-mode hook now updates todo progress as [DONE:id] tags stream in
    - Each todo item has unique ID for reliable tracking
  • WIP: Add hook API for dynamic tool control with plan-mode hook example
    - 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 slash commands and hook commands during streaming
    - 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
  • Add shell commands without context contribution (!! prefix)
    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
  • feat(coding-agent): update AgentSession for steer()/followUp() API
    - 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 guard against concurrent prompt() calls
    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 agent state methods to CustomToolContext and fix abort signature
    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
  • Split HookContext and HookCommandContext to prevent deadlocks
    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).
  • Add session management and agent state methods to hooks API
    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
  • Consolidate session events: remove session_before_new/session_new, add reason field to switch events
    - 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
  • WIP: Rewrite export-html with tree sidebar, client-side rendering
    - Add tree sidebar with search and filter (Default/All/Labels)
    - Client-side markdown/syntax highlighting via vendored marked.js + highlight.js
    - Base64 encode session data to avoid escaping issues
    - Reuse theme.ts color tokens via getResolvedThemeColors()
    - Sticky sidebar, responsive mobile layout with overlay
    - Click tree node to scroll to message
    - Keyboard shortcuts: Esc to reset, Ctrl/Cmd+F to search
  • Change branch() to use entryId instead of entryIndex
    - AgentSession.branch(entryId: string) now takes entry ID
    - SessionBeforeBranchEvent.entryId replaces entryIndex
    - getUserMessagesForBranching() returns entryId
    - Update RPC types and client
    - Update UserMessageSelectorComponent
    - Update hook examples and tests
    - Update docs (hooks.md, sdk.md)
  • Rework custom tools API with CustomToolContext
    - 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
  • Replace custom tool dispose() with shutdown session event
    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
  • refactor(coding-agent): fix compaction for branched sessions, consolidate hook context types
    Compaction API:
    - prepareCompaction() now takes (pathEntries, settings) only
    - CompactionPreparation restructured: removed cutPoint/messagesToKeep/boundaryStart, added turnPrefixMessages/isSplitTurn/previousSummary/fileOps/settings
    - compact() now takes (preparation, model, apiKey, customInstructions?, signal?)
    - Fixed token overflow by using getPath() instead of getEntries()
    
    Hook types:
    - HookEventContext renamed to HookContext
    - HookCommandContext removed, RegisteredCommand.handler takes (args, ctx)
    - HookContext now includes model field
    - SessionBeforeCompactEvent: removed previousCompactions/model, added branchEntries
    - SessionBeforeTreeEvent: removed model (use ctx.model)
    - HookRunner.initialize() added for modes to set up callbacks
  • Add fromHook field to CompactionEntry and BranchSummaryEntry
    - fromHook: true = hook generated, skip file extraction
    - fromHook: undefined/false = pi generated, extract files (backward compatible)
    - branchWithSummary now accepts fromHook parameter
    - File extraction only runs for !entry.fromHook entries
  • Store file lists in BranchSummaryEntry.details for cumulative tracking
    - BranchSummaryResult now returns readFiles and modifiedFiles separately
    - BranchSummaryDetails type for details: { readFiles, modifiedFiles }
    - branchWithSummary accepts optional details parameter
    - Collect files from existing branch_summary.details when preparing entries
    - Files accumulate across nested branch summaries
  • Use reserveTokens for branch summary (tokens left for prompt + response)
    - tokenBudget = contextWindow - reserveTokens
    - Default 16384, same as compaction
    - Consistent naming with CompactionSettings.reserveTokens
  • Use token-based maxTokens instead of fraction-based reserveFraction
    - BranchSummarySettings.maxTokens (default 100000) instead of reserveFraction
    - More intuitive and consistent with CompactionSettings.keepRecentTokens
  • Use AgentMessage in BranchPreparation and add BranchSummarySettings
    - BranchPreparation now uses AgentMessage[] instead of custom type
    - Reuse getMessageFromEntry pattern from compaction.ts
    - Add BranchSummarySettings with reserveFraction to settings.json
    - Add getBranchSummarySettings() to SettingsManager
    - Use settings for reserveFraction instead of hardcoded value
  • Add ReadonlySessionManager and refactor branch summarization
    - Add ReadonlySessionManager interface to session-manager.ts
    - Re-export from hooks/index.ts
    - Add collectEntriesForBranchSummary() to extract entries for summarization
    - Don't stop at compaction boundaries (include their summaries as context)
    - Add token budget support to prepareBranchEntries()
    - Walk entries newest-to-oldest to prioritize recent context
    - Use options object for generateBranchSummary()
    - Handle compaction entries as context summaries
    - Export new types: CollectEntriesResult, GenerateBranchSummaryOptions
  • Refactor: move compaction code to src/core/compaction/
    - Move compaction.ts to src/core/compaction/compaction.ts
    - Extract branch summarization to src/core/compaction/branch-summarization.ts
    - Add index.ts to re-export all compaction utilities
    - Update all imports across the codebase
  • Add tree navigation tests and shared test utilities
    - Add test/utilities.ts with shared helpers (API_KEY, userMsg, assistantMsg, createTestSession)
    - Add agent-session-tree-navigation.test.ts with e2e tests for tree navigation
    - Add getChildren() method to SessionManager
    - Add summaryEntry to navigateTree return type
    - Update existing tests to use shared utilities
  • Fix branch summarization abort handling and tree navigation
    - 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
  • feat(coding-agent): implement /tree command for session tree navigation
    - 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
  • refactor(hooks): split session events into individual typed events
    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.
  • Support multiple messages in agent.prompt() and agentLoop
    - agentLoop now accepts AgentMessage[] instead of single message
    - agent.prompt() accepts AgentMessage | AgentMessage[]
    - Emits message_start/end for each message in the array
    - AgentSession.prompt() builds array with hook message + user message
    - TUI now receives events for before_agent_start injected messages
  • Implement before_agent_start hook event
    - Add BeforeAgentStartEvent and BeforeAgentStartEventResult types
    - Add emitBeforeAgentStart to HookRunner
    - Call in AgentSession.prompt() before agent.prompt()
    - Hook can return a message to inject into context (persisted + visible)
    - Add test hook demonstrating custom message rendering and before_agent_start
  • Use exhaustive switch on message.role throughout coding-agent
    - 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
  • WIP: Major cleanup - move Attachment to consumers, simplify agent API
    - Removed Attachment from agent package (now in web-ui/coding-agent)
    - Agent.prompt now takes (text, images?: ImageContent[])
    - Removed transports from web-ui (duplicate of agent package)
    - Updated coding-agent to use local message types
    - Updated mom package for new agent API
    
    Remaining: Fix AgentInterface.ts to compose UserMessageWithAttachments
  • WIP: Refactor agent package - not compiling
    - 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
  • Cleanup: unify HookMessage naming and simplify SessionContext
    - 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
  • Move exec to HookAPI, sessionManager/modelRegistry to HookEventContext
    Breaking changes:
    - HookEventContext now has sessionManager and modelRegistry (moved from SessionEventBase)
    - HookAPI now has exec() method (moved from HookEventContext/HookCommandContext)
    - HookRunner constructor takes sessionManager and modelRegistry as required params
    - Session events no longer include sessionManager/modelRegistry fields
    
    Hook code migration:
    - event.sessionManager -> ctx.sessionManager
    - event.modelRegistry -> ctx.modelRegistry
    - ctx.exec() -> pi.exec()
    
    Updated:
    - src/core/hooks/types.ts - type changes
    - src/core/hooks/runner.ts - constructor, createContext
    - src/core/hooks/loader.ts - add exec to HookAPI
    - src/core/sdk.ts - pass sessionManager/modelRegistry to HookRunner
    - src/core/agent-session.ts - remove sessionManager/modelRegistry from events
    - src/modes/* - remove setSessionFile calls, update events
    - examples/hooks/* - update to new API
  • Add Agent.prompt(AppMessage) overload for custom message types
    Instead of using continue() which validates roles, prompt() now accepts
    an AppMessage directly. This allows hook messages with role: 'hookMessage'
    to trigger proper agent loop with message events.
    
    - Add overloads: prompt(AppMessage) and prompt(string, attachments?)
    - sendHookMessage uses prompt(appMessage) instead of appendMessage+continue
  • Use proper HookAppMessage type instead of _hookData marker
    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.
  • Add emitLastMessage flag to agent.continue()
    When calling continue() with emitLastMessage=true, the agent loop
    emits message_start/message_end events for the last message in context.
    This allows messages added outside the loop (e.g., hook messages via
    sendHookMessage) to trigger proper TUI rendering.
    
    Changes across packages:
    - packages/ai: agentLoopContinue() accepts emitLastMessage parameter
    - packages/agent: Agent.continue(), transports updated to pass flag
    - packages/coding-agent: sendHookMessage passes true when triggerTurn
  • Hook commands: remove string return, use sendMessage() for prompting
    - Command handler now returns Promise<void> instead of Promise<string | undefined>
    - To trigger LLM response, use sendMessage() with triggerTurn: true
    - Simplify _tryExecuteHookCommand to return boolean
    
    Added example hook and slash command in .pi/:
    - .pi/hooks/test-command.ts - /greet command using sendMessage
    - .pi/commands/review.md - file-based /review command
  • Move hook command execution to AgentSession.prompt()
    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