Last verified: 2026-02-10
Detailed patterns for Bubble Tea TUI development in the Render CLI.
For boundaries (Never/Ask/Safe) and escalation guidance, see the main AGENTS.md.
The CLI uses the Elm Architecture:
Message → Update(model) → View(model) → Render
Every component implements: Init(), Update(msg tea.Msg), View()
The StackModel in stack.go manages view navigation with breadcrumbs.
// Push returns a tea.Cmd - must be returned from Update()
cmd := stack.Push(ModelWithCmd{
Model: myModel,
Cmd: "render services list", // For clipboard
Breadcrumb: "Services",
})
return m, cmd// Name messages with Action + Msg suffix
type LoadingDataMsg struct{}
type DataLoadedMsg struct{ Data []Item }
type ErrorMsg struct{ Err error }Never block in Update(). Use commands for I/O:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m, m.fetchData() // Return command, don't block
case DataLoadedMsg:
m.data = msg.Data
return m, nil
}
return m, nil
}
func (m Model) fetchData() tea.Cmd {
return func() tea.Msg {
data, err := m.repo.List()
if err != nil {
return ErrorMsg{Err: err}
}
return DataLoadedMsg{Data: data}
}
}Always delegate updates to child components:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
m.table, cmd = m.table.Update(msg)
cmds = append(cmds, cmd)
m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}// Log to file (stdout is occupied by TUI)
f, _ := tea.LogToFile("debug.log", "debug")
defer f.Close()tail -f debug.log # Watch logs in another terminal
go test ./pkg/tui/... # Run TUI testsIf terminal breaks after crash, run reset.
Test patterns: Table-driven tests with stretchr/testify, manual fakes in testhelper/
func TestMyView(t *testing.T) {
fake := &testhelper.FakeDimensionModel{Value: "test"}
model := NewMyModel(fake)
// Assert on View() output or model state
}Use pkg/style/ for consistent styling. Never hardcode dimensions—use lipgloss.Height() and lipgloss.Width().
title := style.Title.Render("My Title")
height := lipgloss.Height(rendered)- Blocking in
Update()- use commands for async work - Forgetting to handle
tea.KeyCtrlCandtea.KeyCtrlD - Not returning the
tea.CmdfromPush() - Hardcoding dimensions instead of using lipgloss
- Message ordering from concurrent commands is undefined