---
title: internal/tui
description: Go API reference for the tui package.
---
# tui
```go
import "github.com/jbcom/radioactive-ralph/internal/tui"
```
Package tui implements the read\-only Bubble Tea client described in docs/superpowers/specs/2026\-07\-16\-supervisor\-architecture\-design.md §7: "attach/detach is the wrong nomenclature; running the client simply shows the supervisor's live state." The client never writes to the store and never dispatches work — it only calls read methods on the supervisor's IPC client and the shared store. Three drill\-down levels \(macro/meso/micro\) navigate the same live snapshot.
## Index
- [func IsTerminal\(\) bool](<#IsTerminal>)
- [func Run\(ctx context.Context, source DataSource, opts Options\) error](<#Run>)
- [type DataSource](<#DataSource>)
- [func NewLiveDataSource\(runtimeDir string, st \*store.Store, projectID string\) DataSource](<#NewLiveDataSource>)
- [type Model](<#Model>)
- [func NewModel\(ctx context.Context, source DataSource, projectID string\) Model](<#NewModel>)
- [func \(m Model\) Init\(\) tea.Cmd](<#Model.Init>)
- [func \(m Model\) Update\(msg tea.Msg\) \(tea.Model, tea.Cmd\)](<#Model.Update>)
- [func \(m Model\) View\(\) string](<#Model.View>)
- [type Options](<#Options>)
## func [IsTerminal]()
```go
func IsTerminal() bool
```
IsTerminal reports whether stdout is attached to a real terminal. The client entry point \(cmd/radioactive\_ralph\) MUST check this before calling Run: launching the Bubble Tea program against a pipe or in a non\-interactive CI job would hang waiting for terminal input/output that will never arrive. When it returns false, callers should fall back to a plain status print instead of Run.
## func [Run]()
```go
func Run(ctx context.Context, source DataSource, opts Options) error
```
Run starts the read\-only TUI against source and blocks until the operator quits or ctx is cancelled. Callers MUST verify stdout is a real terminal before calling Run \(see IsTerminal\) — Run itself does not guard against a non\-tty stdout, so that a caller wanting to force a run against a pseudo\-tty in an integration test still can.
## type [DataSource]()
DataSource is every read the TUI needs, gathered behind one interface so Model can be driven by either the real supervisor client \+ shared store or an in\-memory fake in tests. Every method here is READ\-ONLY by contract: this is the enforcement point for the spec's "read\-only" guarantee — Model.Update never calls anything but these methods, and none of them may mutate durable state. \(The real implementation, liveDataSource in live.go, is a thin wrapper: it forwards to \*ipc.Client.Status/Attach and \*store.Store's existing List\*/Get\* read methods — it adds no new write surface of its own.\)
```go
type DataSource interface {
// Status returns the supervisor's current status snapshot (worker
// counts, task counts, recent heartbeat).
Status(ctx context.Context) (ipc.StatusReply, error)
// ListPlans returns the known plans for the project this client is
// scoped to (empty projectID lists across all projects, matching
// store.Store.ListPlans).
ListPlans(ctx context.Context, projectID string) ([]store.Plan, error)
// PlanProgress reports done/total step counts for one plan.
PlanProgress(ctx context.Context, planID string) (orch.Progress, error)
// ListTasks returns a plan's tasks.
ListTasks(ctx context.Context, planID string) ([]store.Task, error)
// ListProjectEvents returns the most recent events across the whole
// project, most recent first.
ListProjectEvents(ctx context.Context, projectID string, limit int) ([]store.Event, error)
// ListTaskEvents returns the most recent events for one task, most
// recent first.
ListTaskEvents(ctx context.Context, planID, taskID string, limit int) ([]store.Event, error)
// MaxEventID returns the highest event id for the client's project (0 if
// none). The model reads it ONCE before the first attach to seed its resume
// cursor, so it owns the cursor end-to-end: a reconnect resumes from the last
// processed id even if the FIRST subscription ended before yielding any frame
// (its internal seed would otherwise be forgotten and the reconnect would
// skip the gap). An error is non-fatal — the model falls back to 0.
MaxEventID(ctx context.Context) (int64, error)
// Attach subscribes to the live event stream from afterID. fn is invoked
// once per event frame until ctx is cancelled or the stream ends. afterID>0
// RESUMES from a known cursor (a reconnect passes the last id it processed,
// so events during the disconnect gap are not missed); afterID<=0 seeds from
// the current max (an initial attach starts from "now", not full history).
// Attach must not block Model's redraw loop — Run wires it up on its own
// goroutine (see model.go).
Attach(ctx context.Context, afterID int64, fn func(json.RawMessage) error) error
}
```
### func [NewLiveDataSource]()
```go
func NewLiveDataSource(runtimeDir string, st *store.Store, projectID string) DataSource
```
NewLiveDataSource builds the production DataSource: runtimeDir is the directory the supervisor's socket lives under \(xdg.StateRoot\(\)\), st is the shared store, and projectID scopes the plan/event reads.
## type [Model]()
Model is the root tea.Model. It holds the current drill level, the read\-only DataSource, and the last\-fetched snapshot; Update handles key events and the periodic refresh tick, View delegates to the per\-level renderer. Model never calls anything on DataSource except its documented read methods — see datasource.go's DataSource doc comment for the read\-only enforcement point.
```go
type Model struct {
// contains filtered or unexported fields
}
```
### func [NewModel]()
```go
func NewModel(ctx context.Context, source DataSource, projectID string) Model
```
NewModel constructs the root model. ctx bounds the whole TUI session — cancelling it \(e.g. on SIGINT\) unwinds any in\-flight Attach goroutine.
### func \(Model\) [Init]()
```go
func (m Model) Init() tea.Cmd
```
Init starts the refresh loop. It fires an IMMEDIATE refresh tick rather than launching a fetch directly, so the very first gather goes through the same in\-flight\-guarded path as every periodic tick \(Init returns a Cmd and cannot set m.fetching, so a direct fetch here could overlap the first periodic tick if the initial gather is slow\).
### func \(Model\) [Update]()
```go
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd)
```
Update handles key events \(arrows/enter to drill in, esc/backspace to drill out, q to quit\) and the periodic refresh tick. This is the surface the model\_test.go table tests exercise directly, injecting tea.KeyMsg values without a real terminal.
### func \(Model\) [View]()
```go
func (m Model) View() string
```
View delegates to the level renderer.
## type [Options]()
Options configures Run.
```go
type Options struct {
// ProjectID scopes the macro view's plan list + event feed. Empty
// lists across all projects.
ProjectID string
// Output/Input override the terminal streams tea.Program attaches
// to; nil defaults to os.Stdout/os.Stdin. Tests that DO drive a real
// tea.Program (none currently do — model_test.go drives Update
// directly) would use this to avoid touching the real terminal.
Output io.Writer
Input io.Reader
}
```
Generated by [gomarkdoc]()