diff --git a/internal/ui/AGENTS.md b/internal/ui/AGENTS.md index 4140bd60820c2799849e2cd6beccaf36d6ef93e2..f3720d7b8867d60b30d00089b3b567ac70fd61ac 100644 --- a/internal/ui/AGENTS.md +++ b/internal/ui/AGENTS.md @@ -2,78 +2,198 @@ ## General Guidelines -- Never use commands to send messages when you can directly mutate children or state. +- Never use commands to send messages when you can directly mutate children + or state. - Keep things simple; do not overcomplicate. - Create files if needed to separate logic; do not nest models. - Never do IO or expensive work in `Update`; always use a `tea.Cmd`. -- Never change the model state inside of a command use messages and than update the state in the main loop -- Use the `github.com/charmbracelet/x/ansi` package for any string manipulation - that might involves ANSI codes. Do not manipulate ANSI strings at byte level! - Some useful functions: - * `ansi.Cut` - * `ansi.StringWidth` - * `ansi.Strip` - * `ansi.Truncate` - +- Never change the model state inside of a command. Use messages and update + the state in the main `Update` loop. +- Use the `github.com/charmbracelet/x/ansi` package for any string + manipulation that might involve ANSI codes. Do not manipulate ANSI strings + at byte level! Some useful functions: + - `ansi.Cut` + - `ansi.StringWidth` + - `ansi.Strip` + - `ansi.Truncate` ## Architecture +### Rendering Pipeline + +The UI uses a **hybrid rendering** approach: + +1. **Screen-based (Ultraviolet)**: The top-level `UI` model creates a + `uv.ScreenBuffer`, and components draw into sub-regions using + `uv.NewStyledString(str).Draw(scr, rect)`. Layout is rectangle-based via + a `uiLayout` struct with fields like `layout.header`, `layout.main`, + `layout.editor`, `layout.sidebar`, `layout.pills`, `layout.status`. +2. **String-based**: Sub-components like `list.List` and `completions` render + to strings, which are painted onto the screen buffer. +3. **`View()`** creates the screen buffer, calls `Draw()`, then + `canvas.Render()` flattens it to a string for Bubble Tea. + ### Main Model (`model/ui.go`) -Keep most of the logic and state in the main model. This is where: -- Message routing happens +The `UI` struct is the top-level Bubble Tea model. Key fields: + +- `width`, `height` — terminal dimensions +- `layout uiLayout` — computed layout rectangles +- `state uiState` — `uiOnboarding | uiInitialize | uiLanding | uiChat` +- `focus uiFocusState` — `uiFocusNone | uiFocusEditor | uiFocusMain` +- `chat *Chat` — wraps `list.List` for the message view +- `textarea textarea.Model` — the input editor +- `dialog *dialog.Overlay` — stacked dialog system +- `completions`, `attachments` — sub-components + +Keep most logic and state here. This is where: + +- Message routing happens (giant `switch msg.(type)` in `Update`) - Focus and UI state is managed - Layout calculations are performed - Dialogs are orchestrated -### Components Should Be Dumb +### Centralized Message Handling + +The `UI` model is the **sole Bubble Tea model**. Sub-components (`Chat`, +`List`, `Attachments`, `Completions`, etc.) do not participate in the +standard Elm architecture message loop. They are stateful structs with +imperative methods that the main model calls directly: + +- **`Chat`** and **`List`** have no `Update` method at all. The main model + calls targeted methods like `HandleMouseDown()`, `ScrollBy()`, + `SetMessages()`, `Animate()`. +- **`Attachments`** and **`Completions`** have non-standard `Update` + signatures (e.g., returning `bool` for "consumed") that act as guards, not + as full Bubble Tea models. +- **Sidebar** is not its own model: it's a `drawSidebar()` method on `UI`. + +When writing new components, follow this pattern: + +- Expose imperative methods for state changes (not `Update(tea.Msg)`). +- Return `tea.Cmd` from methods when side effects are needed. +- Handle rendering via `Render(width int) string` or + `Draw(scr uv.Screen, area uv.Rectangle)`. +- Let the main `UI.Update()` decide when and how to call into the component. -Components should not handle bubbletea messages directly. Instead: -- Expose methods for state changes -- Return `tea.Cmd` from methods when side effects are needed -- Handle their own rendering via `Render(width int) string` +### Chat View (`model/chat.go`) -### Chat Logic (`model/chat.go`) +The `Chat` struct wraps a `list.List` with an ID-to-index map, mouse +tracking (drag, double/triple click), animation management, and a `follow` +flag for auto-scroll. It bridges screen-based and string-based rendering: -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`). +```go +func (m *Chat) Draw(scr uv.Screen, area uv.Rectangle) { + uv.NewStyledString(m.list.Render()).Draw(scr, area) +} +``` + +Individual chat items in `chat/` should be simple renderers that cache their +output and invalidate when data changes (see `cachedMessageItem` in +`chat/messages.go`). ## Key Patterns ### Composition Over Inheritance -Use struct embedding for shared behaviors. See `chat/messages.go` for examples of reusable embedded structs for highlighting, caching, and focus. +Use struct embedding for shared behaviors. See `chat/messages.go` for +examples of reusable embedded structs for highlighting, caching, and focus. + +### Interface Hierarchy + +The chat message system uses layered interface composition: + +- **`list.Item`** — base: `Render(width int) string` +- **`MessageItem`** — extends `list.Item` + `list.RawRenderable` + + `Identifiable` +- **`ToolMessageItem`** — extends `MessageItem` with tool call/result/status + methods +- **Opt-in capabilities**: `Focusable`, `Highlightable`, `Expandable`, + `Animatable`, `Compactable`, `KeyEventHandler` + +Key interface locations: + +- List item interfaces: `list/item.go` +- Chat message interfaces: `chat/messages.go` +- Tool message interfaces: `chat/tools.go` +- Dialog interface: `dialog/dialog.go` -### Interfaces +### Tool Renderers -- List item interfaces are in `list/item.go` -- Chat message interfaces are in `chat/messages.go` -- Dialog interface is in `dialog/dialog.go` +Each tool has a dedicated renderer in `chat/`. The `ToolRenderer` interface +requires: + +```go +RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string +``` + +`NewToolMessageItem` in `chat/tools.go` is the central factory that routes +tool names to specific types: + +| File | Tools rendered | +| --------------------- | ---------------------------------------------- | +| `chat/bash.go` | Bash, JobOutput, JobKill | +| `chat/file.go` | View, Write, Edit, MultiEdit, Download | +| `chat/search.go` | Glob, Grep, LS, Sourcegraph | +| `chat/fetch.go` | Fetch, WebFetch, WebSearch | +| `chat/agent.go` | Agent, AgenticFetch | +| `chat/diagnostics.go` | Diagnostics | +| `chat/references.go` | References | +| `chat/lsp_restart.go` | LSPRestart | +| `chat/todos.go` | Todos | +| `chat/mcp.go` | MCP tools (`mcp_` prefix) | +| `chat/generic.go` | Fallback for unrecognized tools | +| `chat/assistant.go` | Assistant messages (thinking, content, errors) | +| `chat/user.go` | User messages (input + attachments) | ### Styling -- All styles are defined in `styles/styles.go` -- Access styles via `*common.Common` passed to components -- Use semantic color fields rather than hardcoded colors +- All styles are defined in `styles/styles.go` (massive `Styles` struct with + nested groups for Header, Pills, Dialog, Help, etc.). +- Access styles via `*common.Common` passed to components. +- Use semantic color fields rather than hardcoded colors. ### Dialogs -- Implement the dialog interface in `dialog/dialog.go` -- Return message types from `Update()` to signal actions to the main model -- Use the overlay system for managing dialog lifecycle +- Implement the `Dialog` interface in `dialog/dialog.go`: + `ID()`, `HandleMsg()` returning an `Action`, `Draw()` onto `uv.Screen`. +- `Overlay` manages a stack of dialogs with push/pop/contains operations. +- Dialogs draw last and overlay everything else. +- Use `RenderContext` from `dialog/common.go` for consistent layout (title + gradients, width, gap, cursor offset helpers). + +### Shared Context + +The `common.Common` struct holds `*app.App` and `*styles.Styles`. Thread it +through all components that need access to app state or styles. ## File Organization -- `model/` - Main UI model and major components (chat, sidebar, etc.) -- `chat/` - Chat message item types and renderers -- `dialog/` - Dialog implementations -- `list/` - Generic list component with lazy rendering -- `common/` - Shared utilities and the Common struct -- `styles/` - All style definitions -- `anim/` - Animation system -- `logo/` - Logo rendering +- `model/` — Main UI model and major sub-models (chat, sidebar, header, + status, pills, session, onboarding, keys, etc.) +- `chat/` — Chat message item types and tool renderers +- `dialog/` — Dialog implementations (models, sessions, commands, + permissions, API key, OAuth, filepicker, reasoning, quit) +- `list/` — Generic lazy-rendered scrollable list with viewport tracking +- `common/` — Shared `Common` struct, layout helpers, markdown rendering, + diff rendering, scrollbar +- `completions/` — Autocomplete popup with filterable list +- `attachments/` — File attachment management +- `styles/` — All style definitions, color tokens, icons +- `diffview/` — Unified and split diff rendering with syntax highlighting +- `anim/` — Animated spinnner +- `image/` — Terminal image rendering (Kitty graphics) +- `logo/` — Logo rendering +- `util/` — Small shared utilities and message types ## Common Gotchas -- Always account for padding/borders in width calculations -- Use `tea.Batch()` when returning multiple commands -- Pass `*common.Common` to components that need styles or app access +- Always account for padding/borders in width calculations. +- Use `tea.Batch()` when returning multiple commands. +- Pass `*common.Common` to components that need styles or app access. +- The `list.List` only renders visible items (lazy). No render cache exists + at the list level — items should cache internally if rendering is + expensive. +- Dialog messages are intercepted first in `Update` before other routing. +- Focus state determines key event routing: `uiFocusEditor` sends keys to + the textarea, `uiFocusMain` sends them to the chat list.