Iteratr is a Go TUI application built with BubbleTea v2 that manages iterative development sessions with an AI agent. The application features a multi-pane interface with real-time updates via NATS messaging, agent output streaming, task/note management, modal overlays, and subagent session replay.
Screen/Draw pattern : Components render directly to screen buffers using Ultraviolet
Message-based updates : State changes propagate via typed messages
Lazy rendering : ScrollList components only render visible items
Hierarchical focus management : Priority-based keyboard routing (Dialog > Prefix Mode > Modal > View > Focus > Component)
Prefix key sequences : ctrl+x initiates a two-key command sequence
App (internal/tui/app.go:37-1445)
├── Root BubbleTea Model
├── Implements: tea.Model (Init, Update, View)
├── State Management: session.Store, NATS event subscription, Orchestrator control
├── Channels: eventChan (NATS events), sendChan (user input to orchestrator)
├── Prefix Mode: awaitingPrefixKey for ctrl+x sequences
│
├─── Dashboard (internal/tui/dashboard.go:27-325)
│ ├── Main content area component
│ ├── Implements: FocusableComponent
│ ├── Focus Management: FocusPane enum (FocusAgent, FocusTasks, FocusNotes, FocusInput)
│ ├── Renders: "Agent Output" panel with title bar
│ ├── Child Components:
│ │ ├── AgentOutput (shared reference, rendered by Dashboard)
│ │ └── Sidebar (shared reference for focus delegation)
│ └── Message Handling:
│ ├── KeyPress: Tab (cycle focus), i (focus input), Enter/Esc (input control)
│ ├── UserInputMsg → emitted when user submits text
│ └── Focus delegation to child components
│
├─── AgentOutput (internal/tui/agent.go:15-1024)
│ ├── Streaming agent conversation display
│ ├── Implements: Component (Draw, Update)
│ ├── Child Components:
│ │ ├── ScrollList (messages viewport)
│ │ ├── textinput.Model (bubbles v2 - user input field)
│ │ └── GradientSpinner (streaming animation)
│ ├── Message Types (internal/tui/messages.go):
│ │ ├── TextMessageItem (assistant text with markdown rendering)
│ │ ├── UserMessageItem (user text, right-aligned)
│ │ ├── ThinkingMessageItem (reasoning content, collapsible)
│ │ ├── ToolMessageItem (tool calls with status, expandable)
│ │ ├── SubagentMessageItem (subagent tasks with session viewer)
│ │ ├── InfoMessageItem (model/provider/duration metadata)
│ │ ├── HookMessageItem (hook command execution, expandable)
│ │ └── DividerMessageItem (iteration separator)
│ ├── Layout: Vertical split (viewport: height-5, input area: 5 lines)
│ ├── Renders:
│ │ ├── ScrollList viewport with message items
│ │ ├── Separator line
│ │ ├── Input field ("> " prompt + text input + queue indicator)
│ │ └── Help text (context-sensitive hints)
│ ├── Mouse Interaction:
│ │ ├── Click-to-expand: Toggles expandable messages (ToolMessageItem, ThinkingMessageItem, HookMessageItem)
│ │ ├── Click SubagentMessageItem → opens SubagentModal
│ │ └── Input area click: Focuses text input
│ └── Message Handling:
│ ├── AgentOutputMsg → AppendText()
│ ├── AgentToolCallMsg → AppendToolCall()
│ ├── AgentThinkingMsg → AppendThinking()
│ ├── AgentFinishMsg → AppendFinish()
│ ├── HookStartMsg → AppendHook()
│ ├── HookCompleteMsg → UpdateHook()
│ ├── KeyPress: up/down (scroll), j/k (vim scroll), space/enter (toggle expand)
│ └── GradientSpinnerMsg → spinner animation updates
│
├─── Sidebar (internal/tui/sidebar.go:174-878)
│ ├── Tasks and notes list display with logo
│ ├── Implements: FocusableComponent
│ ├── Child Components:
│ │ ├── tasksScrollList (task items)
│ │ ├── notesScrollList (note items)
│ │ └── Pulse (animation effect for status changes)
│ ├── Layout: Vertical split (Logo: 6, Tasks: 55%, Notes: 45% of remainder)
│ ├── Renders:
│ │ ├── Logo panel: gradient-colored "iteratr" ASCII art
│ │ ├── Tasks panel: "Tasks" title + ScrollList of taskScrollItem
│ │ └── Notes panel: "Notes" title + ScrollList of noteScrollItem
│ ├── Task Item Format: " [icon] content" (icons: ►=in_progress, ○=remaining, ✓=completed, ⊘=blocked, ⊗=cancelled)
│ ├── Note Item Format: " [icon] content" (icons: *=learning, !=stuck, ›=tip, ◇=decision)
│ ├── Mouse Interaction:
│ │ ├── TaskAtPosition() → opens TaskModal
│ │ └── NoteAtPosition() → opens NoteModal
│ ├── Message Handling:
│ │ ├── KeyPress: j/down (cursor down), k/up (cursor up), enter (open modal)
│ │ ├── PulseMsg → pulse animation updates
│ │ ├── StateUpdateMsg → detects task status changes, triggers pulse
│ │ └── OpenTaskModalMsg → emitted when task selected
│ └── State Tracking:
│ ├── taskIndex (ID → position lookup)
│ ├── noteIndex (ID → position lookup)
│ └── pulsedTaskIDs (track status changes)
│
├─── StatusBar (internal/tui/status.go:18-394)
│ ├── Session info, git status, and keybinding hints
│ ├── Implements: FullComponent
│ ├── Child Components:
│ │ └── Spinner (bubbles v2 - activity indicator)
│ ├── Layout: Single row at top of screen
│ ├── Renders: "iteratr | session | branch* hash | H:MM:SS | Iteration #N | stats | [spinner] | PAUSED/PAUSING | hints"
│ ├── Left Side: title, session name, git info, duration, iteration number, task stats, file count, spinner, pause state
│ ├── Right Side: keybinding hints (ctrl+x r restart [when complete], ctrl+x p pause, ctrl+x l logs, ctrl+c quit)
│ ├── Prefix Mode: Shows "(awaiting key...)" when waiting for second key
│ └── Message Handling:
│ ├── StateUpdateMsg → updates task stats, starts/stops spinner
│ ├── DurationTickMsg → updates elapsed time display
│ ├── PauseStateMsg → updates pause indicator
│ ├── AgentBusyMsg → determines PAUSING vs PAUSED display
│ ├── GitInfoMsg → updates git status display
│ └── ConnectionStatusMsg → updates connection indicator
│
├─── LogViewer (internal/tui/logs.go:15-223) [Modal Overlay]
│ ├── Event history modal
│ ├── Implements: FocusableComponent
│ ├── Child Components:
│ │ └── viewport.Model (bubbles v2)
│ ├── Visibility: Toggled by ctrl+x l
│ ├── Renders: Centered modal (80% screen size) with event log
│ ├── Event Format: "HH:MM:SS [TYPE] action data"
│ └── Message Handling:
│ ├── EventMsg → AddEvent() (appends to log, auto-scrolls to bottom)
│ └── KeyPress: esc (close), up/down (scroll)
│
├─── TaskModal (internal/tui/modal.go:15-303) [Modal Overlay]
│ ├── Task detail view
│ ├── Visibility: Controlled by App.taskModal.visible
│ ├── Renders: Centered modal (60x20) with task details
│ ├── Content: ID, Status badge, Priority badge, Content, Dependencies, Timestamps
│ ├── Mouse Interaction:
│ │ ├── Click outside → closes modal
│ │ └── Click on different task → switches task
│ └── Message Handling:
│ └── KeyPress: esc (close)
│
├─── NoteModal (internal/tui/note_modal.go:42-597) [Modal Overlay]
│ ├── Interactive note editor modal
│ ├── Visibility: Controlled by App.noteModal.visible
│ ├── Child Components:
│ │ └── textarea.Model (bubbles v2 - content editing, 500 char limit)
│ ├── Focus Zones: noteModalFocusType, noteModalFocusContent, noteModalFocusDelete
│ ├── Renders: Centered modal (60x22) with interactive controls
│ ├── Content: ID, Type selector (badges), Content textarea, Created/Updated timestamps, Delete button, hint bar
│ ├── Type Selection: learning, stuck, tip, decision (cycle with left/right, immediate save via UpdateNoteTypeMsg)
│ ├── Mouse Interaction:
│ │ ├── Click outside → closes modal
│ │ └── Click on different note → switches note
│ └── Message Handling:
│ ├── KeyPress: Tab/Shift+Tab (cycle focus zones)
│ ├── KeyPress: left/right (cycle type when Type focused)
│ ├── KeyPress: ctrl+enter (save content → UpdateNoteContentMsg)
│ ├── KeyPress: enter/space (delete when Delete focused → RequestDeleteNoteMsg)
│ ├── KeyPress: d (delete shortcut from non-textarea focus → RequestDeleteNoteMsg)
│ ├── KeyPress: esc (blur textarea if focused, else close modal)
│ ├── PasteMsg → forwarded to textarea when Content focused (with char limit truncation + ShowToastMsg)
│ ├── UpdateNoteTypeMsg → emitted on type cycle (App → store.NoteType())
│ ├── UpdateNoteContentMsg → emitted on ctrl+enter save (App → store.NoteContent())
│ ├── RequestDeleteNoteMsg → emitted on delete (App → confirmation Dialog → DeleteNoteMsg → store.NoteDelete())
│ └── DeleteNoteMsg → closes modal, clears sidebar selection
│
├─── NoteInputModal (internal/tui/note_input_modal.go:14-512) [Modal Overlay]
│ ├── Interactive note creation modal
│ ├── Visibility: Controlled by ctrl+x n
│ ├── Child Components:
│ │ └── textarea.Model (bubbles v2 - multi-line input)
│ ├── Focus Zones: focusTypeSelector, focusTextarea, focusSubmitButton
│ ├── Renders: Centered modal with type badges, textarea, submit button
│ ├── Type Selection: learning, stuck, tip, decision (cycle with left/right)
│ ├── Mouse Interaction:
│ │ ├── Button click → submits note
│ │ └── Click outside → closes modal
│ └── Message Handling:
│ ├── KeyPress: Tab/Shift+Tab (cycle focus), left/right (cycle type)
│ ├── KeyPress: ctrl+enter (submit), esc (close)
│ └── CreateNoteMsg → emitted on submit
│
├─── TaskInputModal (internal/tui/task_input_modal.go:14-443) [Modal Overlay]
│ ├── Interactive task creation modal
│ ├── Visibility: Controlled by ctrl+x t
│ ├── Child Components:
│ │ └── textarea.Model (bubbles v2 - multi-line input)
│ ├── Focus Zones: focusPrioritySelector, focusTextarea, focusSubmitButton
│ ├── Renders: Centered modal with priority badges, textarea, submit button
│ ├── Priority Selection: critical, high, medium, low, backlog (cycle with left/right)
│ ├── Mouse Interaction:
│ │ ├── Button click → submits task
│ │ └── Click outside → closes modal
│ └── Message Handling:
│ ├── KeyPress: Tab/Shift+Tab (cycle focus), left/right (cycle priority)
│ ├── KeyPress: ctrl+enter (submit), esc (close)
│ └── CreateTaskMsg → emitted on submit
│
├─── SubagentModal (internal/tui/subagent_modal.go:17-630) [Modal Overlay]
│ ├── Full-screen subagent session viewer
│ ├── Visibility: Opened by clicking SubagentMessageItem with sessionID
│ ├── Child Components:
│ │ ├── ScrollList (message replay viewport)
│ │ ├── GradientSpinner (loading state)
│ │ └── SessionLoader (ACP subprocess for session replay)
│ ├── States: loading (spinner), error (message), content (scroll list)
│ ├── Renders: Full-screen modal with subagent conversation replay
│ ├── Mouse Interaction:
│ │ └── Click-to-expand: Toggles expandable messages
│ └── Message Handling:
│ ├── SubagentTextMsg → appendText()
│ ├── SubagentToolCallMsg → appendToolCall()
│ ├── SubagentThinkingMsg → appendThinking()
│ ├── SubagentUserMsg → appendUserMessage()
│ ├── SubagentDoneMsg → session replay complete
│ ├── SubagentErrorMsg → displays error
│ └── KeyPress: esc (close), up/down (scroll)
│
└─── Dialog (internal/tui/dialog.go:10-172) [Modal Overlay]
├── Simple confirmation dialog
├── Visibility: Controlled by App.dialog.visible
├── Renders: Centered rounded border dialog with title, message, OK button
├── Used for: Session completion notification
├── Mouse Interaction: Click anywhere → dismisses dialog
└── Message Handling:
├── KeyPress: enter/space/esc (close, execute onClose callback)
└── SessionCompleteMsg → shown when all tasks completed
Supporting Components (Non-BubbleTea Models)
ScrollList (internal/tui/scrolllist.go:21-480)
Purpose : Lazy-rendering scrollable list (only renders visible items)
Interface : ScrollItem (ID(), Render(width), Height())
Used By : AgentOutput, Sidebar (tasks/notes), SubagentModal
Features : Offset-based scrolling, auto-scroll to bottom, keyboard navigation (pgup/pgdown/home/end, j/k), selection highlighting
Message Items (internal/tui/messages.go)
All implement ScrollItem interface:
Item
Lines
Purpose
TextMessageItem
47-111
Assistant text with markdown rendering via glamour
UserMessageItem
54-161
User text, right-aligned with border
ThinkingMessageItem
225-325
Reasoning content, collapsible (last 10 lines when collapsed)
ToolMessageItem
327-611
Tool execution: header, code output, diffs, expandable
SubagentMessageItem
681-798
Subagent task: spinner, status, click-to-view hint
InfoMessageItem
613-679
Model/provider/duration metadata
HookMessageItem
869-1014
Hook command: icon (running/success/error), hook type label, command, duration, expandable output
DividerMessageItem
800-857
Iteration separator
Animation Components (internal/tui/anim.go)
Component
Lines
Purpose
Used By
Spinner
12-52
MiniDot activity indicator
StatusBar, SubagentMessageItem
Pulse
54-153
5-frame fade in/out effect
Sidebar (task status changes)
GradientSpinner
155-229
Animated gradient text
AgentOutput, SubagentModal ("Generating..."/"Thinking..."/"Loading...")
NotesPanel (internal/tui/notes.go:15-223)
Purpose : Dedicated notes view (grouped by type)
Used By : Potential future view switch
Features : Color-coded type headers, word wrapping, viewport scrolling
Footer (internal/tui/footer.go:12-239)
Purpose : Navigation footer bar (not currently used in main app)
Features : View navigation hints, clickable buttons, condensed mode for narrow terminals
main → Orchestrator.Start()
→ NewApp(ctx, store, sessionName, workDir, nc, sendChan, orchestrator)
→ App.Init() → tea.Batch(
subscribeToEvents(), // NATS subscription
waitForEvents(), // Event channel listener
loadInitialState(), // Load session from store
agent.Init(), // Initialize AgentOutput
checkConnectionHealth(), // Periodic health checks
status.StartDurationTick(), // Start elapsed time timer
fetchGitInfo() // Fetch git repository status
)
NATS Message (iteratr.{session}.>)
→ subscribeToEvents() → eventChan
→ waitForEvents() → EventMsg
→ App.Update(EventMsg)
├→ logs.AddEvent(event)
├→ loadInitialState()
└→ waitForEvents() // Continue listening
loadInitialState()
→ StateUpdateMsg{state}
→ App.Update(StateUpdateMsg)
├→ status.SetState(state)
├→ sidebar.SetState(state) // Detects changes → pulse
├→ dashboard.UpdateState(state)
└→ logs.SetState(state)
'i' → Dashboard.Update → focusPane = FocusInput → agent.SetInputFocused(true) → textinput.Focus()
typing → agent.Update → textinput.Update()
Enter → Dashboard.Update → UserInputMsg{text} → App.Update → sendChan <- text → orchestrator → agent
Agent runner → orchestrator → NATS/direct
→ App.Update receives:
├─ AgentOutputMsg → agent.AppendText()
├─ AgentToolCallMsg → agent.AppendToolCall()
├─ AgentThinkingMsg → agent.AppendThinking()
├─ AgentFinishMsg → agent.AppendFinish()
├─ HookStartMsg → agent.AppendHook()
└─ HookCompleteMsg → agent.UpdateHook()
→ ScrollList.SetItems() → auto-scroll
ctrl+x p → togglePause()
→ orchestrator.IsPaused() check
├→ Not paused: orchestrator.RequestPause() → PauseStateMsg{true}
├→ Paused + busy: orchestrator.CancelPause() → PauseStateMsg{false}
└→ Paused + idle: orchestrator.Resume() → PauseStateMsg{false}
→ StatusBar displays PAUSING... (agent busy) or PAUSED (agent idle)
ctrl+x r → restartSession()
→ Guard: state.Complete must be true (no-op otherwise)
→ store.SessionRestart(sessionName) (async goroutine)
→ status.stoppedAt = zero (clear frozen timestamp)
→ status.StartDurationTick() (resume timer)
→ ShowToastMsg{"Session restarted"}
Click SubagentMessageItem with sessionID
→ OpenSubagentModalMsg{sessionID, subagentType}
→ NewSubagentModal() → subagentModal.Start()
→ agent.NewSessionLoader() → loader.LoadAndStream()
→ streamNext() → SubagentTextMsg/SubagentToolCallMsg/...
→ HandleUpdate() → append to messages → refreshContent()
→ SubagentDoneMsg (on EOF)
ESC → subagentModal.Close() → subagentModal = nil
Keyboard Routing Priority
App.handleKeyPress(KeyPressMsg)
Priority 0: Global keys (ctrl+x prefix, ctrl+c quit) - work everywhere, even with modals
Priority 1: Dialog visible → Dialog.Update() (enter/space/esc closes)
Priority 2: Prefix mode (ctrl+x followed by l/b/n/t/p)
→ ctrl+x l: toggle logs
→ ctrl+x b: toggle sidebar
→ ctrl+x n: create note (opens NoteInputModal)
→ ctrl+x t: create task (opens TaskInputModal)
→ ctrl+x p: toggle pause/resume
→ ctrl+x r: restart completed session
Priority 3: TaskModal visible → forward all keys to TaskModal.Update()
Priority 4: NoteModal visible → forward all keys to NoteModal.Update()
Priority 5: NoteInputModal visible → forward to modal Update()
Priority 6: TaskInputModal visible → forward to modal Update()
Priority 7: SubagentModal visible → ESC closes, else forward scroll keys
Priority 8: LogViewer visible → ESC closes, else logs.Update()
Priority 9: dashboard.Update()
→ 'i' focus input
→ Tab cycle focus
→ Forward to agent (FocusAgent) or sidebar (FocusTasks/FocusNotes)
CalculateLayout() (internal/tui/layout.go:43-85)
Desktop Mode (width >= 100, height >= 25): 3-column layout (Status, Main, Sidebar)
Compact Mode (width < 100 or height < 25): 2-row layout (Status, Main), sidebar overlays on toggle
CompactWidthBreakpoint: 100 chars
CompactHeightBreakpoint: 25 rows
SidebarWidthDesktop: 45 chars
StatusHeight: 1 row
WindowSizeMsg → App.Update
→ CalculateLayout(width, height) → Layout{Mode, Status, Main, Sidebar}
→ propagateSizes()
├→ status.SetSize() + status.SetLayoutMode()
├→ dashboard.SetSize() → agent.UpdateSize()
├→ logs.SetSize()
└→ sidebar.SetSize()
App.View()
1. Recalculate layout if dirty
2. Create screen buffer: uv.NewScreenBuffer(width, height)
3. Draw in order (back to front):
├─ dashboard.Draw(scr, layout.Main)
├─ status.Draw(scr, layout.Status)
├─ sidebar.Draw(scr, layout.Sidebar) [desktop mode or sidebarVisible]
├─ logs.Draw(scr, area) [if logsVisible]
├─ subagentModal.Draw(scr, area) [if subagentModal != nil]
├─ taskModal.Draw(scr, area) [if visible]
├─ noteModal.Draw(scr, area) [if visible]
├─ noteInputModal.Draw(scr, area) [if visible]
├─ taskInputModal.Draw(scr, area) [if visible]
└─ dialog.Draw(scr, area) [if visible]
4. canvas.Render() → string
5. Return tea.View{Content, AltScreen, MouseMode, BackgroundColor}
File
Purpose
Lines
internal/tui/app.go
Root BubbleTea model, message routing, layout
1445
internal/tui/dashboard.go
Main content area, focus management
325
internal/tui/agent.go
Agent conversation display, user input
1024
internal/tui/sidebar.go
Tasks/notes lists with logo and pulse animation
878
internal/tui/status.go
Status bar with session/git info, pause state
394
internal/tui/logs.go
Event log modal overlay
223
internal/tui/modal.go
Task detail modal
303
internal/tui/note_modal.go
Interactive note editor modal
597
internal/tui/note_input_modal.go
Note creation modal with textarea
512
internal/tui/task_input_modal.go
Task creation modal with textarea
443
internal/tui/subagent_modal.go
Subagent session viewer modal
630
internal/tui/dialog.go
Simple confirmation dialog
172
internal/tui/scrolllist.go
Lazy-rendering scroll container
480
internal/tui/messages.go
Message item types for conversation display
1680
internal/tui/anim.go
Animation components (Spinner, Pulse, GradientSpinner)
229
internal/tui/draw.go
Drawing utilities (DrawText, DrawStyled, DrawPanel)
122
internal/tui/hints.go
Keybinding hint rendering utilities
99
internal/tui/markdown.go
Markdown rendering via glamour
37
internal/tui/styles.go
Modal title rendering with gradient
35
internal/tui/notes.go
NotesPanel component (grouped notes view)
223
internal/tui/footer.go
Footer navigation bar (unused in main app)
239
internal/tui/interfaces.go
Component interfaces
63
internal/tui/layout.go
Layout calculation logic
85
internal/tui/theme/
Theme system (manager, styles, catppuccin)
—
internal/tui/wizard/
Setup wizard components
—
internal/tui/setup/
Setup flow components
—