Documentation
¶
Overview ¶
Package ux's PhaseRunner is the sequential phase checklist primitive that sits below the cell-open banner (Banner) and above the structured debug info table (KV). CELL-262.
Design separation between the three ux primitives:
- Banner — header. One row. Identity (cell · project #cellID).
- KV — structured info table. N rows of key/value pairs, debug-gated.
- PhaseRunner — sequential step list. N rows of `✓ name [— detail] <elapsed>`.
Each does ONE job; callers compose them top-to-bottom.
Cooked-mode constraint ¶
PhaseRunner intentionally uses the same ProgressSpinner model as everywhere else in this package: a goroutine ticker that writes `\r\033[K` for line-clearing, never a Bubble Tea program. The reason: cell-open hands the TTY off to `docker exec -it ... claude` at the end. Anything that puts the terminal in raw mode or alt screen (i.e. tea.NewProgram) risks leaving claude with a broken stdio if the lifecycle isn't perfectly drained. Cooked mode is the boring, proven shape.
Sequencing ¶
Phases run synchronously, one at a time. If a phase needs to fan out internally that's fine; PhaseRunner itself never renders concurrent rows (cursor-positioning gymnastics break under interleaved stderr writes).
Wire format ¶
The success row format is `✓ <name>[ — <detail>] <elapsed>` — same as ProgressSpinner.Success because PhaseRunner is a thin convenience wrapper around it. CELL-261's FormatSecretsPhase produces strings that drop into the `<detail>` slot verbatim.
Index ¶
- Constants
- Variables
- func Banner(cell, project, cellID string) string
- func CloseDebugLog()
- func Debugf(format string, a ...any)
- func Deprecated(w io.Writer, subject, hint string)
- func DeprecatedAt(w io.Writer, subject, hint, source string)
- func DeprecatedLine(w io.Writer, msg string)
- func FailMsg(message string)
- func FormatSecretsPhase(count, failed int) (string, bool)
- func GetConfirmation(message string) (bool, error)
- func GetSelectionKV(message string, options []SelectOption) (string, error)
- func HumanBytes(n int64) string
- func Info(message string)
- func InitDebugLog(projectDir, commandName string)
- func InteractiveTable(headers []string, rows [][]string, sortHandler func(key SortKey) [][]string)
- func KV(keyWidth int, key, value string) string
- func ParseStatus(s string) (streak.Status, bool)
- func PrintData(v any)
- func PrintTable(headers []string, rows [][]string)
- func SaveCursor()
- func SortKeyString(key SortKey) string
- func SuccessMsg(message string)
- func SuccessRow(title string, elapsed time.Duration)
- func Warn(message string)
- type BootEvent
- type BootPanel
- func (p *BootPanel) Apply(ev BootEvent) error
- func (p *BootPanel) Clear()
- func (p *BootPanel) ClearBelowGroups()
- func (p *BootPanel) Finish(text string)
- func (p *BootPanel) FinishError(text string)
- func (p *BootPanel) Message(text string, s streak.Status)
- func (p *BootPanel) Phase(name string, fn func() error) error
- func (p *BootPanel) PhaseDetailed(name string, fn func() (string, error)) error
- func (p *BootPanel) PhaseDetailedRunning(running, finalName string, fn func() (string, error)) error
- func (p *BootPanel) PhaseDetailedRunningWarn(running, finalName string, fn func() (string, bool, error)) error
- func (p *BootPanel) PhaseDetailedWarn(name string, fn func() (string, bool, error)) error
- func (p *BootPanel) PromotePending()
- func (p *BootPanel) PromoteRunning()
- func (p *BootPanel) Seal(name string)
- func (p *BootPanel) SetBannerRows(n int)
- func (p *BootPanel) SetBoot(group, step string, s streak.Status, title string)
- func (p *BootPanel) Snapshot() *streak.Grid
- func (p *BootPanel) UpdateText(message string)
- func (p *BootPanel) WarnBoot(msg string)
- type BootView
- type BuildErrorHint
- type EventPanel
- func (p *EventPanel) Bind(s EventSink) error
- func (p *EventPanel) ClearBelowGroups()
- func (p *EventPanel) Events() []BootEvent
- func (p *EventPanel) Finish(text string)
- func (p *EventPanel) FinishError(text string)
- func (p *EventPanel) Finished() (finished bool, failed bool)
- func (p *EventPanel) Message(text string, s streak.Status)
- func (p *EventPanel) Phase(name string, fn func() error) error
- func (p *EventPanel) PhaseDetailed(name string, fn func() (string, error)) error
- func (p *EventPanel) PhaseDetailedRunning(running, finalName string, fn func() (string, error)) error
- func (p *EventPanel) PhaseDetailedRunningWarn(running, finalName string, fn func() (string, bool, error)) error
- func (p *EventPanel) PhaseDetailedWarn(name string, fn func() (string, bool, error)) error
- func (p *EventPanel) PromotePending()
- func (p *EventPanel) PromoteRunning()
- func (p *EventPanel) Seal(name string)
- func (p *EventPanel) SetBoot(group, step string, s streak.Status, title string)
- func (p *EventPanel) SinkErr() error
- func (p *EventPanel) Unbind()
- func (p *EventPanel) UpdateText(message string)
- func (p *EventPanel) WarnBoot(msg string)
- type EventSink
- type PanelGroup
- type PhaseRunner
- func (p *PhaseRunner) Phase(name string, fn func() error) error
- func (p *PhaseRunner) PhaseDetailed(name string, fn func() (detail string, err error)) error
- func (p *PhaseRunner) PhaseDetailedRunning(running, finalName string, fn func() (detail string, err error)) error
- func (p *PhaseRunner) PhaseDetailedRunningWarn(running, finalName string, fn func() (detail string, warn bool, err error)) error
- func (p *PhaseRunner) PhaseDetailedWarn(name string, fn func() (detail string, warn bool, err error)) error
- func (p *PhaseRunner) Seal(name string)
- func (p *PhaseRunner) UpdateText(message string)
- type Phaser
- type ProgressSpinner
- func (ps *ProgressSpinner) Fail(message string) *ProgressSpinner
- func (ps *ProgressSpinner) Stop()
- func (ps *ProgressSpinner) Success(message string) *ProgressSpinner
- func (ps *ProgressSpinner) UpdateText(message string) *ProgressSpinner
- func (ps *ProgressSpinner) Warn(message string) *ProgressSpinner
- type SelectOption
- type SortKey
Constants ¶
const ( EventGroups = "groups" EventRunning = "running" // Step, Text = running label EventDone = "done" // Step, Detail EventFail = "fail" // Step, Text = error EventWarn = "warn" // Step, Detail EventSet = "set" // Group, Step, Status, Text = title EventMessage = "message" // Text, Status (footer) EventPromotePending = "promote_pending" EventPromoteRunning = "promote_running" EventFinish = "finish" // Text EventFinishError = "finish_error" // Text EventClearBelow = "clear_below" )
Event kinds. EventGroups is always first: it carries the panel layout.
Variables ¶
var ( StyleInfo = lipgloss.NewStyle().Foreground(colorInfo) StyleSuccess = lipgloss.NewStyle().Foreground(colorSuccess) StyleError = lipgloss.NewStyle().Foreground(colorError) StyleWarning = lipgloss.NewStyle().Foreground(colorWarning) StyleDebug = lipgloss.NewStyle().Foreground(colorDebug) StyleMuted = lipgloss.NewStyle().Foreground(colorMuted) StyleBold = lipgloss.NewStyle().Bold(true) StyleSection = lipgloss.NewStyle().Bold(true) StyleAccent = lipgloss.NewStyle().Foreground(colorAccent) // TableBorder is the default style for lipgloss/table borders. TableBorder = lipgloss.NewStyle().Foreground(colorBorder) )
Styles — exported so commands can reuse instead of defining their own.
var ErrUserAborted = huh.ErrUserAborted
ErrUserAborted is returned when the user presses Esc during a prompt.
var LogPlainText bool
LogPlainText disables spinners and uses plain logger output when true. Set before using any ux functions (e.g. when not a TTY or in CI).
var OutputFormat = "text"
OutputFormat controls how PrintTable and PrintData emit output. Values: "text" (default lipgloss table), "json", "yaml".
var Verbose bool
Verbose enables streaming of build output to stdout instead of suppressing it. Implies LogPlainText. Set by --debug.
Functions ¶
func Banner ¶
Banner renders the cell-open header — a single-line identity strip used at the top of every `cell <command>` invocation:
CELL > devcell-304
Workspace name is bold + brand orange; the separator and cell ID are muted. Two segments are suppressed when they carry no signal:
- workspace name empty (single-pane default `main`)
- cellID "0" or "" (the no-multiplexer fallback; surfacing "-0" is noise).
func CloseDebugLog ¶
func CloseDebugLog()
CloseDebugLog flushes and closes the debug log file opened by InitDebugLog.
func Deprecated ¶
Deprecated prints one deprecation row on w, aligned with the other status rows (same ⚠ prefix as ProgressSpinner.Warn):
⚠ <subject> is deprecated (<hint>)
w is normally stderr so --format json/yaml stdout stays parseable.
func DeprecatedAt ¶
DeprecatedAt is Deprecated with the source the deprecated thing came from (a config file name) appended:
⚠ <subject> is deprecated (<hint>) (<source>)
func DeprecatedLine ¶
DeprecatedLine renders a canonical deprecation message (see deprecatedPhrase) through Deprecated. Messages in any other shape are printed as-is behind the warning marker.
func FormatSecretsPhase ¶
FormatSecretsPhase renders the post-resolution detail for the Secrets phase row and signals whether it should be a warning. Partial failures (some resolved, some failed) produce a warning with a short tag prefix so the panel's short-note extraction can derive "N failed" for the completion summary.
func GetConfirmation ¶
GetConfirmation shows an interactive confirmation prompt (defaults to true).
func GetSelectionKV ¶
func GetSelectionKV(message string, options []SelectOption) (string, error)
GetSelectionKV shows an interactive selection with separate display labels and values. Returns the Value of the selected option.
func HumanBytes ¶
HumanBytes formats a byte count as "1.4 GB" / "237 MB" / "812 KB" / "42 B". Uses 1024-based units (KiB/MiB/GiB semantics) but the conventional abbreviations users expect from `docker images`.
func Info ¶
func Info(message string)
Info prints an info-styled message.
Lines use \r\n rather than \n because cell-open writes rows AFTER `docker run -it` has put the host TTY into raw mode (ONLCR cleared). In raw mode a bare \n moves the cursor down one row but leaves the column where it was, producing a staircase. \r\n is correct in both raw and cooked modes (cooked mode's ONLCR only translates lone \n; an explicit CR passes through unchanged).
func InitDebugLog ¶
func InitDebugLog(projectDir, commandName string)
InitDebugLog opens a timestamped log file under projectDir/.devcell/debug/ when Verbose is set. All subsequent Debugf calls write to both stdout and this file. No-op when Verbose is false.
func InteractiveTable ¶
func InteractiveTable( headers []string, rows [][]string, sortHandler func(key SortKey) [][]string, )
InteractiveTable displays headers+rows in an interactive bubbles/table TUI. sortHandler is called when the user presses a sort key (r/s/z/p) and should return the re-sorted rows. Falls back to PrintTable for non-TTY or non-text mode.
func KV ¶
KV renders an aligned key-value row for the cell-open detail block. keyWidth is the *key column width* — the value column starts at `keyWidth + 2` (2-space gap). Keys longer than keyWidth still get a minimum 2-space gap so nothing collides. Pure — no global state.
func ParseStatus ¶
ParseStatus maps a streak.Status name back to the value.
func PrintData ¶
func PrintData(v any)
PrintData serialises any Go value in the current OutputFormat. Use this when commands build typed structs (e.g. for models output). In text mode it falls back to JSON so the caller always gets parseable output.
func PrintTable ¶
PrintTable renders headers+rows in the current OutputFormat. text: lipgloss bordered table. json/yaml: array of objects keyed by header.
func SaveCursor ¶
func SaveCursor()
SaveCursor emits DEC save-cursor (\x1b7) to stderr. Call before the banner so Clear can restore to that position and erase everything the panel wrote. No-op in plain/verbose mode.
func SortKeyString ¶
SortKeyString converts a SortKey to the string value passed to RankModels sortBy.
func SuccessMsg ¶
func SuccessMsg(message string)
SuccessMsg prints a success-styled message (standalone, not spinner).
func SuccessRow ¶
SuccessRow prints a permanent `✓ <title> <elapsed>` row without a spinner: the same shape ProgressSpinner.Success lands, for callers that measured the phase themselves (boot events from inside the container).
Types ¶
type BootEvent ¶
type BootEvent struct {
Kind string `json:"kind"`
Group string `json:"group,omitempty"`
Step string `json:"step,omitempty"`
Text string `json:"text,omitempty"`
Detail string `json:"detail,omitempty"`
Status string `json:"status,omitempty"`
Groups []PanelGroup `json:"groups,omitempty"`
}
BootEvent is one panel mutation on the wire (JSON lines). Kind selects which fields matter; Status is a streak.Status name (see ParseStatus).
type BootPanel ¶
type BootPanel struct {
// contains filtered or unexported fields
}
BootPanel renders startup progress as a streak-chart panel. Each group is a row; each step within a group is a dot (column) that transitions Pending -> Running -> Done/Error/Warning.
Implements Phaser so it can be used as a drop-in for PhaseRunner.
func NewBootPanel ¶
func NewBootPanel(groups []PanelGroup) *BootPanel
NewBootPanel creates a panel from the given groups and starts the Loader.
func (*BootPanel) Apply ¶
Apply replays one event onto the panel. Unknown kinds are an error so a protocol mismatch between devcell versions shows up instead of leaving a dot grey.
func (*BootPanel) Clear ¶
func (p *BootPanel) Clear()
Clear restores the cursor to the position saved by SaveCursor and erases from there to the end of the screen, removing the banner and panel output. No-op in plain/verbose mode.
func (*BootPanel) ClearBelowGroups ¶
func (p *BootPanel) ClearBelowGroups()
ClearBelowGroups restores the saved cursor, moves past the banner, group rows and rule, then erases everything below (issue lines, footer message). The agent starts on a clean line right after the rule. No-op in plain/verbose mode.
Vertical layout from the saved cursor position:
line 0: banner (1 line) line 1..N: group rows (len(groups) lines) line N+1: rule (1 line) line N+2: footer/issues ← erase from here
func (*BootPanel) FinishError ¶
FinishError stops the Loader with an error status.
func (*BootPanel) Phase ¶
Phase runs fn under the named step. Sets the dot to Running while fn executes, then Done on success or Error on failure.
func (*BootPanel) PhaseDetailed ¶
PhaseDetailed is like Phase but the callback returns a detail string.
func (*BootPanel) PhaseDetailedRunning ¶
func (p *BootPanel) PhaseDetailedRunning(running, finalName string, fn func() (string, error)) error
PhaseDetailedRunning shows a different label while running vs the final row.
func (*BootPanel) PhaseDetailedRunningWarn ¶
func (p *BootPanel) PhaseDetailedRunningWarn(running, finalName string, fn func() (string, bool, error)) error
PhaseDetailedRunningWarn combines PhaseDetailedRunning and PhaseDetailedWarn.
func (*BootPanel) PhaseDetailedWarn ¶
PhaseDetailedWarn is like PhaseDetailed but the callback also returns a warn bool. When true, the dot renders as Warning instead of Done.
func (*BootPanel) PromotePending ¶
func (p *BootPanel) PromotePending()
PromotePending sets every Pending dot to Done. Call when attaching to an already-running container so Boot/Services/Environment groups show green instead of grey.
func (*BootPanel) PromoteRunning ¶
func (p *BootPanel) PromoteRunning()
PromoteRunning sets every Running dot to Done. Call when an external signal (e.g. boot.ready) confirms all services are up, even if some never emitted their own "up" event (longruns that exec into a daemon).
func (*BootPanel) SetBannerRows ¶
SetBannerRows tells ClearBelowGroups how many lines precede the group rows. Default 1 (the identity banner); the cage hides it and passes 0.
func (*BootPanel) UpdateText ¶
UpdateText changes the footer message while a phase is running.
type BootView ¶
type BootView interface {
Phaser
Message(text string, s streak.Status)
SetBoot(group, step string, s streak.Status, title string)
WarnBoot(msg string)
PromotePending()
PromoteRunning()
Finish(text string)
FinishError(text string)
ClearBelowGroups()
}
BootView is everything the engines call on the boot panel. BootPanel draws it on the tty; EventPanel records it as BootEvents for a renderer in another process (CELL-657: the cage's tmux popup).
type BuildErrorHint ¶
BuildErrorHint describes a user-facing explanation and fix for a known build failure.
func ClassifyBuildOutput ¶
func ClassifyBuildOutput(output string) *BuildErrorHint
ClassifyBuildOutput scans docker build output for known error patterns and returns a user-facing hint. Returns nil when no pattern matches.
type EventPanel ¶
type EventPanel struct {
// contains filtered or unexported fields
}
EventPanel is a BootView that records every mutation and forwards it to at most one sink. Events emitted before Bind are buffered and replayed on Bind, so a renderer that attaches late sees the whole history. Phase* run their callbacks exactly like BootPanel does.
func NewEventPanel ¶
func NewEventPanel(groups []PanelGroup) *EventPanel
NewEventPanel starts the event log with the layout event.
func (*EventPanel) Bind ¶
func (p *EventPanel) Bind(s EventSink) error
Bind replays the buffered events into s, then streams new ones to it. Any previously bound sink stops receiving events.
func (*EventPanel) ClearBelowGroups ¶
func (p *EventPanel) ClearBelowGroups()
func (*EventPanel) Events ¶
func (p *EventPanel) Events() []BootEvent
Events returns a copy of everything emitted so far.
func (*EventPanel) Finish ¶
func (p *EventPanel) Finish(text string)
func (*EventPanel) FinishError ¶
func (p *EventPanel) FinishError(text string)
func (*EventPanel) Finished ¶
func (p *EventPanel) Finished() (finished bool, failed bool)
Finished reports whether a finish or finish_error event was emitted.
func (*EventPanel) PhaseDetailed ¶
func (p *EventPanel) PhaseDetailed(name string, fn func() (string, error)) error
func (*EventPanel) PhaseDetailedRunning ¶
func (p *EventPanel) PhaseDetailedRunning(running, finalName string, fn func() (string, error)) error
func (*EventPanel) PhaseDetailedRunningWarn ¶
func (*EventPanel) PhaseDetailedWarn ¶
func (*EventPanel) PromotePending ¶
func (p *EventPanel) PromotePending()
func (*EventPanel) PromoteRunning ¶
func (p *EventPanel) PromoteRunning()
func (*EventPanel) Seal ¶
func (p *EventPanel) Seal(name string)
func (*EventPanel) SetBoot ¶
func (p *EventPanel) SetBoot(group, step string, s streak.Status, title string)
func (*EventPanel) SinkErr ¶
func (p *EventPanel) SinkErr() error
SinkErr returns the first error a sink reported, if any.
func (*EventPanel) Unbind ¶
func (p *EventPanel) Unbind()
Unbind detaches the current sink; events keep buffering.
func (*EventPanel) UpdateText ¶
func (p *EventPanel) UpdateText(message string)
func (*EventPanel) WarnBoot ¶
func (p *EventPanel) WarnBoot(msg string)
type EventSink ¶
EventSink receives BootEvents in order. BootPanel is one (local replay); the cage adds a JSON-lines writer over a socket.
type PanelGroup ¶
PanelGroup defines one row in the panel: a label and its ordered steps.
type PhaseRunner ¶
type PhaseRunner struct {
// contains filtered or unexported fields
}
PhaseRunner owns the sequential phase list above the resumed parent spinner. Zero-value is usable; no constructor needed.
func (*PhaseRunner) Phase ¶
func (p *PhaseRunner) Phase(name string, fn func() error) error
Phase runs fn under a per-phase ProgressSpinner. On nil-error, lands a permanent `✓ <name> <elapsed>` row and returns nil. On error, lands a permanent `✗ <name> — <err>` row and returns the error verbatim so the caller can propagate or recover with errors.Is.
func (*PhaseRunner) PhaseDetailed ¶
func (p *PhaseRunner) PhaseDetailed(name string, fn func() (detail string, err error)) error
PhaseDetailed is the variant for phases that compute a permanent-line suffix on success (e.g. "Loading secrets" returning "7 resolved", "Image pin" returning the short SHA). Empty detail string omits the trailing " — " so single-value rows stay clean.
On error, behaves like Phase: lands `✗ <name> — <err>` and returns the error. The detail value is discarded in the error path because the row already carries the error message.
func (*PhaseRunner) PhaseDetailedRunning ¶
func (p *PhaseRunner) PhaseDetailedRunning(running, finalName string, fn func() (detail string, err error)) error
PhaseDetailedRunning is like PhaseDetailed but renders a different label while the spinner is active vs the permanent ✓/✗ row. Use for phases whose in-progress text carries a user prompt that no longer applies once the phase completes (e.g. "Loading secrets (please authorize 1Password)" while running, "Loading secrets — 7 resolved" once done).
Identical behavior to PhaseDetailed on the final row: `✓ <finalName>` / `✓ <finalName> — <detail>` / `✗ <finalName> — <err>`.
func (*PhaseRunner) PhaseDetailedRunningWarn ¶
func (p *PhaseRunner) PhaseDetailedRunningWarn(running, finalName string, fn func() (detail string, warn bool, err error)) error
PhaseDetailedRunningWarn combines PhaseDetailedRunning and PhaseDetailedWarn: a different label while running, and the callback can signal a warning.
func (*PhaseRunner) PhaseDetailedWarn ¶
func (p *PhaseRunner) PhaseDetailedWarn(name string, fn func() (detail string, warn bool, err error)) error
PhaseDetailedWarn is like PhaseDetailed but the callback also returns a warn bool. When warn is true, the permanent row renders ⚠ (warning) instead of ✓ (success).
func (*PhaseRunner) Seal ¶
func (p *PhaseRunner) Seal(name string)
Seal lands the final ✓ row before the host→container handoff. Explicit boundary marker — the call site that follows Seal is the docker exec that takes the TTY. Distinct from Phase because there's no work to wrap; the runner just emits the row.
In practice the caller writes:
pr.Seal("Cell ready")
return execClaude(ctx, ...) // takes the TTY from the row below
func (*PhaseRunner) UpdateText ¶
func (p *PhaseRunner) UpdateText(message string)
UpdateText changes the in-progress spinner text. Safe to call from the closure passed to Phase/PhaseDetailed/PhaseDetailedRunning. No-op if no phase is currently running.
type Phaser ¶
type Phaser interface {
Phase(name string, fn func() error) error
PhaseDetailed(name string, fn func() (detail string, err error)) error
PhaseDetailedWarn(name string, fn func() (detail string, warn bool, err error)) error
PhaseDetailedRunning(running, finalName string, fn func() (detail string, err error)) error
PhaseDetailedRunningWarn(running, finalName string, fn func() (detail string, warn bool, err error)) error
UpdateText(message string)
Seal(name string)
}
Phaser is the interface for sequential phase renderers. Both PhaseRunner (scrolling log) and BootPanel (streak-chart dot panel) implement it.
type ProgressSpinner ¶
type ProgressSpinner struct {
// contains filtered or unexported fields
}
ProgressSpinner displays an animated spinner with a message. In plain-text mode it falls back to simple log lines.
func NewProgressSpinner ¶
func NewProgressSpinner(message string) *ProgressSpinner
NewProgressSpinner creates and starts a spinner, or logs the message if in plain-text mode.
func (*ProgressSpinner) Fail ¶
func (ps *ProgressSpinner) Fail(message string) *ProgressSpinner
Fail stops the spinner and prints a failure message.
func (*ProgressSpinner) Stop ¶
func (ps *ProgressSpinner) Stop()
Stop clears the spinner without leaving any output.
func (*ProgressSpinner) Success ¶
func (ps *ProgressSpinner) Success(message string) *ProgressSpinner
Success stops the spinner and prints a success message.
func (*ProgressSpinner) UpdateText ¶
func (ps *ProgressSpinner) UpdateText(message string) *ProgressSpinner
UpdateText updates the spinner text or prints the message.
func (*ProgressSpinner) Warn ¶
func (ps *ProgressSpinner) Warn(message string) *ProgressSpinner
Warn stops the spinner and prints a warning row (⚠ instead of ✓).
type SelectOption ¶
SelectOption pairs a display label with a value for typed selection.