---
name: bubble-tea-tui-builder
description: Build, test, and harden Go Bubble Tea terminal user interfaces with replayable events, responsive rendering, and terminal-safe fallbacks.
license: MIT
tags: [tui, terminal, bubble-tea, go, testing, graphics]
agents: [claude-code, codex, autojack]
category: terminal
metadata:
version: "1.1.0"
capabilities:
network: true
filesystem: readwrite
tools: [Bash, Read, Edit, Write]
requires-secrets: []
resources:
- path: story.md
type: file
---Bubble Tea TUI Builder
When To Use
Use this skill when building or improving a standalone terminal UI in Go,
especially with Bubble Tea, Bubbles, Lip Gloss, local JSONL/Unix-socket
bridges, PTY tests, fixture replay, audio-level meters, latency strips, or
iTerm2 graphics fallbacks.
The goal is a reliable terminal product, not a browser dashboard or React/Ink
patch.
Core Defaults
- Build the runtime boundary first: TUI state comes from a structured event stream, not stdout scraping.
- Keep Bubble Tea
Model,Update, andViewpure enough to test with fixture messages. - Coalesce high-frequency events before they become layout work. Mic/VAD levels should render around 10Hz unless the user explicitly needs more.
- Keep the terminal readable when graphics are unavailable. Every image or rich visual must have a text fallback.
- Prefer bounded histories: transcript turns, timeline entries, diagnostics, and waveform samples must have caps.
- Use one canonical event schema and command schema; do not invent one-off payloads per component.
- Treat visual verification as part of the work. Text replay and passing unit tests are not enough to claim a TUI looks good.
Architecture Pattern
Use these packages unless the repo already has stronger conventions:
cmd/<binary>/main.go: CLI argument parsing and process exit.internal/protocol: event and command envelopes, JSONL encode/decode.internal/bridge: child process launcher, Unix-socket/TCP bridge, command round trips.internal/cockpit: Bubble Tea model, update logic, view rendering.internal/graphics: iTerm2 inline image renderer plus text fallback.fixtures: replayable event streams for offline development.
State rules:
Updateconsumes typed messages and updates one model tree.Viewonly formats existing model state.- Runtime side effects live in commands or bridge adapters, not view code.
- Rendering helpers receive width/height explicitly; no hidden global terminal reads inside layout functions.
Event Contract
When a TUI receives state from another process, use newline-delimited JSON over
a documented local transport. Keep the protocol application-neutral and make
fixture replay independent of live services.
Event envelope:
{
"version": "v1",
"type": "status.changed",
"seq": 1,
"ts": "2026-05-09T00:00:00.000Z",
"session_id": "conv-...",
"source": "service-name",
"payload": {}
}
Command envelope:
{
"version": "v1",
"type": "command",
"id": "cmd-1",
"command": "refresh",
"payload": {}
}
Typical event types:
runtime.ready,runtime.status,runtime.exitstatus.changed,progress.updated,connection.changeditem.created,item.updated,item.completedlatency.snapshottool.start,tool.completediagnostic.entryagent.event,task.event
Typical commands:
refreshpauseresumequit
Testing Workflow
Follow test-first implementation.
- Write protocol tests before protocol code:
- decode valid JSONL envelopes;
- reject malformed lines without crashing replay;
- preserve command ids through ack/error.
- Write model tests before UI behavior:
- apply each required event type;
- cap activity, timeline, and diagnostics history;
- coalesce high-frequency updates to the intended render cadence.
- Write view tests before layout changes:
- narrow, standard, and wide terminal sizes;
- active, paused, busy, diagnostic-error, and disconnect states;
- no empty panels that consume permanent screen space.
- Use Bubble Tea testing support:
- prefer model/update tests for most behavior;
- use
github.com/charmbracelet/x/exp/teatest/v2for rendered model output; - use a PTY/headless terminal harness only for end-to-end keyboard and ANSI behavior.
- Add fixture replay:
<app>-tui replay fixtures/<name>.jsonl;- fixtures should work without live services or external credentials.
- Add visual review artifacts for UI changes:
- use scripted terminal capture, preferably Charm VHS, for repeatable PNG screenshots and GIFs;
- cover standard, narrow, help, diagnostics, active voice, and disconnect states;
- inspect generated artifacts before claiming visual work is complete;
- attach or reference the relevant screenshot when reporting UI changes.
Testing ladder for interactive TUIs:
- Plain model/protocol tests for state and schema.
teatestor PTY/grid tests for key interactions and rendered smoke checks.- Static fixture replay for deterministic text output.
replay --hold, demo mode, or an equivalent interactive PTY run for human inspection.- VHS screenshots/GIFs, or native terminal screenshots as a fallback, for design acceptance.
Visual Rules
- Header: current phase, connection status, and persistent latency when useful.
- Primary pane: the task's main content, with a clear empty state.
- Side/activity pane: operations, progress, and diagnostics that are currently useful.
- Footer: terse keymap only.
- iTerm2 graphics may use inline image escape sequences, but text fallback is mandatory and should be the default in tests.
Research Checklist
Before adding dependencies or adopting outside TUI automation tools:
- Prefer official docs for Bubble Tea, Bubbles, Lip Gloss, teatest, and the target terminal graphics protocol.
- Audit third-party TUI automation projects from source before installing.
- Avoid remote installer shortcuts. Use package managers or source-reviewed builds.
- Record why a tool is used, what it verifies, and what remains covered by plain model tests.
- Prefer VHS for repeatable visual QA when available. It can script terminal size, waits, screenshots, GIFs, and key input; use native macOS/iTerm screenshots only as a local fallback.
Bubble Tea v2 Migration Notes
The v2 stack (stable since Feb 2026) is the current default for new work and
for porting v1 cockpits. The identifiers below are verified against a real
v1→v2 port — each one is a silent break if missed.
- Module paths moved to
charm.land, each with a/v2suffix:charm.land/bubbletea/v2,charm.land/bubbles/v2/...,charm.land/lipgloss/v2(+.../v2/compat,.../v2/tree),charm.land/glamour/v2. teatest is the exception — it stays on GitHub and only gains/v2:github.com/charmbracelet/x/exp/teatest/v2. Do not assume it moved tocharm.landlike the others. spacekey, not" ". v2 delivers the space bar as the key string"space", not a" "rune. A keymap binding built on" "silently stops firing (in a voice cockpit, space is often interrupt — the primary control). Audit every binding string and add a table-driven test that synthesizes a v2KeyPressMsgfor each catalog binding.View() string→View() tea.View. The model'sViewnow returns atea.Viewstruct (cursor/background/layer fields), not a string. Keep the pure string renderer asViewString()and wrap it in a thinView() tea.Viewthat sets alt-screen, mouse mode, and an explicitly hidden cursor. This isolates blast radius and keepsViewString()unit-testable; print sites (e.g.replay) callViewString().- lipgloss v2
Width/Heightare border-inclusive. They now count border and padding inside the given size, where v1 added them outside. Layouts ported verbatim shrink by the border/padding width — recompute pane budgets, and measure rendered width with escape-awareansi.StringWidth, neverlen. AdaptiveColormoved tocharm.land/lipgloss/v2/compat. The v1 top-levellipgloss.AdaptiveColoris gone; the drop-in iscompat.AdaptiveColor{Light, Dark}. The cleaner long-term target isLightDark(isDark)driven bytea.BackgroundColorMsgonce you have the terminal background signal.- teatest/v2 assertions: never assert on escape-sequence goldens. Renderer-version churn makes raw ANSI byte goldens brittle. Assert on
ansi.Strip-ed substrings / stable markers. Under-race, reconstruct the screen from the final frame rather than diffing intermediate frames, which can interleave nondeterministically.
Anti-Patterns
- Scraping runtime stdout for product state.
- Letting every audio frame trigger full layout work.
- Hiding protocol decisions inside view components.
- Adding a graphics-only feature without a text fallback.
- Shipping blank AGENTS/TASKS panels as permanent layout.
- Treating a passing build as proof that a TUI is usable.
- Treating static text replay as proof that a TUI looks good.
- Asking the user to manually open every iteration instead of generating and inspecting repeatable visual artifacts.
- Rebuilding an application's backend logic inside the TUI.