| name | tui-components |
| description | Use when building or restyling strike's TUI — views, panels, modals, pickers, badges, dashboards, status rows, or any layout work in internal/frontend/tui. Covers the internal/frontend/tui/ui component catalog and internal/frontend/tui/theme tokens. Do not use for backend/engine work (internal/engine, internal/provider, internal/tool, internal/permission, internal/persist/session, internal/product/auth, internal/frontend/host) — those packages have no UI surface. |
TUI components (strike-cli)
internal/frontend/tui/theme is the resolved token source and internal/frontend/tui/ui is the
component library. Root views compose completed theme styles and ui
components, then perform only structural operations (joining, allocation,
selection, and layout). Keep this catalog synchronized with internal/frontend/tui/ui.
Boundaries
- Edit
internal/frontend/tui/app/_src/<group>/ only for package tui. Flattened
internal/frontend/tui/app/*.go are gitignored and regenerated by
go generate ./internal/frontend/tui/app / make / CI — edits there are silently lost.
Real packages theme, ui, term, common are edited in place.
internal/frontend/tui/... may import only internal/protocol, internal/frontend/host,
internal/frontend/tui/..., stdlib, and third-party packages. The boundary test
enforces this. Kit packages (ui, theme, common, term) must not
import protocol or host. Charm modules: v1 github.com/charmbracelet/… or v2
charm.land/… — never github.com/charmbracelet/…/v2.
internal/frontend/tui/ui may import only stdlib, lipgloss, bubbles,
charmbracelet/x/ansi, and internal/frontend/tui/theme; components do not import
the app model or handle tea.Msg. The Strike wordmark lives in the app.
- Resolve a supplied theme before reading tokens:
th = th.Resolve(). Colors,
glyphs, borders, spacing, and emphasis come only from that resolved theme.
Never put literal colors, visual glyphs, border/spacing values, or visual
modifiers in a root view. A ui visual-modifier argument must trace to a
resolved theme value; manually review unknown or interprocedural origins.
- Components are pure renderers: exported rendering functions take a
theme.Theme and options and return a string. State and key handling remain
in internal/frontend/tui/app model files.
Component catalog
Tone is ToneDefault, ToneAccent, ToneAccentAlt, ToneSuccess,
ToneWarning, ToneError, ToneDanger, or ToneMuted. Level is
LevelInfo, LevelSuccess, LevelWarning, or LevelError.
| Component | Exact signature | Use |
|---|
| Panel | Panel(th theme.Theme, opts PanelOpts, body string) string | Width-safe framed tile. PanelOpts has Title, Footer, mandatory Width, optional Height, Focused, Dim, Tone, and Borderless. Default chrome is bordered (outline only, square ┌┐└┘). Focused bordered panes: BorderFocus outline, no title-edge wash, no FocusBar. Soft (chrome: soft) keeps surface-filled body + rounded ╭╮╰╯; focused soft panes: body stays Surface, title edge uses SurfaceFocus, outline uses BorderFocus. Solid chrome keeps title-edge SurfaceFocus + thin FocusBar. Never a full-panel wash. TextSelection is a separate role. Tone dialogs keep elevated SurfaceFocus body. Soft degrades below width 6. Borderless omits chrome. |
| Panel geometry | InnerWidth(width int) int; PanelInnerWidth(th theme.Theme, width int) int; PanelInnerHeight(width, height int) int; PanelInnerHeightFor(th, width, height int) int; PanelContentOrigin(th theme.Theme, width int) (x, y int) | Body dimensions and content-cell origin under chrome. Themed callers must use PanelInnerWidth; InnerWidth is default-theme compatibility only. PanelInnerHeight uses default bordered chrome; use PanelInnerHeightFor for non-default themes. |
| Dialog | Dialog(th theme.Theme, opts DialogOpts, body string) string | Focused Panel with a muted final hint. DialogOpts: Title, Hint, Width, Height, Tone. Body lines longer than inner width are word-wrapped (idempotent if already wrapped). |
| Badge | Badge(th theme.Theme, tone Tone, text string) string |
Dialog word-wraps body lines to PanelInnerWidth; Panel still truncates any
over-long line as a safety net. Prefer pre-wrapping free text for clarity.
List truncates by default; set Wrap: true for multi-line option bodies
(question modal). Card wraps its own body. Size panel-backed child windows
with ui.PanelInnerHeight / PanelInnerHeightFor rather than unconditionally
subtracting border rows, so unbordered narrow panels retain their full height.
Theme tokens
Theme.Resolve fills every unset role from theme.Default(). It preserves
only theme.NoBackground() as the explicit transparent-background choice;
otherwise Background resolves to a solid lipgloss.TerminalColor.
| Token | Role |
|---|
Text, TextMuted | primary and secondary foreground |
OverlayScrim | de-emphasized modal background (scrim) fill |
Accent, AccentAlt, Highlight | primary, secondary, and selected emphasis |
Success, Warning, Error, Danger | semantic state colors |
Background | application background (lipgloss.TerminalColor) |
Surface, SurfaceFocus, SurfaceMuted | panel fills: body default / title-edge focus (and tone dialogs) / dim |
Border, BorderFocus, BorderMuted | soft/bordered frame colors |
UserLabel, ToolLabel, DiffAdded, DiffRemoved | transcript and diff roles |
Chrome | ChromeBordered (default, square ┌┐└┘), ChromeSolid, or ChromeSoft (opt-in rounded) |
BorderStyle | soft/bordered panel border weight and six glyphs |
Spacing | None, XS, SM, MD, LG layout gaps; left|right pane gutter uses XS (keeps 93-col split); bento/welcome card gaps use SM; Label is the gap between a numbered permission-choice shortcut (for example, 1)) and its label, defaulting to 1 when resolved |
Icons | glyph set below |
AgentState | runtime status coloring via Theme.AgentStateColor / (not a palette field) |
Icons fields are Prompt, Assistant, Tool, OK, Err, Info,
Agent, Bolt, Dot, Cursor, InputCursor, FilterCursor, ToolGuide,
FocusBar,
BadgeLeft, BadgeRight, DetailSeparator, Ellipsis, LogoTopRule,
LogoBottomRule, MeterFill, MeterEmpty, TreeExpanded,
TreeCollapsed, CheckboxOn ([x]), CheckboxOff ([ ]), and Sparkline
(low→high bar runes). Stock badge delimiters are empty (soft pills). Use
th.Icons, never the literal glyph.
theme.AgentState is the live session/agent status vocabulary for dynamic
coloring: Ready → Success, Working → AccentAlt, Attention →
Warning, Error → Error, and reserved Dead → TextMuted (unmapped by
reducers until dead-session lifecycle exists). Use
th.AgentStateColor/AgentStateStyle/AgentStateStrongStyle; never hardcode
state colors in views. Spinner uses the working token (AccentAlt).
th.S() returns semantic styles: Text, Muted, Accent, AccentAlt,
Title, Success, Warning, Error, Danger; their *Strong variants;
Selected, SelectedUnderline; UserLabel, AssistantLabel, ToolLabel;
Input, InputPrompt, InputPlaceholder, InputCursor, Spinner;
Border, BorderFocus, BorderMuted; DiffAdded, DiffRemoved,
DiffAddedStrong, DiffRemovedStrong (bold word-diff spans); and
DiffAddedLine, DiffRemovedLine (role FG on SurfaceMuted wash). Call
it once per render and reuse it.
BorderStyle selects BorderWeightUnset, BorderWeightLight, or
BorderWeightHeavy; resolving selects the matching preset and fills invalid
or missing one-cell glyphs. NewSpacing(xs, sm, md, lg) Spacing initializes
XS, SM, MD, and LG, including explicit zero values. It does not
initialize Label, so Theme.Resolve supplies the default Label value of
1. WithXS(v int) Spacing, WithSM(v int) Spacing, WithMD(v int) Spacing,
WithLG(v int) Spacing, and WithLabel(v int) Spacing set their respective
token and preserve an explicit zero. Literal nonzero fields, including
Label, are inferred as set; use the constructor or corresponding With
method when an explicit zero must be preserved.
Configure Bubble widgets from the resolved foreground/cursor/spinner styles
and glyphs, without setting widget backgrounds. ui.Canvas owns background
painting after root views and overlays compose, filling every cell unless the
theme explicitly uses theme.NoBackground().
Recipes
body := ui.List(th, ui.ListOpts{
Items: items, Cursor: m.cursor,
Width: max(1, ui.PanelInnerWidth(th, width)),
Visible: providerModalVisible, Empty: "no providers configured",
})
return ui.Dialog(th, ui.DialogOpts{Title: "Select provider", Width: width}, body)
The empty-transcript dashboard is app composition, not a component API. Its
header owns the compact brand; the dashboard directly allocates fixed-height
Panel cards (bordered chrome by default), with one or two columns according to
available width. It has no outer welcome panel or logo card. The keys card is
always present; get started is present only when no provider is selected or
the selected provider is unauthenticated; agents & skills requires at least
one valid configured agent or skill; and recent prompts requires history.
Preserve those conditions and the short-view fallback when changing the
dashboard, without exposing app-private helpers as component recipes.
Extending and verification
Add a component under internal/frontend/tui/ui, make it width-safe and zero-value
tolerant, document it, add its production-appropriate coverage, and update
this catalog in the same change. New colors, glyphs, borders, spacing, and
emphasis belong in theme.
go test ./internal/frontend/tui/... -count=1
make test
For visual review, the opt-in gallery renders parameterized scenarios rather
than every view at both 80×24 and 120×40. Its current matrix is: 80×24 left
dashboard and right context; 93×40 canonical split; 92×60 left-only; 120×40
split; 120×80 cycle, permission modal, provider picker, command palette, and
busy transcript; and 160×45 long-data/status and danger-modal cases:
STRIKE_GALLERY=1 go test ./internal/frontend/tui/app/ -run Gallery -count=1 -v
Review the constrained 80×24 dashboard, split and modal scenarios, and the
long-data cases appropriate to the change.