on main help sign in
chore: update and improve ui/AGENTS.md
commit53ca4dc3e9eb84fffc9d7128a764bddbd5973b2b authorChristian Rocha <christian@rocha.is> dateMon 9 Mar 2026, 12:25 (7 months ago) parentcfcbd69
1 file changed+161−41
internal/ui/AGENTS.md+161−41
internal/ui/AGENTS.md+161−41
@@ -2,78 +2,198 @@
22
33## General Guidelines
44
5−- Never use commands to send messages when you can directly mutate children or state.
5+- Never use commands to send messages when you can directly mutate children
6+ or state.
67- Keep things simple; do not overcomplicate.
78- Create files if needed to separate logic; do not nest models.
89- Never do IO or expensive work in `Update`; always use a `tea.Cmd`.
9−- Never change the model state inside of a command use messages and than update the state in the main loop
10−- Use the `github.com/charmbracelet/x/ansi` package for any string manipulation
11− that might involves ANSI codes. Do not manipulate ANSI strings at byte level!
12− Some useful functions:
13− * `ansi.Cut`
14− * `ansi.StringWidth`
15− * `ansi.Strip`
16− * `ansi.Truncate`
17−
10+- Never change the model state inside of a command. Use messages and update
11+ the state in the main `Update` loop.
12+- Use the `github.com/charmbracelet/x/ansi` package for any string
13+ manipulation that might involve ANSI codes. Do not manipulate ANSI strings
14+ at byte level! Some useful functions:
15+ - `ansi.Cut`
16+ - `ansi.StringWidth`
17+ - `ansi.Strip`
18+ - `ansi.Truncate`
1819
1920## Architecture
2021
22+### Rendering Pipeline
23+
24+The UI uses a **hybrid rendering** approach:
25+
26+1. **Screen-based (Ultraviolet)**: The top-level `UI` model creates a
27+ `uv.ScreenBuffer`, and components draw into sub-regions using
28+ `uv.NewStyledString(str).Draw(scr, rect)`. Layout is rectangle-based via
29+ a `uiLayout` struct with fields like `layout.header`, `layout.main`,
30+ `layout.editor`, `layout.sidebar`, `layout.pills`, `layout.status`.
31+2. **String-based**: Sub-components like `list.List` and `completions` render
32+ to strings, which are painted onto the screen buffer.
33+3. **`View()`** creates the screen buffer, calls `Draw()`, then
34+ `canvas.Render()` flattens it to a string for Bubble Tea.
35+
2136### Main Model (`model/ui.go`)
2237
23−Keep most of the logic and state in the main model. This is where:
24−- Message routing happens
38+The `UI` struct is the top-level Bubble Tea model. Key fields:
39+
40+- `width`, `height` — terminal dimensions
41+- `layout uiLayout` — computed layout rectangles
42+- `state uiState` — `uiOnboarding | uiInitialize | uiLanding | uiChat`
43+- `focus uiFocusState` — `uiFocusNone | uiFocusEditor | uiFocusMain`
44+- `chat *Chat` — wraps `list.List` for the message view
45+- `textarea textarea.Model` — the input editor
46+- `dialog *dialog.Overlay` — stacked dialog system
47+- `completions`, `attachments` — sub-components
48+
49+Keep most logic and state here. This is where:
50+
51+- Message routing happens (giant `switch msg.(type)` in `Update`)
2552- Focus and UI state is managed
2653- Layout calculations are performed
2754- Dialogs are orchestrated
2855
29−### Components Should Be Dumb
56+### Centralized Message Handling
57+
58+The `UI` model is the **sole Bubble Tea model**. Sub-components (`Chat`,
59+`List`, `Attachments`, `Completions`, etc.) do not participate in the
60+standard Elm architecture message loop. They are stateful structs with
61+imperative methods that the main model calls directly:
62+
63+- **`Chat`** and **`List`** have no `Update` method at all. The main model
64+ calls targeted methods like `HandleMouseDown()`, `ScrollBy()`,
65+ `SetMessages()`, `Animate()`.
66+- **`Attachments`** and **`Completions`** have non-standard `Update`
67+ signatures (e.g., returning `bool` for "consumed") that act as guards, not
68+ as full Bubble Tea models.
69+- **Sidebar** is not its own model: it's a `drawSidebar()` method on `UI`.
70+
71+When writing new components, follow this pattern:
72+
73+- Expose imperative methods for state changes (not `Update(tea.Msg)`).
74+- Return `tea.Cmd` from methods when side effects are needed.
75+- Handle rendering via `Render(width int) string` or
76+ `Draw(scr uv.Screen, area uv.Rectangle)`.
77+- Let the main `UI.Update()` decide when and how to call into the component.
3078
31−Components should not handle bubbletea messages directly. Instead:
32−- Expose methods for state changes
33−- Return `tea.Cmd` from methods when side effects are needed
34−- Handle their own rendering via `Render(width int) string`
79+### Chat View (`model/chat.go`)
3580
36−### Chat Logic (`model/chat.go`)
81+The `Chat` struct wraps a `list.List` with an ID-to-index map, mouse
82+tracking (drag, double/triple click), animation management, and a `follow`
83+flag for auto-scroll. It bridges screen-based and string-based rendering:
3784
38−Most chat-related logic belongs here. Individual chat items in `chat/` should be simple renderers that cache their output and invalidate when data changes (see `cachedMessageItem` in `chat/messages.go`).
85+```go
86+func (m *Chat) Draw(scr uv.Screen, area uv.Rectangle) {
87+ uv.NewStyledString(m.list.Render()).Draw(scr, area)
88+}
89+```
90+
91+Individual chat items in `chat/` should be simple renderers that cache their
92+output and invalidate when data changes (see `cachedMessageItem` in
93+`chat/messages.go`).
3994
4095## Key Patterns
4196
4297### Composition Over Inheritance
4398
44−Use struct embedding for shared behaviors. See `chat/messages.go` for examples of reusable embedded structs for highlighting, caching, and focus.
99+Use struct embedding for shared behaviors. See `chat/messages.go` for
100+examples of reusable embedded structs for highlighting, caching, and focus.
101+
102+### Interface Hierarchy
103+
104+The chat message system uses layered interface composition:
105+
106+- **`list.Item`** — base: `Render(width int) string`
107+- **`MessageItem`** — extends `list.Item` + `list.RawRenderable` +
108+ `Identifiable`
109+- **`ToolMessageItem`** — extends `MessageItem` with tool call/result/status
110+ methods
111+- **Opt-in capabilities**: `Focusable`, `Highlightable`, `Expandable`,
112+ `Animatable`, `Compactable`, `KeyEventHandler`
113+
114+Key interface locations:
115+
116+- List item interfaces: `list/item.go`
117+- Chat message interfaces: `chat/messages.go`
118+- Tool message interfaces: `chat/tools.go`
119+- Dialog interface: `dialog/dialog.go`
45120
46−### Interfaces
121+### Tool Renderers
47122
48−- List item interfaces are in `list/item.go`
49−- Chat message interfaces are in `chat/messages.go`
50−- Dialog interface is in `dialog/dialog.go`
123+Each tool has a dedicated renderer in `chat/`. The `ToolRenderer` interface
124+requires:
125+
126+```go
127+RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string
128+```
129+
130+`NewToolMessageItem` in `chat/tools.go` is the central factory that routes
131+tool names to specific types:
132+
133+| File | Tools rendered |
134+| --------------------- | ---------------------------------------------- |
135+| `chat/bash.go` | Bash, JobOutput, JobKill |
136+| `chat/file.go` | View, Write, Edit, MultiEdit, Download |
137+| `chat/search.go` | Glob, Grep, LS, Sourcegraph |
138+| `chat/fetch.go` | Fetch, WebFetch, WebSearch |
139+| `chat/agent.go` | Agent, AgenticFetch |
140+| `chat/diagnostics.go` | Diagnostics |
141+| `chat/references.go` | References |
142+| `chat/lsp_restart.go` | LSPRestart |
143+| `chat/todos.go` | Todos |
144+| `chat/mcp.go` | MCP tools (`mcp_` prefix) |
145+| `chat/generic.go` | Fallback for unrecognized tools |
146+| `chat/assistant.go` | Assistant messages (thinking, content, errors) |
147+| `chat/user.go` | User messages (input + attachments) |
51148
52149### Styling
53150
54−- All styles are defined in `styles/styles.go`
55−- Access styles via `*common.Common` passed to components
56−- Use semantic color fields rather than hardcoded colors
151+- All styles are defined in `styles/styles.go` (massive `Styles` struct with
152+ nested groups for Header, Pills, Dialog, Help, etc.).
153+- Access styles via `*common.Common` passed to components.
154+- Use semantic color fields rather than hardcoded colors.
57155
58156### Dialogs
59157
60−- Implement the dialog interface in `dialog/dialog.go`
61−- Return message types from `Update()` to signal actions to the main model
62−- Use the overlay system for managing dialog lifecycle
158+- Implement the `Dialog` interface in `dialog/dialog.go`:
159+ `ID()`, `HandleMsg()` returning an `Action`, `Draw()` onto `uv.Screen`.
160+- `Overlay` manages a stack of dialogs with push/pop/contains operations.
161+- Dialogs draw last and overlay everything else.
162+- Use `RenderContext` from `dialog/common.go` for consistent layout (title
163+ gradients, width, gap, cursor offset helpers).
164+
165+### Shared Context
166+
167+The `common.Common` struct holds `*app.App` and `*styles.Styles`. Thread it
168+through all components that need access to app state or styles.
63169
64170## File Organization
65171
66−- `model/` - Main UI model and major components (chat, sidebar, etc.)
67−- `chat/` - Chat message item types and renderers
68−- `dialog/` - Dialog implementations
69−- `list/` - Generic list component with lazy rendering
70−- `common/` - Shared utilities and the Common struct
71−- `styles/` - All style definitions
72−- `anim/` - Animation system
73−- `logo/` - Logo rendering
172+- `model/` — Main UI model and major sub-models (chat, sidebar, header,
173+ status, pills, session, onboarding, keys, etc.)
174+- `chat/` — Chat message item types and tool renderers
175+- `dialog/` — Dialog implementations (models, sessions, commands,
176+ permissions, API key, OAuth, filepicker, reasoning, quit)
177+- `list/` — Generic lazy-rendered scrollable list with viewport tracking
178+- `common/` — Shared `Common` struct, layout helpers, markdown rendering,
179+ diff rendering, scrollbar
180+- `completions/` — Autocomplete popup with filterable list
181+- `attachments/` — File attachment management
182+- `styles/` — All style definitions, color tokens, icons
183+- `diffview/` — Unified and split diff rendering with syntax highlighting
184+- `anim/` — Animated spinnner
185+- `image/` — Terminal image rendering (Kitty graphics)
186+- `logo/` — Logo rendering
187+- `util/` — Small shared utilities and message types
74188
75189## Common Gotchas
76190
77−- Always account for padding/borders in width calculations
78−- Use `tea.Batch()` when returning multiple commands
79−- Pass `*common.Common` to components that need styles or app access
191+- Always account for padding/borders in width calculations.
192+- Use `tea.Batch()` when returning multiple commands.
193+- Pass `*common.Common` to components that need styles or app access.
194+- The `list.List` only renders visible items (lazy). No render cache exists
195+ at the list level — items should cache internally if rendering is
196+ expensive.
197+- Dialog messages are intercepted first in `Update` before other routing.
198+- Focus state determines key event routing: `uiFocusEditor` sends keys to
199+ the textarea, `uiFocusMain` sends them to the chat list.