diff --git a/specs/unified-bubbletea-architecture.md b/specs/unified-bubbletea-architecture.md deleted file mode 100644 index 5308d573..00000000 --- a/specs/unified-bubbletea-architecture.md +++ /dev/null @@ -1,450 +0,0 @@ -# Unified Bubble Tea Architecture - -## Overview - -Replace the micro-program pattern (3 interactive `tea.NewProgram` calls + 1 standalone progress) with a single persistent Bubble Tea program using child model composition. Extract a thick app layer from `cmd/root.go` to own agent orchestration, message storage, and event emission. TUI becomes purely reactive. - -New capabilities: message queueing during streaming, double-tap ESC cancellation, stacked layout (output above, input pinned below), queue badge with clear support. - -## User Story - -As a KIT user, I want the TUI to remain responsive during agent streaming so I can queue follow-up messages, cancel in-progress work, and see a persistent input area -- instead of waiting for each response to complete before typing. - -As a developer, I want the TUI architecture to follow Bubble Tea's idiomatic child-model pattern so components are composable, testable, and extensible without terminal ownership conflicts. - -## Requirements - -### Architecture -- Single `tea.NewProgram()` call for the entire interactive session -- Parent model manages state transitions and routes messages to child components -- Child components: `InputComponent` (slash commands + autocomplete), `StreamComponent` (streaming display + spinner), `ApprovalComponent` (tool approval) -- Ollama `ProgressModel` remains standalone (different lifecycle, runs during provider init) -- Non-interactive mode bypasses `tea.Program` entirely, uses same app layer without TUI - -### App Layer -- New `internal/app` package owns: agent orchestration loop, in-memory message store, message queue, tool approval callback, hook execution, session persistence, usage tracking -- App layer exposes `Run(prompt)`, `RunOnce(ctx, prompt)`, `CancelCurrentStep()`, `ClearQueue()`, `QueueLength()`, `ClearMessages()` -- Events sent to TUI via `program.Send()` -- no pubsub infra -- Message store: mutable `[]fantasy.Message` with wrapper IDs, emits events on change. Bridges to `session.Manager` for persistence on each step completion. -- `ToolApprovalFunc` provided at construction via `Options`. Interactive mode: channel handshake with TUI. Non-interactive: auto-approve. Channel must be `select`-able against app context to avoid goroutine leaks on shutdown. -- All 7 agent callbacks from `GenerateWithLoopAndStreaming` (`agent.go:144-151`) mapped to events sent via `program.Send()`. See Events section. -- Hook executor (`hooks.Executor`) owned by app layer. Fires `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop` at same points as current `runAgenticStep`. - -### App Layer Options - -Full options mirroring current `AgenticLoopConfig` (`root.go:753-769`): - -```go -type Options struct { - Agent *agent.Agent - ToolApprovalFunc ToolApprovalFunc // required, set at construction - HookExecutor *hooks.Executor // optional - SessionManager *session.Manager // optional, for persistence - MCPConfig *config.Config // for session continuation - ModelName string - ServerNames []string // for slash commands - ToolNames []string // for slash commands - StreamingEnabled bool - Quiet bool - Debug bool - CompactMode bool -} -``` - -### Events - -Events emitted by app layer (defined in `internal/app/events.go`): - -| Event | Source callback | Purpose | -|---|---|---| -| `StreamChunkEvent` | `onStreamingResponse` | Streaming text delta | -| `ToolCallStartedEvent` | `onToolCall` | Tool call initiated (name + args) | -| `ToolExecutionEvent` | `onToolExecution` | Tool execution starting/stopping | -| `ToolResultEvent` | `onToolResult` | Tool result (name, args, result, isError) | -| `ToolCallContentEvent` | `onToolCallContent` | Tool call content display | -| `ResponseCompleteEvent` | `onResponse` | Final response text | -| `StepCompleteEvent` | (after generate returns) | Agent step finished, includes usage data | -| `StepErrorEvent` | (on agent error) | Agent step failed with error | -| `QueueUpdatedEvent` | (on queue change) | Queue length changed | -| `ToolApprovalNeededEvent` | `onToolApproval` | Approval required, includes response channel | -| `SpinnerEvent` | (before first chunk) | Show/hide spinner state | -| `HookBlockedEvent` | (hook returns block) | Hook blocked the action | -| `MessageCreatedEvent` | (on history add) | New message added to store | - -TUI-internal messages (defined in `internal/ui/events.go`, NOT in app layer): - -| Message | Purpose | -|---|---| -| `submitMsg` | Input component submitted text | -| `approvalResultMsg` | Approval component returned decision | -| `cancelTimerExpiredMsg` | 2s ESC timer expired | - -### TUI Behavior -- Stacked layout: latest response output above, input textarea pinned below -- Output area shows latest response only. Completed responses emitted above the BT-managed region via `tea.Println()` before the model resets for the next interaction. This works with BT v2 inline mode (no alt screen). -- Input textarea keeps current sizing behavior from `SlashCommandInput` -- Slash command autocomplete fully self-contained in input component. Component holds `*app.App` reference for executing commands that affect app state (`/clear` calls `app.ClearMessages()`, `/quit` returns `tea.Quit` to parent, `/clear-queue` calls `app.ClearQueue()`). Parent receives either a `submitMsg` (text prompt) or a `tea.Cmd` (slash command side effect). -- Message queueing: user can submit while agent streams. Queue badge shows "N queued" near input. `/clear-queue` slash command flushes queue. -- Double-tap ESC: first press shows "press again to cancel", second press calls `App.CancelCurrentStep()`. Timer expires after 2s, resets state. -- Tool approval: agent blocks on `ToolApprovalFunc` callback. Callback sends `ToolApprovalNeededEvent` (containing a `chan<- bool` response channel) to program, then blocks on that channel via `select` with `ctx.Done()`. TUI transitions to approval state, user decides, parent sends result on channel. If ctx cancelled, callback returns `false, ctx.Err()`. -- Keyboard during streaming: input textarea remains focused and editable. All keystrokes go to the input component normally. ESC is intercepted by parent for cancel flow. Enter/submit queues the message via `app.Run()`. -- Spinner: `StreamComponent` renders a spinner animation (replacing the current standalone goroutine-based `ui.Spinner`) when the agent is processing but hasn't sent any chunks yet. First `StreamChunkEvent` transitions from spinner to streaming display. No more goroutine writing to stderr. - -### Compact Mode - -Current code uses two renderers (`MessageRenderer` and `CompactRenderer`) toggled by `cli.compactMode`. Both renderers retained. The `CompactMode` flag propagated through `App.Options` → parent model → child components. Each component checks the flag and delegates to the appropriate renderer for message formatting. - -### Usage Tracking - -`UsageTracker` moves to the app layer. Created during `App.New()` using model info from `Options`. App layer calls `UpdateUsageFromResponse()` after each step. Emits usage data in `StepCompleteEvent`. TUI renders usage via retained `UsageTracker.RenderUsageInfo()` method. Non-interactive mode reads usage from app layer directly. - -### Non-Interactive Mode -- Same app layer, no TUI. `ToolApprovalFunc` auto-approves (provided at construction). Output prints directly to stdout. -- Current `runNonInteractiveMode` refactored to use `app.RunOnce()`. -- **Behavior change**: current non-interactive non-quiet mode creates a BT streaming display program. New behavior: `RunOnce()` accepts an optional `StreamingWriter io.Writer` for real-time output. Non-interactive passes `os.Stdout`. No BT program created. - -### Session Persistence - -`session.Manager` owned by app layer (passed via `Options.SessionManager`). App layer calls `session.Manager.AddMessages()` after each step completion and on queue drain. `--load-session` flag handled in `cmd/root.go` before app construction -- loaded messages passed to `App.New()` as initial history. `MessageStore.Clear()` also calls `session.Manager.ReplaceAllMessages()`. - -### Error Handling - -Agent errors (API failures, rate limits, MCP crashes) emitted as `StepErrorEvent`. Parent model receives the event, passes error to `StreamComponent` for inline display (matching current behavior), then transitions to `stateInput`. No automatic retry -- user can retry by submitting again. - -### Graceful Shutdown - -Shutdown sequence when user quits (Ctrl+C or `/quit`): - -1. Parent model returns `tea.Quit` -2. `tea.Program.Run()` returns in `cmd/root.go` -3. If agent goroutine running: `app.CancelCurrentStep()` called (deferred) -4. `app.Close()` called (deferred) -- cancels app context, waits for agent goroutine to exit -5. `mcpAgent.Close()` called (deferred, existing) -- closes MCP connections and provider - -`App` holds a top-level `context.Context` (created with `context.WithCancel` in `New()`). All agent goroutines use this context. `App.Close()` cancels it and calls `sync.WaitGroup.Wait()` to ensure clean exit. - -### Parent Model State Machine - -``` -stateInput ──submit──→ stateWorking ──StepComplete──→ stateInput - │ ↑ - ├──ToolApproval──→ stateApproval──approve/deny──┘ - │ │ - ├──StepError────→ stateInput │ - │ │ - └──Cancel────────→ stateInput │ - │ - (queue non-empty: auto-drain) ───┘ -``` - -States: -- `stateInput` -- input focused, waiting for user -- `stateWorking` -- agent running (spinner → streaming → tool calls → streaming → ...) -- `stateApproval` -- tool approval dialog active (sub-state of working) - -### Testing -- Unit tests for each child component (send messages, assert state transitions) -- Unit tests for parent model (state routing, child delegation, cancel flow, error handling) -- Unit tests for app layer (message store, queue, cancel, session save ordering, ToolApprovalFunc channel + ctx cancellation) - -## Technical Implementation - -### Package Structure - -``` -internal/ - app/ - app.go # App struct, New(), Run(), RunOnce(), CancelCurrentStep(), Close() - app_test.go # App tests (queue, cancel, drain, session save) - messages.go # MessageStore (in-memory, wraps []fantasy.Message, bridges session.Manager) - messages_test.go # MessageStore tests - events.go # All event types sent to TUI via program.Send() - options.go # Options struct, ToolApprovalFunc type - ui/ - model.go # Parent tea.Model (AppModel), state machine, message routing - model_test.go # Parent model tests - input.go # InputComponent (refactored slash_command_input.go) - input_test.go # Input tests - stream.go # StreamComponent (refactored streaming_display.go + spinner) - stream_test.go # Stream tests - approval.go # ApprovalComponent (refactored tool_approval_input.go) - approval_test.go # Approval tests - events.go # TUI-internal message types (submitMsg, approvalResultMsg, cancelTimerExpiredMsg) - cli.go # Retained: SetupCLI factory (creates App + AppModel), non-TUI helpers - messages.go # Retained: message rendering (used by StreamComponent) - styles.go # Retained - enhanced_styles.go # Retained - compact_renderer.go # Retained - block_renderer.go # Retained - commands.go # Retained + /clear-queue added - fuzzy.go # Retained - usage_tracker.go # Retained (used by app layer) - debug_logger.go # Retained (used by app layer) - ui/progress/ - ollama.go # Retained standalone (not part of refactor) -``` - -### Parent Model - -```go -type appState int -const ( - stateInput appState = iota // Input focused, waiting for user - stateWorking // Agent running, streaming output - stateApproval // Tool approval dialog active -) - -type AppModel struct { - state appState - app *app.App // Thick app layer reference - input InputComponent // Child: user input + autocomplete - stream StreamComponent // Child: streaming display + spinner - approval ApprovalComponent // Child: tool approval - renderer *MessageRenderer // For tea.Println of completed responses - compactRdr *CompactRenderer // Compact mode renderer - compactMode bool // Which renderer to use - queueCount int // Cached from QueueUpdatedEvent - canceling bool // Double-tap ESC state - approvalChan chan<- bool // Response channel for current approval - width int - height int -} -``` - -### Event Flow - -``` -User types → InputComponent.Update() → submit - ↓ -Parent receives submitMsg → calls app.Run(prompt) in tea.Cmd goroutine - ↓ -Parent transitions to stateWorking → StreamComponent active (spinner mode) - ↓ -App layer goroutine: agent processes - → program.Send(SpinnerEvent{Show: true}) - → program.Send(ToolCallStartedEvent{...}) - → program.Send(StreamChunkEvent{...}) (first chunk hides spinner) - → program.Send(ToolResultEvent{...}) - → program.Send(ToolCallContentEvent{...}) - ↓ -Parent routes events to StreamComponent.Update() - ↓ -Agent needs tool approval → ToolApprovalFunc called - → creates chan bool, sends ToolApprovalNeededEvent{ResponseChan: ch} - → blocks: select { case result := <-ch; case <-ctx.Done() } - ↓ -Parent stores channel in approvalChan, transitions to stateApproval - ↓ -User approves → Parent sends on approvalChan → Agent continues - ↓ -Agent completes → app sends StepCompleteEvent{Usage: ...} - ↓ -Parent: tea.Println() completed response, transitions to stateInput - ↓ -If queue non-empty: App auto-drains next message, stays in stateWorking -``` - -### Cancel Flow - -``` -User presses ESC during stateWorking - ↓ -Parent sets canceling=true, returns cancelTimerCmd (2s tea.Tick) - ↓ -User presses ESC again within 2s → Parent calls app.CancelCurrentStep() - ↓ -App cancels step context → agent goroutine exits - → ToolApprovalFunc unblocks via ctx.Done() if waiting - → StepErrorEvent or StepCompleteEvent emitted - ↓ -Parent transitions to stateInput - ↓ -cancelTimerExpiredMsg arrives (if no second ESC) → resets canceling=false -``` - -### cmd/root.go Changes - -```go -// runNormalMode becomes: -appInstance, err := app.New(app.Options{ - Agent: mcpAgent, - ToolApprovalFunc: toolApprovalFunc, // set per mode, see below - HookExecutor: hookExecutor, - SessionManager: sessionManager, - MCPConfig: mcpConfig, - ModelName: modelString, - ServerNames: serverNames, - ToolNames: toolNames, - StreamingEnabled: viper.GetBool("stream"), - Quiet: quietFlag, - Debug: viper.GetBool("debug"), - CompactMode: viper.GetBool("compact"), -}, initialMessages) // loaded from session if --load-session -defer appInstance.Close() - -// Interactive mode: -toolApprovalFunc = app.NewInteractiveApprovalFunc(appInstance) -model := ui.NewAppModel(appInstance, uiOpts) -program := tea.NewProgram(model) -appInstance.SetProgram(program) // Safe: app.Run() not called until Init() -_, err := program.Run() - -// Non-interactive mode: -toolApprovalFunc = app.AutoApproveFunc -result, err := appInstance.RunOnce(ctx, prompt, os.Stdout) // stdout for streaming -printResult(result) -``` - -**SetProgram timing**: Safe because `app.Run()` is only called from `tea.Cmd` functions after the program starts its event loop. `AppModel.Init()` returns no command that calls `app.Run()` -- the first `Run()` call happens when the user submits input or when `Init()` dispatches an initial prompt (non-interactive continuation via `--no-exit`). - -## Tasks - -### 1. Create app layer skeleton -- [ ] [P0] Create `internal/app/events.go` with all event types: `StreamChunkEvent`, `ToolCallStartedEvent`, `ToolExecutionEvent`, `ToolResultEvent`, `ToolCallContentEvent`, `ResponseCompleteEvent`, `StepCompleteEvent` (with usage data), `StepErrorEvent`, `QueueUpdatedEvent`, `ToolApprovalNeededEvent` (with `ResponseChan chan<- bool`), `SpinnerEvent`, `HookBlockedEvent`, `MessageCreatedEvent` -- [ ] [P0] Create `internal/app/options.go` with `Options` struct (all fields from App Layer Options section), `ToolApprovalFunc` type (`func(ctx context.Context, toolName, toolArgs string) (bool, error)`), `AutoApproveFunc` var, `NewInteractiveApprovalFunc` constructor -- [ ] [P0] Create `internal/app/messages.go` with `MessageStore` wrapping `[]fantasy.Message`. Methods: `Add(fantasy.Message)`, `Replace([]fantasy.Message)`, `GetAll() []fantasy.Message`, `Clear()`. Bridges to `session.Manager` (if non-nil) on every mutation for persistence. -- [ ] [P0] Create `internal/app/app.go` with `App` struct, `New(opts, initialMessages)`, `SetProgram(*tea.Program)`, `Run(prompt)`, `RunOnce(ctx, prompt, io.Writer)`, `CancelCurrentStep()`, `QueueLength()`, `ClearQueue()`, `ClearMessages()`, `Close()`. Internal: `context.WithCancel`, `sync.WaitGroup`, `sync.Mutex` for busy/queue state. - -### 2. Migrate agent orchestration into app layer -- [ ] [P1] Move `runAgenticStep` logic from `cmd/root.go:873-1191` into `App.executeStep()`. Map all 7 agent callbacks to `program.Send()` events. Wire `ToolApprovalFunc` for `onToolApproval`. Emit `SpinnerEvent{Show:true}` before calling agent, `SpinnerEvent{Show:false}` on first stream chunk. -- [ ] [P1] Move hook execution from `cmd/root.go:810-828,943-969,1002-1019,1186-1223` into `App.executeStep()`. Fire `UserPromptSubmit` in `Run()` before `executeStep()`. Fire `PreToolUse`/`PostToolUse`/`Stop` at same points. Emit `HookBlockedEvent` if hook blocks. -- [ ] [P1] Move conversation history management into `MessageStore`. `App.executeStep()` calls `store.Add()` for user message before agent call, `store.Replace()` with updated history after agent returns. Store bridges to `session.Manager`. -- [ ] [P1] Move usage tracking into app layer. Create `UsageTracker` in `App.New()` from model info. Call `UpdateUsageFromResponse()` after each step. Include usage data in `StepCompleteEvent`. -- [ ] [P1] Implement queue drain: after step completes (success or error), if queue non-empty, dequeue next message and call `executeStep()` in same goroutine (no new goroutine spawn). - -### 3. Create parent TUI model -- [ ] [P1] Create `internal/ui/model.go` with `AppModel` struct (see Parent Model section), `NewAppModel()`, `Init()`, `Update()`, `View()`. State machine routes events to children based on `appState`. Handle `tea.WindowSizeMsg` to distribute height. Store `approvalChan` for tool approval response. -- [ ] [P1] Implement double-tap ESC cancel in parent `Update()`: intercept `tea.KeyPressMsg` for ESC during `stateWorking`. Track `canceling` bool, return `tea.Tick(2*time.Second, ...)` as timer cmd, call `app.CancelCurrentStep()` on second press within window. -- [ ] [P1] Implement `tea.Println()` for completed responses: on `StepCompleteEvent`, render the completed response using message renderer (respecting compact mode), emit via `tea.Println()`, then reset `StreamComponent` state. -- [ ] [P1] Implement `StepErrorEvent` handling: render error inline in stream area, transition to `stateInput`. -- [ ] [P1] Implement graceful quit: Ctrl+C and `/quit` return `tea.Quit`. Deferred `app.Close()` in `cmd/root.go` handles cleanup. - -### 4. Refactor child components -- [ ] [P1] Refactor `slash_command_input.go` → `internal/ui/input.go` as `InputComponent`. Remove `tea.Quit` on submit -- return `submitMsg` as a `tea.Cmd`. Keep autocomplete + popup self-contained. Hold `*app.App` reference for slash command execution: `/clear` → `app.ClearMessages()`, `/clear-queue` → `app.ClearQueue()`, `/quit` → return `tea.Quit` cmd. Remove `os.Exit(0)` from `/quit`. -- [ ] [P1] Refactor `streaming_display.go` → `internal/ui/stream.go` as `StreamComponent`. Add spinner state: render KITT-style animation (from current `spinner.go`) when `SpinnerEvent{Show:true}` received, switch to streaming text on first `StreamChunkEvent`. Accept all display events (`ToolCallStartedEvent`, `ToolResultEvent`, etc.) and render via retained `MessageRenderer`/`CompactRenderer`. Remove `streamDoneMsg`/`tea.Quit` -- parent manages lifecycle. Add `Reset()` to clear state between steps. -- [ ] [P1] Refactor `tool_approval_input.go` → `internal/ui/approval.go` as `ApprovalComponent`. Remove `tea.Quit` -- return `approvalResultMsg{approved: bool}` as a `tea.Cmd`. Parent handles sending result on `approvalChan`. - -### 5. Wire TUI to app layer in cmd/root.go -- [ ] [P1] Refactor `runNormalMode()`: create `app.App` with full `Options` (all fields). Wire `ToolApprovalFunc` per mode. Load session messages before construction. Defer `appInstance.Close()`. -- [ ] [P1] Interactive path: create `ui.NewAppModel()` + single `tea.NewProgram(model)` + `appInstance.SetProgram(program)` + `program.Run()`. Remove `SetupCLI()` flow for interactive mode. -- [ ] [P1] Non-interactive path: call `appInstance.RunOnce(ctx, prompt, os.Stdout)`. Handle `--no-exit` by switching to interactive mode after. No `tea.Program` created. Remove old streaming display usage for non-interactive. -- [ ] [P1] Retain `SetupCLI()` as alternative factory for non-interactive quiet mode (just prints final text, no renderers needed). Or inline the quiet-mode logic. - -### 6. Implement message queueing UX -- [ ] [P2] Add queue badge rendering in parent `View()` -- show "N queued" right-aligned on separator line when `queueCount > 0`. Update count on `QueueUpdatedEvent`. -- [ ] [P2] Register `/clear-queue` slash command in `internal/ui/commands.go`. -- [ ] [P2] Handle `submitMsg` during `stateWorking`: parent calls `app.Run()` (which queues internally), does NOT transition state. Input component stays active and clears text. - -### 7. Stacked layout -- [ ] [P2] Implement stacked `View()` in parent: stream output region (variable height) + separator line + input region (current textarea height). Use `lipgloss.JoinVertical`. Separator shows queue badge if applicable. -- [ ] [P2] Handle `tea.WindowSizeMsg` propagation: calculate input height (fixed, from textarea), separator (1 line), remaining goes to stream. Propagate dimensions to children. - -### 8. Cleanup -- [ ] [P2] Delete standalone `tea.NewProgram` calls from `cli.go` (`GetPrompt`, `StartStreamingMessage`, `GetToolApproval`). Remove `streamProgram`/`streamDone` fields. -- [ ] [P2] Delete `runAgenticStep`, `runAgenticLoop`, `runInteractiveLoop`, `addMessagesToHistory`, `replaceMessagesHistory`, `AgenticLoopConfig` from `cmd/root.go`. -- [ ] [P2] Delete old `spinner.go` (replaced by StreamComponent's inline spinner). -- [ ] [P3] Trim `CLI` struct to only non-TUI helpers needed by non-interactive quiet mode. Remove `GetPrompt`, `StartStreamingMessage`, `UpdateStreamingMessage`, `GetToolApproval`, `finishStreaming`, `HandleSlashCommand`. Retain `DisplayError`, `DisplayInfo` for non-interactive error output if needed, or remove entirely if `RunOnce` handles its own output. - -### 9. Tests -- [ ] [P2] Unit tests for `MessageStore`: add, replace, getAll, clear, session.Manager bridge (mock manager, verify calls) -- [ ] [P2] Unit tests for `App`: run (single), run (queued), cancel during step, cancel during approval (verify ToolApprovalFunc unblocks via ctx), queue drain ordering, ClearQueue, Close (verify goroutine cleanup via WaitGroup) -- [ ] [P2] Unit tests for `AppModel`: state transitions (input→working→approval→input), StepError→input, ESC cancel flow (single tap resets, double tap cancels), queue badge update, window resize, tea.Println on step complete -- [ ] [P2] Unit tests for child components: `InputComponent` (submit emits submitMsg, slash commands execute, /quit returns tea.Quit), `StreamComponent` (spinner→streaming transition, chunk accumulation, tool call rendering, reset), `ApprovalComponent` (approve/deny emits approvalResultMsg) - -## UI Mockup - -### Processing (stateWorking, spinner) - -``` - ◇◇◇◆◇◇◇ Thinking... - - -─────────────────────────────────── -> █ -``` - -### During Streaming (stateWorking) - -``` - assistant (claude-sonnet-4-20250514) - Here is the implementation of the requested - feature. First, I'll create the new file... - █ (streaming cursor) - -─────────────────────────────────── 2 queued -> write tests for that too█ -``` - -### Tool Call in Stream (stateWorking) - -``` - assistant (claude-sonnet-4-20250514) - Let me check the build first. - - ⚙ bash: go build -o output/kit - ◇◇◇◆◇◇◇ Executing... - -─────────────────────────────────── 2 queued -> write tests for that too█ -``` - -### During Tool Approval (stateApproval) - -``` - assistant (claude-sonnet-4-20250514) - I need to run a command to check the build. - - ┌─ Tool Approval ──────────────────────┐ - │ bash: go build -o output/kit │ - │ │ - │ [Yes] No │ - └──────────────────────────────────────┘ - -─────────────────────────────────── 2 queued -> █ -``` - -### Cancel in Progress (stateWorking, canceling) - -``` - assistant (claude-sonnet-4-20250514) - Analyzing the codebase structure to find - relevant files... - - ⚠ Press ESC again to cancel - -─────────────────────────────────── 1 queued -> also check the tests█ -``` - -### Error (stateWorking → stateInput) - -``` - ✗ Error: API rate limit exceeded. Try again. - -─────────────────────────────────── -> █ -``` - -## Out of Scope - -- Scrollable viewport / chat history browsing (latest response only for v1) -- Ollama `ProgressModel` unification (stays standalone) -- Persistent message storage / database (session JSON files retained as-is) -- Multi-session support -- Split-pane or tabbed layouts -- Mouse interaction -- Changing tool call display format (keep current rendering via retained renderers) -- Prompt history persistence across sessions -- Any visual/theme changes beyond new layout -- Refactoring the `agent.Agent` or `fantasy` interfaces -- Changing hook execution semantics - -## Open Questions - -- Should queue drain be immediate (next message starts as soon as current step completes) or should there be a brief pause to let the user read the response? -- If the user cancels mid-stream and there are queued messages, should the queue also be flushed or should the next queued message execute? -- Should the input component retain focus (cursor visible, editable) during `stateApproval`, or should focus fully transfer to the approval dialog? -- Should `tea.Println()` of completed responses include tool call/result details, or just the final assistant text? Current behavior shows everything inline. -- How should debug logging work during the TUI lifecycle? Currently `BufferedDebugLogger` accumulates messages shown after agent creation. In the new architecture, should debug messages be events rendered in the stream component? -- For `--no-exit` (non-interactive then interactive): should `RunOnce` return and then `cmd/root.go` creates the TUI program for the interactive continuation, or should the TUI program be created upfront and the initial prompt dispatched via `Init()`?