Commit Graph

767 Commits

  • 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.
  • Add ReadonlySessionManager type for hooks
    Hooks now receive ReadonlySessionManager in contexts, which only
    exposes read methods. Writes must go through pi.sendMessage() or
    pi.appendEntry().
  • Fix nested background issue in HookMessageComponent
    When custom renderer returns a component with its own background,
    don't wrap it in another background box. The nested bg resets
    were causing padding to lose its background color.
  • 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
  • Remove allowDuringStreaming flag - commands always run immediately
    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.
  • 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
  • Add reference to #330 (Dynamic Context Pruning) in plan
    Documents why context event was added and notes the type
    inconsistency between ContextEvent (AgentMessage[]) and
    ContextEventResult (Message[])
  • Add investigation item for context event vs before_agent_start
    Reference: #324
    
    Documents:
    - Current context event behavior (AgentMessage[], transient)
    - Proposed before_agent_start event (persistent, TUI visible)
    - Key differences table
    - Open design questions
    - Need to verify AgentMessage vs Message abstraction level
  • Add BranchSummaryMessageComponent, unify styling with hook messages
    - 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
  • 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
  • Fix hook message duplication in TUI
    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
  • 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
  • Fix messageTransformer to normalize HookMessage string content to array
    HookMessage.content can be string or array, but LLM Message.content
    must be an array. This was causing 'messages: at least one message
    is required' errors when hooks sent string content.
  • 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
  • Wire context event to preprocessor for per-LLM-call execution
    - Change from contextTransform (runs once at agent start) to preprocessor
    - preprocessor runs before EACH LLM call inside the agent loop
    - ContextEvent now uses Message[] (pi-ai format) instead of AppMessage[]
    - Deep copy handled by pi-ai preprocessor, not Agent
    
    This enables:
    - Pruning rules applied on every turn (not just agent start)
    - /prune during long agent loop takes effect immediately
    - Compaction can use same transforms (future work)
  • Add context event for non-destructive message modification before LLM calls
    - Add contextTransform option to Agent (runs before messageTransformer)
    - Deep copy messages before passing to contextTransform (modifications are ephemeral)
    - Add ContextEvent and ContextEventResult types
    - Add emitContext() to HookRunner (chains multiple handlers)
    - Wire up in sdk.ts when creating Agent with hooks
    
    Enables dynamic context pruning: hooks can modify messages sent to LLM
    without changing session data. See discussion #330.
  • Snake: add full border frame and pause/resume with session persistence
    - Full box border around title, game area, and instructions
    - ESC pauses and saves state to session via pi.appendEntry()
    - Resume shows 'PAUSED - press any key to continue'
    - Q quits and clears saved state
    - High score persists across games
  • Fix snake velocity: render cells as 2 chars wide for square aspect
    Terminal cells are ~2:1 aspect ratio, so movement appeared faster
    vertically. Now each game cell is 2 characters wide.
  • Improve snake game visuals
    - Rounded box corners (╭╮╰╯)
    - Better characters: ● head, ○ body, ◆ food, · empty
    - Colored title with emoji
    - Dimmed borders and help text
    - Bold highlights for score and controls
  • Add immediate flag to hook commands for non-queued execution
    Commands with immediate: true run right away even during streaming.
    Used for UI-only commands like /snake that don't interact with LLM.
  • Add ui.custom() for custom hook components with keyboard focus
    - Add custom() to HookUIContext: returns { close, requestRender }
    - Component receives keyboard input via handleInput()
    - CustomMessageComponent default rendering now limits to 5 lines when collapsed
    - Add snake.ts example hook with /snake command
  • Refactor: shared exec utility, rename CustomMessageRenderer to HookMessageRenderer
    - 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
  • Fix tests for sessionManager/modelRegistry on context
    - compaction-hooks-example.test.ts: get sessionManager/modelRegistry from ctx
    - compaction-hooks.test.ts:
      - Pass sessionManager/modelRegistry to HookRunner constructor
      - Remove setSessionFile call
      - Update tests to use session.sessionManager instead of event.sessionManager
  • 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
  • Update plan: move exec to HookAPI, sessionManager/modelRegistry to contexts
    - exec() moves from HookEventContext/HookCommandContext to HookAPI
    - sessionManager/modelRegistry move from SessionEventBase to HookEventContext
    - HookCommandContext keeps sessionManager/modelRegistry (command handlers need them)
    - Both sendMessage and exec accessed via pi closure in command handlers
  • 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
  • Hook API: replace send() with sendMessage(), add appendEntry() and registerCommand()
    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
  • Wire up hook custom message renderers to TUI
    - 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