ux

package
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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

View Source
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

View Source
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.

View Source
var ErrUserAborted = huh.ErrUserAborted

ErrUserAborted is returned when the user presses Esc during a prompt.

View Source
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).

View Source
var OutputFormat = "text"

OutputFormat controls how PrintTable and PrintData emit output. Values: "text" (default lipgloss table), "json", "yaml".

View Source
var Verbose bool

Verbose enables streaming of build output to stdout instead of suppressing it. Implies LogPlainText. Set by --debug.

Functions

func Banner(cell, project, cellID string) string

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 Debugf

func Debugf(format string, a ...any)

Debugf prints a formatted debug message when Verbose (--debug) is enabled.

func Deprecated

func Deprecated(w io.Writer, subject, hint string)

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

func DeprecatedAt(w io.Writer, subject, hint, source string)

DeprecatedAt is Deprecated with the source the deprecated thing came from (a config file name) appended:

⚠      <subject> is deprecated (<hint>) (<source>)

func DeprecatedLine

func DeprecatedLine(w io.Writer, msg string)

DeprecatedLine renders a canonical deprecation message (see deprecatedPhrase) through Deprecated. Messages in any other shape are printed as-is behind the warning marker.

func FailMsg

func FailMsg(message string)

FailMsg prints a failure row (standalone, not spinner).

func FormatSecretsPhase

func FormatSecretsPhase(count, failed int) (string, bool)

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

func GetConfirmation(message string) (bool, error)

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

func HumanBytes(n int64) string

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

func KV(keyWidth int, key, value string) string

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

func ParseStatus(s string) (streak.Status, bool)

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

func PrintTable(headers []string, rows [][]string)

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

func SortKeyString(key SortKey) string

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

func SuccessRow(title string, elapsed time.Duration)

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).

func Warn

func Warn(message string)

Warn prints a warning row (same ⚠ prefix as ProgressSpinner.Warn).

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

func (p *BootPanel) Apply(ev BootEvent) error

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) Finish

func (p *BootPanel) Finish(text string)

Finish stops the Loader and draws the final frame.

func (*BootPanel) FinishError

func (p *BootPanel) FinishError(text string)

FinishError stops the Loader with an error status.

func (*BootPanel) Message

func (p *BootPanel) Message(text string, s streak.Status)

Message sets the footer message text.

func (*BootPanel) Phase

func (p *BootPanel) Phase(name string, fn func() error) error

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

func (p *BootPanel) PhaseDetailed(name string, fn func() (string, error)) error

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

func (p *BootPanel) PhaseDetailedWarn(name string, fn func() (string, bool, error)) error

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) Seal

func (p *BootPanel) Seal(name string)

Seal marks a step as Done without running work.

func (*BootPanel) SetBannerRows

func (p *BootPanel) SetBannerRows(n int)

SetBannerRows tells ClearBelowGroups how many lines precede the group rows. Default 1 (the identity banner); the cage hides it and passes 0.

func (*BootPanel) SetBoot

func (p *BootPanel) SetBoot(group, step string, s streak.Status, title string)

SetBoot updates a boot event step by its group and step name.

func (*BootPanel) Snapshot

func (p *BootPanel) Snapshot() *streak.Grid

Snapshot returns a copy of the underlying grid for inspection (tests).

func (*BootPanel) UpdateText

func (p *BootPanel) UpdateText(message string)

UpdateText changes the footer message while a phase is running.

func (*BootPanel) WarnBoot

func (p *BootPanel) WarnBoot(msg string)

WarnBoot shows a warning in the footer (for warnings not tied to a step).

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

type BuildErrorHint struct {
	Title string
	Body  string
	Fixes []string
}

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) Message

func (p *EventPanel) Message(text string, s streak.Status)

func (*EventPanel) Phase

func (p *EventPanel) Phase(name string, fn func() error) error

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 (p *EventPanel) PhaseDetailedRunningWarn(running, finalName string, fn func() (string, bool, error)) error

func (*EventPanel) PhaseDetailedWarn

func (p *EventPanel) PhaseDetailedWarn(name string, fn func() (string, bool, error)) error

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

type EventSink interface {
	Apply(BootEvent) error
}

EventSink receives BootEvents in order. BootPanel is one (local replay); the cage adds a JSON-lines writer over a socket.

type PanelGroup

type PanelGroup struct {
	Label string
	Steps []string
}

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

type SelectOption struct {
	Label string
	Value string
}

SelectOption pairs a display label with a value for typed selection.

type SortKey

type SortKey string

SortKey identifies the column being sorted in the interactive table.

const (
	SortRecommended SortKey = "r"
	SortSWE         SortKey = "s"
	SortSpeed       SortKey = "z"
	SortSize        SortKey = "p"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL