GAIA Terminal UI — Developer Manual
For working on the TUI, or driving it from a test or an assistant. Using it day to day is the user manual.What it is made of
Three processes, and knowing which one you are looking at saves most of the debugging time:gaia init --check, which is Python and talks to Lemonade.
Building
Needsgit, Go, and a Python env with gaia installed.
test, test-race, lint, vet, cover, mock-agent,
dev (build + mock-agent), clean.
For a dev run against your checkout:
--dev shows per-turn timings, steps, and tool arguments on screen;
--trace=<path> writes the same events to JSONL instead. They are independent.
Point it at your checkout, not the published sidecar
mode: user runs the published frozen sidecar, which is routinely older
than source. When testing an agent from a checkout, set the matching mode on the
TUI launch so its ensure request agrees:
user creates
a mode conflict. GET /daemon/v1/agents shows the api_version actually in
use.
Switching models on the relayed route
On the subprocess route/model <id> is a turn: the agent’s stdio loop
intercepts it and confirms with a model-state ping. The relay has no such turn,
so /model, /provider and the startup restore go through
client.TurnModelSwitcher instead: the TUI checks the flagship’s contract
(2.14 or newer moves a retained session in place), checks that Lemonade lists
the id, then sends it as model on every following /query. The first final
on it saves the choice; a 409 could not switch session puts the previous model
back (client.ModelSwitchRefusedError).
Know which branch you built
Stacked branches are siblings; no single branch has everything. A leaf build shows behaviour already fixed elsewhere — that has produced bug reports for things fixed on another branch. Build from a merged integration branch, or state explicitly which slice you tested. The header’sagent gaia v<x> and the welcome line’s GAIA core <y> come from
the running agent only: the agent_version and gaia_version fields of its
model-state status ping (stdio), or the peer’s own version (HTTP). Never from
~/.gaia/agents/<id>/.installed. No version on screen means the agent that
answered predates those fields, which is itself the answer to “which build?”.
Patched dependencies
tui/third_party/ holds patched copies of upstream modules, wired in with
replace in tui/go.mod; its README.md lists each patch and why. Bubble Tea
is one of them: v1.3.10’s Windows console reader drops the Ctrl state on
Backspace and Delete, so the copy reports Ctrl+Backspace as ctrl+h and adds a
ctrl+delete key (also mapped from ESC[3;5~ on VT terminals). Upgrading Bubble
Tea means re-applying the lines marked GAIA patch, or dropping the copy once
upstream has the fix.
Status and error lines
A status or error line is one sentence on what is wrong plus the one next step. Daemon errors implementdaemon.Briefer; render daemon.Brief(err) on the line
and keep err.Error() (route, alternatives, log path) for a d details pane. In
--dev, harness lines the user view hides (Step 4/50) show as they are, with
no [harness] tag.
The control API
Off by default.--control auto-assigns a port; --control-port N pins one. It
binds 127.0.0.1 only and is bearer-token authenticated.
The token is written to $GAIA_TUI_HOME/control.json (mode 0600), defaulting to
~/.gaia/tui/control.json. Set GAIA_TUI_HOME to keep parallel sessions from
colliding.
Endpoints
All under/control/v1/.
text types into the composer but does not submit. Follow it with
{"keys":["enter"]}.
Wait, never sleep
POST /control/v1/wait blocks server-side until a condition holds, with
timeout_ms:
sleep is either a wasted minute or a
half-rendered capture.
{"state": {"streaming": false}} can match the instant before a turn starts,
because it was not streaming yet either. Submit, confirm streaming went true,
and only then wait for it to go false — or poll status until it reports false.State values
view is splash · preflight · chat · unknown. There is no hub screen:
the TUI boots into one agent’s chat behind the readiness gate.
On preflight, blocker names the row refusing the launch: binary,
lemonade, model, daemon, sidecar, mailbox. Wait on
{"state": {"view": "chat"}} rather than a screen substring.
A daemon or sidecar row that is simply not running is started by the gate
itself, once per launch (Row.AutoFix in internal/ui/preflight). While that
runs the row is [..] starting… and there is no blocker; only a start that
failed, a foreign daemon record, or a remote LEMONADE_BASE_URL leaves it
waiting on f. To reproduce a reboot, stop the daemon you started for the
drive (GAIA_DAEMON_HOME set) and relaunch.
esc quits on preflight (there is no screen behind it) and clears the
composer on an idle chat. With the d details pane open, it closes the pane
first.
Copy and selection keys
The terminal owns the mouse by default, so a plain drag is the terminal’s own selection and the app never sees it. That selection can’t scroll the transcript, which is why the app has its own.
Copied text is plain: each message’s span records its chrome (
msgSpan.gutter,
bodyStart/bodyEnd), and a message covered end to end is copied from its
source markdown. The control API’s mouse actions are single-point gestures, so
a drag can’t be driven through it; selection_test.go covers it, decoding the
OSC 52 payload. OSC 52 is write-only, so a screen capture can’t confirm a copy.
Giving it a terminal
The TUI queries terminal size and colour support at startup, so it needs a real pty. Two things that do not work from a tool call or CI step:script -q /dev/null ...— fails withtcgetattr/ioctl: Operation not supported on socketwhen stdout is a socket.osascript -e 'tell application "Terminal" to do script ...'— can returnAppleEvent timed out (-1712)and leave nothing running.
resize first in any drive script for the same reason.
Colour
GET /screen?format=ansi and count \x1b: dozens on a styled frame, zero means
the colour profile degraded at launch. On Windows, launch through wt.exe — a
cmd.exe window spawned by Start-Process lands in legacy conhost, reports no
colour support, and the TUI then emits no escape sequences at all. It reads as
“syntax highlighting is broken” but nothing is broken.
Capture mistakes that produce false bug reports
- Always capture the whole screen. A cropped capture once produced a fabricated “uninstall silently does nothing” — the status line is the second-to-last row.
- Check which screen you are on before sending keys. Keys sent to the wrong screen do nothing and read as “the binding is broken”.
- Grep for the right words. The confirmation prompt’s first line is a
risk badge (
READ,WRITE,RUNS CODE,DESTRUCTIVE) and a question. A tool the TUI knows gets a plain question — a shell command readsRUNS CODE Run this command?, a file writeWRITE Write this file?— and any other tool readsAllow <tool>?. There is noConfirm: <tool>line. format=plainstrips ANSI, so it cannot answer a colour question.screencaptureis blocked on some machines; use the control API text.
Logs and traces
The agent log is where tool calls live, with arguments and results:
TOOL_LOADER lines record which tools were selected for each turn — the
first thing to check when the agent claims a capability “isn’t available”.
Running a local model on a dev box
The embedded Lemonade is self-contained and does not need a system install:gaia init --check is the exact gate the preflight screen uses, and it names
the missing model id:
Related
- User manual
tui/README.md— install and binaries- CLI reference