Commit Graph

750 Commits

  • 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
  • Add TUI rendering for CustomMessageEntry
    - Add CustomMessageComponent with purple-tinted styling
    - Add theme colors: customMessageBg, customMessageText, customMessageLabel
    - Rename renderMessages to renderSessionContext taking SessionContext directly
    - renderInitialMessages now gets context from sessionManager
    - Skip rendering for display: false entries
  • Fix: compaction now handles branch_summary and custom_message entries
    - Add getMessageFromEntry helper to extract AppMessage from any context-producing entry
    - Update findValidCutPoints to treat branch_summary/custom_message as valid cut points
    - Update findTurnStartIndex to recognize branch_summary/custom_message as turn starters
    - Update all message extraction loops to use getMessageFromEntry
  • Add CustomMessageEntry rendering infrastructure
    - Add renderCustomMessage to HookAPI for registering custom renderers
    - Add CustomMessageRenderer type and CustomMessageRenderOptions
    - Store customMessageRenderers in LoadedHook
    - Add getCustomMessageRenderer(customType) to HookRunner
    - SessionContext.entries now aligned with messages (same length, corresponding indices)
    
    TUI can now correlate messages with their source entries to identify
    custom_message entries and use hook-provided renderers.
  • Fix CustomMessageEntry content type to match UserMessage
    content: string | (TextContent | ImageContent)[]
    
    This matches the UserMessage type from pi-ai, so content can be
    passed directly to AppMessage without conversion.
  • Add CustomMessageEntry for hook-injected messages in LLM context
    - CustomMessageEntry<T> type with customType, content, display, details
    - appendCustomMessageEntry() in SessionManager
    - buildSessionContext() includes custom_message entries as user messages
    - Exported CustomEntry and CustomMessageEntry from main index
    
    CustomEntry is for hook state (not in context).
    CustomMessageEntry is for hook-injected content (in context).
  • Refactor SessionEventBase to pass sessionManager and modelRegistry
    Breaking changes to hook types:
    - SessionEventBase now passes sessionManager and modelRegistry directly
    - before_compact: passes preparation, previousCompactions (newest first)
    - before_switch: has targetSessionFile; switch: has previousSessionFile
    - Removed resolveApiKey (use modelRegistry.getApiKey())
    - getSessionFile() returns string | undefined for in-memory sessions
    
    Updated:
    - All session event emissions in agent-session.ts
    - Hook examples (custom-compaction.ts, auto-commit-on-exit.ts, confirm-destructive.ts)
    - Tests (compaction-hooks.test.ts, compaction-hooks-example.test.ts)
    - export-html.ts guards for in-memory sessions
  • Make CompactionEntry and CompactionResult generic with details field
    - CompactionEntry<T> and CompactionResult<T> now have optional details?: T
    - appendCompaction() accepts optional details parameter
    - Hooks can return compaction.details to store custom data
    - Enables structured compaction with ArtifactIndex (see #314)
    - Fix CompactionResult export location (now from compaction.ts)
    - Update plan with remaining compaction refactor items
  • Improve CustomEntry docs and make it generic
    - Add detailed doc comment explaining purpose (hook state persistence)
    - Make CustomEntry<T = unknown> generic
    - Clarify difference from CustomMessageEntry in plan
    - Update changelog
  • Use CompactionResult type for hook compaction return value
    - Import CompactionResult in hooks/types.ts
    - Replace inline type with CompactionResult for SessionEventResult.compaction
    - Add labels feature to changelog
  • Add label support for session entries
    - Add LabelEntry type with targetId and label (string | undefined)
    - Add labelsById map built on load via linear scan
    - Add getLabel(id) and appendLabelChange(targetId, label) methods
    - Add label field to SessionTreeNode, populated by getTree()
    - Update createBranchedSession to preserve labels for entries on path
    - Labels are ignored by buildSessionContext (not sent to LLM)
    - Add comprehensive tests for label functionality
  • Session tree: simplify types, add branching API, comprehensive tests
    Types:
    - SessionEntryBase with type field, extended by all entry types
    - CustomEntry for hooks (type: 'custom', customType, data)
    - Remove XXXContent types and TreeNode (redundant)
    
    API:
    - Rename saveXXX to appendXXX with JSDoc explaining tree semantics
    - Rename branchInPlace to branch() with better docs
    - Add createBranchedSession(leafId) replacing index-based version
    - Add getTree() returning SessionTreeNode[] for tree traversal
    - Add appendCustomEntry(customType, data) for hooks
    
    Tests:
    - tree-traversal.test.ts: 28 tests covering append, getPath, getTree,
      branch, branchWithSummary, createBranchedSession
    - save-entry.test.ts: custom entry integration
    
    Docs:
    - Class-level JSDoc explaining append-only tree model
    - Method docs explaining leaf advancement and branching
    - CHANGELOG.md entry for all changes
  • Refactor session manager: migration chain, validation, tests
    - Add migrateV1ToV2/migrateToCurrentVersion for extensible migrations
    - createSummaryMessage now takes timestamp from entry
    - loadEntriesFromFile validates session header
    - findMostRecentSession only returns valid session files (reads first 512 bytes)
    - Remove ConversationEntry alias
    - Fix mom context.ts TreeNode type
    
    Tests:
    - migration.test.ts: v1 migration, idempotency
    - build-context.test.ts: 14 tests covering trivial, compaction, branches
    - file-operations.test.ts: loadEntriesFromFile, findMostRecentSession
  • Use short 8-char IDs for session entries
    - Replace custom uuidv4() with native crypto.randomUUID()
    - Entry IDs use first 8 hex chars with collision checking
    - Session IDs stay full UUIDs (used in filenames)
    - ~0.01 collisions per 10k entries, retry handles it
  • Fix --session flag to use provided filename
    When --session path was provided for a non-existent file, _initNewSession() was overwriting the path with an auto-generated one. Now it only generates a filename if sessionFile wasn't already set.
  • Fix SessionEntry type to exclude SessionHeader
    - SessionEntry now only contains conversation entries (messages, compaction, etc.)
    - SessionHeader is separate, not part of SessionEntry
    - FileEntry = SessionHeader | SessionEntry (for file storage)
    - getEntries() filters out header, returns SessionEntry[]
    - Added getHeader() for accessing session metadata
    - Updated compaction and tests to not expect header in entries
    - Updated mom package to use FileEntry for internal storage