goa

TUI (Terminal User Interface)

Goa’s TUI is an ANSI-native terminal UI engine with differential rendering, viewport management, and a component tree. It wraps the agentic SDK for LLM interaction and provides keyboard-driven chat, markdown rendering, scrollable history, and a status bar with animated spinner.

Architecture

Goa’s TUI is a compositor MVC. Concerns are strictly separated:

Controller (input keys + agent events) ──► Model (Conversation + Scene)
                                                  │
                           ┌──────────────────────┼──────────────────────┐
                           ▼                      ▼                      ▼
                     Compositor             AgentView                tests
                  (terminal bytes)       (plain text for AI)
┌──────────────────────────────────────────────────────────────┐
│  Header: goa coding agent v0.1                               │
│  Ctrl+C/D exit  |  / commands  |  Tab complete  |  ↑↓ history│
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ChatViewport (scrollable message history)                   │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │ • Connected to LM Studio (qwen3.5-9b)                   │ │
│  │                                                         │ │
│  │ User: hello                                             │ │
│  │ ┌─────────────────────────────────────────────────────┐ │ │
│  │ │ Assistant response...                               │ │ │
│  │ └─────────────────────────────────────────────────────┘ │ │
│  │                                                         │ │
│  │ ◉ bash ls -la                                           │ │
│  │   ← [bash: ls -la]\nExit: 0\n...                       │  │
│  └─────────────────────────────────────────────────────────┘ │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│  StatusBar: ⠋ thinking                                       │
├──────────────────────────────────────────────────────────────┤
│  Input area (editor, grows up to 12 lines max)               │
├──────────────────────────────────────────────────────────────┤
│  Footer: ~/dev/goa (⎇ main)              coder │ YOLO        │
│          ↑120 ↓80  200/4096 tok           qwen3.5-9b         │
└──────────────────────────────────────────────────────────────┘

Key Components

TUI Engine (tui/tui.go)

The TUI orchestrates components into a Scene and delegates output to the Compositor. It manages:

engine := tui.NewTUI(terminal)   // engine owns a Compositor bound to terminal
engine.AddChild(header)
engine.AddChild(chatViewport)
engine.AddChild(editor)
engine.SetFocus(editor)
engine.Start()
// engine.AgentFrame() / engine.VisibleText() → ANSI-free view for AI tooling

Terminal (tui/terminal.go)

ProcessTerminal implements raw-mode I/O with:

Component Tree

Components implement the Component interface:

type Component interface {
    Render(width int) []string
    HandleInput(data string)
    Invalidate()
}

Standard components: | Component | File | Description | |———–|——|————-| | Header | tui/header.go | App name, version, keybinding hints | | ChatViewport | tui/chat_viewport.go | Scrollable message history with typed messages | | Editor | tui/editor.go | Multi-line text input with undo, kill-ring, autocomplete | | Footer | tui/footer.go | Status bar with workdir, git branch, model, stats | | StatusMsg | tui/status.go | Ephemeral status line with animated spinner | | Separator | tui/box.go | Horizontal line across the terminal width | | Selector | tui/selector.go | Interactive item picker overlay | | SelectList | tui/select_list.go | Scrollable list for completion popups |

Input Handling

The ProcessTerminal.readLoop goroutine reads raw bytes from stdin, decodes them through StdinBuffer (which handles partial escape sequences, Kitty CSI-u, bracketed paste), and forwards decoded key strings to TUI.handleKey. The key is routed to the focused component’s HandleInput.

Rendering Pipeline

  1. renderNow() acquires the TUI mutex
  2. renderTree() calls each component’s Render(width), concatenating lines
  3. Overlays are composited on top of the base content
  4. clipToViewport() slices visible portion for terminal height
  5. diffRender() compares against previous frame, writes changed lines using ANSI positioning (\x1b[N;1H\x1b[K...)
  6. extractCursorPos() finds the hardware cursor marker (\x1b_pi:c\x07) emitted by the focused component
  7. positionCursor() places the terminal cursor for IME support

Scrolling

Markdown Rendering

Markdown in assistant messages is rendered by MDStreamRenderer (tui/markdown.go) which handles:

Goa Text Panel

All goa-originated text (slash-command output such as /help, /docs, /hotkeys, /goal:status) is rendered inside a bordered goa panel (renderGoaPanel in tui/chat_viewport_components.go). The panel:

This is the global convention for goa-authored output across the tool.

Information ordering — when a command both needs user input and shows context, the input title is presented first (on the editor top border) and the supporting goa text is shown as a panel immediately after, avoiding redundant or ambiguous bubbles.

Input discipline

All user text input must flow through the main input zone (the persistent Editor), never through a throwaway overlay Input.

  1. Hosts capture input via requestMainInput(prompt, onSubmit) (or the core.Context.RequestMainInput callback), which sets the editor title (top border) to the prompt so the user always knows what is being asked.
  2. A modal/card rendered inside the conversation viewport may accompany the prompt to display richer context (title, summary, options), but the typing still happens in the main editor — the card itself never captures keys.
  3. When a default/seed value is needed, call Editor.SetText(current) before registering the request (the review handlers in events.go use this idiom).
  4. Empty submit (or Ctrl+C) cancels the pending request; the request’s onCancel restores prior UI state.

Rationale: the main editor carries history, kill-yank, autocomplete, undo, and the live title cue. Spawning a separate Input overlay (ShowInput) forfeits all of these and produces a second, inconsistent input region. The legacy ShowInput/ShowInputFunc overlay path is therefore deprecated for free-text capture and retained only as a transition shim; new code must not use it for user input.

Autocomplete

The Editor supports inline autocomplete via a Completer interface. Tab triggers completion; Enter accepts and submits. Down/Up cycle through candidates when the popup is active.

Event Flow

Agent SDK → OutputEvent → AgentManager.OnEvent → TUI events channel
  → handleAgentOutputEvent → chat.AddMessage / statusMsg.Show / footer updates
  → renderNow()

Dependencies

The TUI has no external dependencies beyond: