tui

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// MaxInputLines caps the textarea height when the user types multiple lines.
	MaxInputLines = 8

	// PaletteMaxVisible caps simultaneously visible slash-command suggestions.
	PaletteMaxVisible = 8

	// ToolResultMaxLines caps how many lines of a tool result are shown in scrollback
	// before collapsing into "… +N lines".
	ToolResultMaxLines = 5

	// ToolStreamTailLines is the window (in lines) kept visible while a tool streams
	// live output in the bottom-pinned area.
	ToolStreamTailLines = 8
)
View Source
const ConnectorPad = "   "

ConnectorPad matches TreeConnector's width (3 cells) for continuation lines.

View Source
const RecentCompletedTTL = 30 * time.Second

RecentCompletedTTL is how long a freshly-completed task stays pinned at the top of the truncated task tree before sinking to the bottom group.

View Source
const TreeConnector = "⎿  "

TreeConnector is the result connector: U+23BF (⎿) plus two spaces for alignment under the tool header.

Variables

View Source
var (
	// Bullet — green for successful tool call, red for failure.
	ToolIconStyle  = lipgloss.NewStyle().Foreground(Success)
	ErrorIconStyle = lipgloss.NewStyle().Foreground(Danger)

	// Tool name — bold only, default foreground. Lets terminal theme show through.
	ToolNameStyle = lipgloss.NewStyle().Bold(true)

	// Tool args and result body intentionally carry no color — they inherit the
	// terminal's default foreground so the user's theme stays in charge.
	ToolArgsStyle   = lipgloss.NewStyle()
	ToolResultStyle = lipgloss.NewStyle()

	ToolPathStyle = lipgloss.NewStyle().Foreground(Path)
)
View Source
var (
	// Assistant bullet — pure white on dark / pure black on light, bold.
	// Uses Strong rather than the terminal default so contrast against the
	// Subtle-gray thinking bullet is guaranteed under any terminal palette.
	AssistantIconStyle = lipgloss.NewStyle().Foreground(Strong).Bold(true)

	// Thinking bullet — dim gray, same family as the italic thinking body.
	ThinkingIconStyle = lipgloss.NewStyle().Foreground(Subtle)
	ThinkingBodyStyle = lipgloss.NewStyle().Foreground(Subtle).Italic(true)

	ReplyLabelStyle = lipgloss.NewStyle().Foreground(RoleAssistant)

	SystemMsgStyle = lipgloss.NewStyle().Foreground(Muted).Italic(true)

	QueuedMsgStyle = lipgloss.NewStyle().Foreground(Muted).Italic(true)
)
View Source
var (
	ErrorStyle   = lipgloss.NewStyle().Foreground(Danger).Bold(true)
	CommandStyle = lipgloss.NewStyle().Foreground(Info)
	MutedStyle   = lipgloss.NewStyle().Foreground(Muted)
	TokenStyle   = lipgloss.NewStyle().Foreground(Muted)
)
View Source
var (
	DiffAddGutterStyle    = lipgloss.NewStyle().Foreground(Success).Background(DiffAddBg)
	DiffRemoveGutterStyle = lipgloss.NewStyle().Foreground(Danger).Background(DiffRemoveBg)

	DiffAddBodyStyle    = lipgloss.NewStyle().Background(DiffAddBg)
	DiffRemoveBodyStyle = lipgloss.NewStyle().Background(DiffRemoveBg)

	// Word-level intra-line emphasis: deeper bg shade in the same hue, no fg
	// override. The body's foreground (or future syntax highlighting) flows
	// through; only the background tells the eye "this part actually changed".
	DiffAddInverseStyle    = lipgloss.NewStyle().Background(DiffAddBgStrong)
	DiffRemoveInverseStyle = lipgloss.NewStyle().Background(DiffRemoveBgStrong)

	// /diff's --stat bar: plain foreground sigils (no background fill, unlike
	// the edit-result rows above) for the per-file +/- change graph.
	DiffStatAddStyle    = lipgloss.NewStyle().Foreground(Success)
	DiffStatRemoveStyle = lipgloss.NewStyle().Foreground(Danger)
)

The whole row — gutter + body — sits on the same colored band so the diff reads as a single visual unit. Gutter additionally carries a fg so the line number / sigil stand out; body has bg only, leaving the existing foreground (syntax highlighting, path tokens) untouched.

View Source
var (
	SeparatorStyle = lipgloss.NewStyle().Foreground(Separator)
	BoxBorderStyle = lipgloss.NewStyle().Foreground(Border)

	CardTitleStyle   = lipgloss.NewStyle().Foreground(Title).Bold(true)
	CardSectionStyle = lipgloss.NewStyle().Foreground(BrandSoft).Bold(true)

	// ConnectorStyle dims the tree connector "⎿  " so it recedes visually.
	ConnectorStyle = lipgloss.NewStyle().Foreground(Subtle)
)
View Source
var (
	InputPanelStyle      = inputPanel(InputRule)
	ShellInputPanelStyle = inputPanel(RoleShell)

	// ShellAccentStyle colors both the prompt caret "❯" and the "!" prefix
	// when the input is in shell mode — they share the same style by design.
	ShellAccentStyle = lipgloss.NewStyle().Foreground(RoleShell).Bold(true)

	InputHintStyle = lipgloss.NewStyle().Foreground(Muted)

	ImageSelectedStyle = lipgloss.NewStyle().Reverse(true)
	ImageTagStyle      = lipgloss.NewStyle().Foreground(Brand)
)
View Source
var (
	WelcomeTitleStyle  = lipgloss.NewStyle().Foreground(Title).Bold(true)
	WelcomeKickerStyle = lipgloss.NewStyle().Foreground(Brand).Bold(true)
	WelcomeBodyStyle   = lipgloss.NewStyle().Foreground(Text)
	WelcomeMutedStyle  = lipgloss.NewStyle().Foreground(Muted)
)
View Source
var (
	CommandPaletteSelectedStyle     = lipgloss.NewStyle().Foreground(Brand).Bold(true)
	CommandPaletteItemStyle         = lipgloss.NewStyle().Foreground(Text)
	CommandPaletteDescStyle         = lipgloss.NewStyle().Foreground(Muted)
	CommandPaletteSelectedDescStyle = lipgloss.NewStyle().Foreground(RoleAssistant)
	CommandPaletteHintStyle         = lipgloss.NewStyle().Foreground(Border)
	// Trailing Kind tag rendered on the right of each row (skill / custom).
	// Uses the Meta token (one step below desc's Muted) so the tag recedes
	// to "dim metadata" weight — desc carries the meaning, tag just labels.
	CommandPaletteTagStyle = lipgloss.NewStyle().Foreground(Meta)
)
View Source
var (
	ContextChipStyle       = lipgloss.NewStyle().Foreground(Muted)
	ContextChipAccentStyle = lipgloss.NewStyle().Foreground(Brand)
	ContextChipPathStyle   = lipgloss.NewStyle().Foreground(BrandSoft)
	ContextChipTeamStyle   = lipgloss.NewStyle().Foreground(RoleTeammate)

	// Transient hints like "Press Ctrl+C again to exit" or "bash mode" — muted
	// gray, not a loud warning.
	ContextChipWarnStyle = lipgloss.NewStyle().Foreground(Muted)

	SubagentCardStyle = card(Accent)

	// TranscriptTitleStyle is the header row of the teammate-transcript
	// modal. Painted with RoleTeammate (purple — the same token used for
	// teammate chips elsewhere) over SurfaceAccent (the same low-contrast
	// strip the user-echo row uses) so the title reads as "you are now
	// observing a teammate" at a glance without shouting. Padding adds a
	// single-column gutter inside the band; the caller fills .Width(...)
	// before Render so the strip stretches edge to edge.
	TranscriptTitleStyle = lipgloss.NewStyle().
							Bold(true).
							Foreground(RoleTeammate).
							Background(SurfaceAccent).
							Padding(0, 1)

	PermissionTitleStyle = lipgloss.NewStyle().Foreground(Accent).Bold(true)
	AskCardStyle         = card(BrandSoft)

	TagSubtleStyle = lipgloss.NewStyle().Foreground(Text)
)
View Source
var (
	Strong = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"} // highest contrast — bullets, critical anchors
	Text   = lipgloss.AdaptiveColor{Light: "236", Dark: "252"}         // body text
	Muted  = lipgloss.AdaptiveColor{Light: "242", Dark: "247"}         // secondary labels, status
	Subtle = lipgloss.AdaptiveColor{Light: "246", Dark: "243"}         // placeholder, thinking, hints
	Meta   = lipgloss.AdaptiveColor{Light: "243", Dark: "246"}         // line numbers, tails, dim metadata
)

Foundation — text scale (recedes left-to-right: Strong > Text > Muted > Subtle > Meta).

View Source
var (
	Border    = lipgloss.AdaptiveColor{Light: "247", Dark: "241"}
	Separator = lipgloss.AdaptiveColor{Light: "248", Dark: "242"}
	Title     = lipgloss.AdaptiveColor{Light: "235", Dark: "255"}
	InputRule = lipgloss.AdaptiveColor{Light: "245", Dark: "244"}
)

Chrome — borders, separators, titles, rules.

View Source
var (
	Brand     = lipgloss.AdaptiveColor{Light: "#2B7B70", Dark: "#3FA796"}
	BrandSoft = lipgloss.AdaptiveColor{Light: "30", Dark: "72"} // muted teal for borders/labels
)

Brand — teal, primary action.

View Source
var (
	Success = lipgloss.AdaptiveColor{Light: "#2C7A39", Dark: "#4EBA65"} // pure saturated green
	Danger  = lipgloss.AdaptiveColor{Light: "#C53030", Dark: "#E06C75"}
	Info    = lipgloss.AdaptiveColor{Light: "#1E6FAF", Dark: "#78C6E7"}
	Live    = lipgloss.AdaptiveColor{Light: "31", Dark: "153"} // spinner / running chrome
)

Status.

View Source
var (
	DiffAddBg          = lipgloss.AdaptiveColor{Light: "#DAFBE1", Dark: "#1A4529"}
	DiffRemoveBg       = lipgloss.AdaptiveColor{Light: "#FFEBE9", Dark: "#5A1F23"}
	DiffAddBgStrong    = lipgloss.AdaptiveColor{Light: "#AAEBC1", Dark: "#2D7242"}
	DiffRemoveBgStrong = lipgloss.AdaptiveColor{Light: "#FFC1BC", Dark: "#8E2A2F"}
)

Diff backgrounds — low-saturation tints for full-line fills, with a deeper shade reserved for word-level intra-line emphasis. Keeping these as background-only lets the body's existing foreground (syntax highlighting, path tokens, etc.) survive intact.

Dark-mode tints went through a tuning pass: an earlier iteration used #0E2A1A / #3A1416 (~11% lightness) which read as near-black with a faint hue cast and made the foreground feel dim by association. The current values land near 18-20% lightness — clearly red/green, but still calm enough that fg tokens (Name #D4D4D4) hold AAA-level contrast (~9:1).

View Source
var (
	RoleUser      = lipgloss.AdaptiveColor{Light: "31", Dark: "#9CC2F9"}
	RoleAssistant = lipgloss.AdaptiveColor{Light: "#3A6F6B", Dark: "#B8E1DD"}
	RoleShell     = lipgloss.AdaptiveColor{Light: "#A04870", Dark: "#D16D9E"}
	RoleTeammate  = lipgloss.AdaptiveColor{Light: "#7A4D9C", Dark: "#C5A3E5"}
)

Message roles.

View Source
var Accent = lipgloss.AdaptiveColor{Light: "#B47A2E", Dark: "#E5B567"}

Accent — amber, emphasis & tool surfaces.

View Source
var Path = lipgloss.AdaptiveColor{Light: "26", Dark: "111"} // file / path token

Highlights.

View Source
var (
	SurfaceAccent = lipgloss.AdaptiveColor{Light: "254", Dark: "236"} // user echo strip
)

Surfaces — background tints (foreground colors for backgrounds).

Functions

func DrawBox added in v0.0.3

func DrawBox(lines []string, innerWidth, fixedRows int) string

DrawBox draws a rounded border box with fixed height and gray border. innerWidth is the content width; fixedRows is the exact number of content rows (short content is padded with empty lines to keep view height stable).

func FormatProgressLine

func FormatProgressLine(progress *agentcore.ProgressPayload) string

FormatProgressLine formats a structured tool progress update for display.

func FormatSubagentOutput

func FormatSubagentOutput(result json.RawMessage) string

FormatSubagentOutput extracts the full output from a subagent result, appending usage stats as a footer. Returns content for card display.

func FormatTokens

func FormatTokens(n int) string

FormatTokens formats a token count with k/M suffix for readability. Uses floor truncation at 0.1 precision so the displayed number never overstates the real value (e.g. 1,050,000 → "1M", not "1.1M"). A trailing ".0" is omitted so whole values render as "200k" / "1M" instead of "200.0k".

func FormatToolOutput added in v0.0.2

func FormatToolOutput(text string, maxVisible int, styles ...lipgloss.Style) string

FormatToolOutput formats tool result text with tree connectors. First line gets the TreeConnector, subsequent lines get ConnectorPad alignment. Truncates to maxVisible lines with "… +N lines" hint. Optional styles override the default ToolResultStyle for line content.

func FormatToolResult

func FormatToolResult(result json.RawMessage, isError bool) string

FormatToolResult extracts displayable text from a tool result. Truncation is handled by the caller (FormatToolOutput).

func IsHiddenTool added in v0.1.3

func IsHiddenTool(tool string) bool

IsHiddenTool reports whether a tool's invocation should be omitted from the visible TUI stream (live events and session restore alike).

task_* tools manage shared coordination state for the agent's own bookkeeping, not work the user wants to follow turn-by-turn. SubAgent dispatch and other execution-unit tools stay visible because they signal real progress.

func IsHiddenToolCall added in v0.1.3

func IsHiddenToolCall(tool string, args json.RawMessage) bool

IsHiddenToolCall extends tool-level hiding with call-specific internal filesystem paths. Auto-memory reads are system context hydration, like AGENTS.md loading, so their ENOENT/success output should not enter the user transcript.

func RenderEditResult

func RenderEditResult(result json.RawMessage, filePath string, width int) string

RenderEditResult renders the edit tool result with colored diff output. Single-line changes get intra-line highlighting (only the changed portion uses a deeper bg). filePath selects the chroma lexer; width is the body cells available for right-padding so the bg band reaches the edge instead of stopping at the last code character.

func RenderLsResult added in v0.0.4

func RenderLsResult(result json.RawMessage) (dirPath string, body string)

RenderLsResult renders ls tool results with tree structure. Returns the directory path (for header update) and the formatted body.

func RenderReadResult added in v0.0.4

func RenderReadResult(result json.RawMessage) string

RenderReadResult renders glob tool results as a path list with colored line numbers. Handles both numbered lines (" 123\tcontent") and plain path lists.

func RenderReadSummary added in v0.1.2

func RenderReadSummary(result json.RawMessage) string

RenderReadSummary renders a one-line summary for the read tool ("Read N lines"), avoiding dumping file contents into the log.

func RenderStreamingOutput

func RenderStreamingOutput(full string, maxLines int) string

RenderStreamingOutput shows the last N lines of streaming tool output with tree connectors (TreeConnector on first line, ConnectorPad for alignment).

func RenderToolHeader added in v0.1.1

func RenderToolHeader(tool string, args json.RawMessage) string

RenderToolHeader styles the tool name while keeping the summary muted.

func RenderWriteResult added in v0.0.2

func RenderWriteResult(result json.RawMessage) string

RenderWriteResult renders the write completion as a summary. The content preview is already emitted during the preview update, so repeating it here duplicates the same file body in scrollback.

func SendCommandResult

func SendCommandResult(text string) tea.Cmd

SendCommandResult is a helper that wraps text into a CommandResultMsg tea.Cmd.

func ShortenPath added in v0.1.3

func ShortenPath(p string) string

ShortenPath replaces the home directory prefix with ~.

func TasksTickCmd added in v0.0.3

func TasksTickCmd() tea.Cmd

TasksTickCmd returns a tea.Cmd that fires TasksRefreshMsg after 500ms.

func TruncateLines

func TruncateLines(s string, maxLines int) string

TruncateLines truncates text to maxLines, appending "..." if truncated.

Types

type AgentEventMsg

type AgentEventMsg struct {
	Event agentcore.Event
}

AgentEventMsg bridges agentcore events into the bubbletea Elm loop.

type AskUserMsg

type AskUserMsg struct {
	Questions []tools.Question
	RespCh    chan<- *tools.AskUserResponse
}

AskUserMsg is sent by the AskUser handler to show questions in the TUI.

type BtwResultMsg added in v0.0.4

type BtwResultMsg struct {
	Answer string
	Err    error
}

BtwResultMsg carries the result of a /btw side question back to the overlay.

type CommandResultMsg

type CommandResultMsg struct {
	Text string
	// Inline prints the result flush against the previous scrollback block
	// (no leading blank line). Use for output that should feel like a direct
	// continuation — e.g. shell command output under its echoed prompt.
	Inline           bool
	Quit             bool   // true for /exit
	Clear            bool   // true for /clear
	NewProvider      string // non-empty if provider was switched
	NewModel         string // non-empty if model was switched
	NewContextWindow int    // non-zero if context window changed
}

CommandResultMsg carries the result of a slash command back to the model.

type CompletionItem added in v0.0.2

type CompletionItem struct {
	Name        string // command name without "/" (e.g. "model")
	Description string
	Usage       string
	Kind        string
	Category    string
	NeedsIdle   bool
	Source      string
	Aliases     []string
	AutoExecute bool
}

CompletionItem is a single command completion candidate.

type Config

type Config struct {
	Placeholder          string
	Version              string
	Provider             string
	ContextWindow        int
	Cwd                  string
	PlansDir             string // absolute path to the plan files directory; enables hidden rendering for write/edit on plan files
	GitBranch            string
	EnvHint              string                   // shown below welcome when using env var credentials
	History              *storage.History         // input history (Up/Down navigation)
	InitialTasks         *storage.TaskSnapshot    // initial task snapshot restored before first render
	RestoredMessages     []agentcore.AgentMessage // messages restored from a previous session (rendered on Init)
	OnKey                func(m *Model, msg tea.KeyMsg) (handled bool, cmd tea.Cmd)
	OnEvent              func(m *Model, ev agentcore.Event) tea.Cmd
	OnPaste              func(m *Model) tea.Cmd              // Ctrl+V: read clipboard image, return ImageAttachedMsg
	OnDrop               func(m *Model, text string) tea.Cmd // Drag-drop: if text is image path, return cmd; else nil
	OnMCPReady           func(msg MCPReadyMsg)               // called when MCP servers finish connecting
	OnHideCompletedTasks func(snap storage.TaskSnapshot) tea.Cmd
	StatusRight          func(m *Model) string
	StatusMode           func(m *Model) string                // mode indicator for context bar (e.g. "⏵⏵ trust")
	StatusTeam           func(m *Model) string                // active-team indicator for context bar (e.g. "△ alpha · 2 idle")
	Overlay              func(m *Model) *OverlayState         // interactive command overlay
	Completions          func(prefix string) []CompletionItem // slash command completions
	OnBtwResult          func(msg BtwResultMsg)               // called when /btw side question completes

	// TeammateEvents is the optional fan-out hub for teammate AgentLoop
	// events. When non-nil the Ctrl+T modal subscribes to it to render a
	// teammate's live transcript. nil disables the modal entirely.
	TeammateEvents *agent.TeammateEventHub
}

Config provides hooks for extending the base TUI behavior.

type Deps added in v0.1.0

type Deps struct {
	Driver        Driver
	ModelName     string
	ContextWindow int
	Provider      string
	Version       string
	// contains filtered or unexported fields
}

Deps holds external dependencies and static configuration for the TUI.

type Driver

type Driver interface {
	Prompt(text string) error
	PromptWithBlocks(blocks []agentcore.ContentBlock) error
	Steer(text string)
	Abort()
}

Driver defines the minimal conversation operations required by the TUI.

type HideCompletedTasksMsg added in v0.1.0

type HideCompletedTasksMsg struct {
	Version uint64
}

HideCompletedTasksMsg hides the task card after all tasks stayed completed for a short delay. Version prevents stale timers from hiding a newer list.

type ImageAttachedMsg

type ImageAttachedMsg struct {
	Block agentcore.ContentBlock // pre-built ImageBlock (base64 + mime)
}

ImageAttachedMsg notifies the Model that an image has been pasted from clipboard.

type InfoOverlayFrame added in v0.1.1

type InfoOverlayFrame struct {
	Title    string
	Subtitle string
	Tabs     []InfoOverlayTab
	Active   int
	Hint     string
	Width    int
	// Height is the terminal viewport height. When > 0, every tab body is
	// normalised to the same number of rows: long bodies are clipped so the
	// header stays on screen, short bodies are padded so switching tabs does
	// not change the overlay's total height (which would scroll the header
	// up/down in inline render mode).
	Height int
}

InfoOverlayFrame renders a modal-style info overlay: title + tab bar + divider + body + footer hint. Used by information commands that want to replace the input area rather than scroll past it.

frame := tui.InfoOverlayFrame{
    Title: "Settings",
    Tabs:  []tui.InfoOverlayTab{{Name: "general", Body: renderGeneral}},
    Hint:  "Esc to close",
    Width: width,
}
fmt.Print(frame.Render())

func (InfoOverlayFrame) Render added in v0.1.1

func (f InfoOverlayFrame) Render() string

Render produces the full styled overlay body.

type InfoOverlayTab added in v0.1.1

type InfoOverlayTab struct {
	Name string
	Body func(width int) string
}

InfoOverlayTab describes one pane inside an InfoOverlayFrame. Body is invoked on each render so it can reflect live session state. The width passed in is the inner content width (already subtracted from the frame chrome) — body renderers should hand it to InfoPanel.SetWidth or equivalent so long values wrap instead of overflowing the terminal.

type InfoPanel added in v0.1.1

type InfoPanel struct {
	// contains filtered or unexported fields
}

InfoPanel builds structured info displays used by /settings, /context, etc.

p := NewInfoPanel("Settings")
p.Row("Provider", "anthropic")
p.Row("Model", "claude-sonnet-4-6")
p.Section("Runtime")
p.Row("Thinking", "low")
p.Hint("Config", "~/.codebot/settings.json")
fmt.Print(p.Render())

func NewInfoPanel added in v0.1.1

func NewInfoPanel(title string) *InfoPanel

NewInfoPanel creates a panel with the given title.

func (*InfoPanel) Blank added in v0.1.1

func (p *InfoPanel) Blank()

Blank adds an empty line.

func (*InfoPanel) Hint added in v0.1.1

func (p *InfoPanel) Hint(label, value string)

Hint adds a subdued metadata row.

func (*InfoPanel) Render added in v0.1.1

func (p *InfoPanel) Render() string

Render produces the final styled string.

func (*InfoPanel) Row added in v0.1.1

func (p *InfoPanel) Row(label, value string)

Row adds a normal key-value row.

func (*InfoPanel) Section added in v0.1.1

func (p *InfoPanel) Section(title string)

Section adds a section header with a blank line before it.

func (*InfoPanel) SetWidth added in v0.1.3

func (p *InfoPanel) SetWidth(width int)

SetWidth tells the panel how many columns it has to render in. When set, values that exceed the value column wrap to subsequent lines indented under the value column. width <= 0 disables wrapping (legacy behavior).

func (*InfoPanel) Warn added in v0.1.1

func (p *InfoPanel) Warn(label, value string)

Warn adds an accent-colored row.

type MCPReadyMsg added in v0.0.3

type MCPReadyMsg struct {
	Tools        int      // total number of tools loaded across all servers
	Errors       []string // connection errors (server: reason)
	Instructions string   // MCP server instructions to set as system suffix
}

MCPReadyMsg notifies the TUI that background MCP server connection has completed.

type Model

type Model struct {
	Deps
	State
}

Model is the bubbletea Model for the agent TUI. Completed content is printed to terminal scrollback via tea.Println; View() only renders the live area (status + streaming + input).

func New

func New(driver Driver, modelName string, cfg ...Config) *Model

New creates a Model with the given agent, model name, and optional config.

func (*Model) Emit added in v0.1.2

func (m *Model) Emit(body string) tea.Cmd

Emit is the single entry point for writing content to terminal scrollback. It caches the exact body given to tea.Println so handleResize can replay the entire stream after a clear. All pre-formatted paths — printBlock, printInline, and direct tea.Println calls that used to exist — funnel through here so the cache stays authoritative. Exported so the outer ui package (app.go, plan.go) can push external scrollback writes through the same cache.

func (*Model) FlushStreamingAssistant added in v0.1.3

func (m *Model) FlushStreamingAssistant() tea.Cmd

FlushStreamingAssistant prints the current live assistant stream into scrollback before a programmatic abort can skip EventMessageEnd.

func (*Model) HandleAgentEvent

func (m *Model) HandleAgentEvent(ev agentcore.Event) (tea.Model, tea.Cmd)

HandleAgentEvent processes agent events. Completed content is printed to terminal scrollback via tea.Println. In-progress content (streaming, tool output) is shown in the live View().

func (*Model) Init

func (m *Model) Init() tea.Cmd

Init implements tea.Model.

func (*Model) RenderContextBar

func (m *Model) RenderContextBar() string

RenderContextBar renders the context line below the input (env info).

func (*Model) RenderMarkdown

func (m *Model) RenderMarkdown(content string) string

RenderMarkdown renders a lightweight terminal-friendly markdown subset.

func (*Model) RenderMarkdownBlock added in v0.1.3

func (m *Model) RenderMarkdownBlock(content string, indent int) string

RenderMarkdownBlock renders complete markdown and applies only outer indentation. Markdown output is already wrapped by the renderer.

func (*Model) RenderPromptOutput

func (m *Model) RenderPromptOutput(text string) string

RenderPromptOutput renders a user message with optional welcome banner for scrollback.

func (*Model) RenderStatusBar

func (m *Model) RenderStatusBar() string

RenderStatusBar renders the live status block pinned above the input: the Running spinner line (when the agent is active) plus a compact task tree (when there are tasks). Either component may be empty depending on state — the task tree stays visible between turns so users can track progress at idle without losing the momentum view.

func (*Model) Update

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update implements tea.Model.

func (*Model) View

func (m *Model) View() string

View renders the live area pinned at the bottom of the terminal. Completed content lives in terminal scrollback (printed via tea.Println).

type OverlayState added in v0.0.2

type OverlayState struct {
	HandleKey     func(msg tea.KeyMsg) (handled bool, cmd tea.Cmd)
	View          func(width, height int) string
	ReplacesInput bool // when true, overlay replaces the input area instead of appearing below it
}

OverlayState bridges an interactive command overlay to the TUI.

type PasteErrorMsg

type PasteErrorMsg struct {
	Text string
}

PasteErrorMsg carries an error from clipboard paste or file drag-drop. Decrements Pasting counter and displays the error text.

type PasteTextMsg

type PasteTextMsg struct{}

PasteTextMsg signals that Ctrl+V found no image; the textarea should paste text.

type PermissionDismissMsg added in v0.0.2

type PermissionDismissMsg struct{}

PermissionDismissMsg tells the TUI to close the permission prompt.

type PermissionMsg added in v0.0.2

type PermissionMsg struct {
	Tool         string
	Command      string
	Reason       string
	Preview      string
	Warning      string // UI-only destructive-action hint; "" suppresses the row
	OutsideRoots bool   // when true, only AllowOnce and Deny are shown
	RespCh       chan<- PermitChoice
}

PermissionMsg is sent to the TUI to show a permission confirmation prompt.

type PermitChoice added in v0.0.2

type PermitChoice int

PermitChoice is the user's response to a permission prompt.

const (
	PermitChoiceDeny         PermitChoice = iota
	PermitChoiceAllowOnce                 // allow this invocation only
	PermitChoiceAllowSession              // allow for the rest of the session
	PermitChoiceAllowAlways               // persist to project config
)

type PromptMsg

type PromptMsg struct {
	Text string
}

PromptMsg injects a message as if the user typed and sent it. The TUI renders it as a user message and forwards it to the agent.

type RestoreMsg added in v0.0.2

type RestoreMsg struct{ Msgs []agentcore.AgentMessage }

RestoreMsg is sent to replay restored session messages into scrollback.

type RetryStatusMsg added in v0.1.2

type RetryStatusMsg struct {
	Prefix   string
	Deadline time.Time
}

RetryStatusMsg updates the single in-place retry status shown in the live area. Empty Prefix clears the current retry status; Deadline drives the live countdown.

type State added in v0.1.0

type State struct {
	Input   textarea.Model
	Spinner spinner.Model

	ToolSpinner spinner.Model // breathing-dot spinner for tool execution

	Streaming *strings.Builder
	Thinking  *strings.Builder
	IsStream  bool
	// SuppressNextAssistantText avoids double-printing when an in-flight
	// assistant stream is flushed manually before a terminal tool aborts the
	// run, but a late MessageEnd still arrives with the same content.
	SuppressNextAssistantText string

	Running         bool
	TurnCount       int
	PendingTools    map[string]string           // toolID -> display label (== "Plan" marks a plan-file write/edit)
	HiddenToolCalls map[string]struct{}         // toolID -> internal call hidden from UI
	ToolHeaders     map[string]string           // toolID -> formatted header (printed at end)
	ToolOutputBuf   map[string]*strings.Builder // toolID -> streaming output
	ToolDeltaBuf    map[string]*strings.Builder // toolID -> accumulated subagent delta text
	ToolThinkingBuf map[string]*strings.Builder // toolID -> accumulated subagent thinking text

	Width  int
	Height int
	Ready  bool

	Cwd         string
	PlansDir    string
	GitBranch   string
	ShowWelcome bool
	EnvHint     string // env var hint shown below welcome
	RunStats    runStats
	Images      []agentcore.ContentBlock // attached images (from Ctrl+V clipboard paste)
	ImageCursor int                      // -1 = not selecting; 0+ = selected image index
	Pasting     int                      // number of async image reads in progress (clipboard paste or drag-drop)

	Markdown *markdown.Renderer

	AskUser    *askUserState    // non-nil when ask-user UI is active
	Permission *permissionState // non-nil when permission prompt is active

	Tasks *storage.TaskSnapshot // non-nil when task items exist; displayed above input

	QueuedMsgs []string // messages queued while agent is running (display only)

	// Retry countdown shown in the live area while auto-retrying.
	// RetryPrefix is the static text (e.g. "Request failed, retrying (1/3)");
	// RetryDeadline is when the retry will fire — View() computes remaining seconds.
	RetryPrefix   string
	RetryDeadline time.Time

	MCPLoading bool // true while MCP servers are connecting in background

	// Scrollback mirrors the stream of pre-formatted bodies sent to
	// tea.Println. It exists solely to cure the terminal-resize ghost /
	// reflow duplication problem.
	//
	// Precise cause: Println content, once written, is inert — bubbletea
	// never redraws it, so reflow at worst re-wraps it in place without
	// duplicating. The ghosts come exclusively from the live View()
	// (status bar, input panel borders, streaming area). On resize the
	// terminal pushes those rows up into OS scrollback to make room for
	// the new viewport, but bubbletea's `linesRendered` cursor tracking
	// still points at the old position — its next frame's "erase previous
	// frame" sequence misses, and the pushed-up copy is marooned in
	// scrollback as a ghost.
	//
	// Fix: on WindowSizeMsg we wipe viewport + OS scrollback (`\x1b[2J`
	// + `\x1b[3J` + `\x1b[H`) to evict the ghosts, then replay this cache
	// so legitimate Println history survives the nuke. Without the cache
	// we'd be trading ghosts for lost conversation history. See
	// handleResize for the replay path, Emit for the write path,
	// handleCommandResult (msg.Clear) for the reset path.
	//
	// Entries are the exact string passed to tea.Println (as returned by
	// formatScrollbackBlock), so joining them with "\n" and Println'ing
	// once is byte-for-byte equivalent to the original per-block Println
	// sequence — tea.Println splits on "\n" internally. Bounded by
	// scrollbackCacheLimit; entries beyond the cap are dropped FIFO,
	// which only materialises as lost history after a resize (the live
	// terminal scrollback remains complete until the next resize clears
	// it).
	Scrollback []string

	Suggestion string // prompt suggestion shown as placeholder after agent completes

	QuitPending bool // true after first Ctrl+C, waiting for second to quit

	// TranscriptModal renders the live transcript of a teammate when the
	// user opens the popup (Ctrl+T). nil = closed; non-nil = open and
	// full-screen, taking over all keyboard input except Esc / Ctrl+T /
	// Ctrl+C and the scroll keys.
	TranscriptModal *TranscriptView
	// TranscriptAgent is the teammate currently shown in the modal.
	TranscriptAgent string
	// contains filtered or unexported fields
}

State holds mutable runtime state for the TUI.

type SuggestionMsg added in v0.0.4

type SuggestionMsg struct {
	Text string
}

SuggestionMsg carries a prompt suggestion generated after agent completion.

type TaskListUpdateMsg

type TaskListUpdateMsg struct {
	Snapshot storage.TaskSnapshot
}

TaskListUpdateMsg notifies the TUI that the task list has changed.

type TasksRefreshMsg added in v0.0.3

type TasksRefreshMsg struct{}

TasksRefreshMsg is a periodic tick that triggers a re-render of the /tasks overlay.

type TranscriptChannelClosedMsg added in v0.2.0

type TranscriptChannelClosedMsg struct {
	Agent string
}

TranscriptChannelClosedMsg signals the hub subscription closed (modal closed via Esc, teammate disappeared, etc.). Used purely to break the cmd → msg → cmd recursion; Update reacts only if the modal is still open.

type TranscriptEventMsg added in v0.2.0

type TranscriptEventMsg struct {
	Agent string
	Event agentcore.Event
}

TranscriptEventMsg is a teammate event delivered to Update for the open modal. The agent name is included so a late-arriving event for a teammate the user has since switched away from can be discarded.

type TranscriptView added in v0.2.0

type TranscriptView struct {
	// contains filtered or unexported fields
}

TranscriptView renders an agentcore.Event stream as a scrollable in-memory transcript. It is the read-only sibling of the leader's scrollback path in events.go, designed for the modal popup that lets the user observe a teammate's live activity.

Why a separate renderer and not events.go?

  • events.go writes to terminal scrollback via tea.Println (global stream, cannot be split across multiple views).
  • HandleAgentEvent reads/writes >10 Model fields (PendingTools, ToolHeaders, RunStats, Streaming, …). Reusing it would require either dragging the whole Model into the modal or extracting every helper that touches them.
  • The modal scope is narrower: teammates can't enter plan mode, can't spawn nested subagents, can't show ask_user dialogs. Most of events.go's special-case branches don't apply.

What IS shared with events.go: the rendering style constants and pure formatting helpers (RenderToolHeader, FormatToolResult, FormatToolOutput, FormatProgressLine, indentBlock, truncateRunes). The two views look the same because they call the same helpers — there is no parallel theme to drift.

func NewTranscriptView added in v0.2.0

func NewTranscriptView(title string) *TranscriptView

NewTranscriptView returns an empty view. Call SetSize before View() — without it the viewport has zero dimensions and View() returns "".

func (*TranscriptView) GotoBottom added in v0.2.0

func (t *TranscriptView) GotoBottom()

func (*TranscriptView) HandleEvent added in v0.2.0

func (t *TranscriptView) HandleEvent(ev agentcore.Event)

HandleEvent updates state in response to one agentcore event. Mirrors the subset of events.go HandleAgentEvent that a teammate actually produces. After each call the viewport content is rebuilt so View() reflects the latest state; the cost is O(len(blocks)) per event which is fine for the target scale (hundreds of blocks per teammate run).

func (*TranscriptView) PageDown added in v0.2.0

func (t *TranscriptView) PageDown()

func (*TranscriptView) PageUp added in v0.2.0

func (t *TranscriptView) PageUp()

func (*TranscriptView) ScrollDown added in v0.2.0

func (t *TranscriptView) ScrollDown(n int)

func (*TranscriptView) ScrollUp added in v0.2.0

func (t *TranscriptView) ScrollUp(n int)

ScrollUp / ScrollDown / GotoBottom proxy to the viewport. The repaint that follows event handling auto-scrolls when the user is already at the bottom (viewport's default behaviour with SetContent).

func (*TranscriptView) SetLiveBadge added in v0.2.0

func (t *TranscriptView) SetLiveBadge(badge string)

SetLiveBadge swaps the spinner/idle marker shown at the head of the status line. Cheap on purpose: no layout recompute, no repaint — only the next View() call picks up the new badge. Pass "" to hide.

func (*TranscriptView) SetSize added in v0.2.0

func (t *TranscriptView) SetSize(width, height int)

SetSize records new outer dimensions and recomputes the viewport area. Width 0 or height < 2 makes the view effectively invisible.

func (*TranscriptView) SetStatus added in v0.2.0

func (t *TranscriptView) SetStatus(status string)

SetStatus updates the bottom-bar label. Status visibility affects how much vertical room the viewport gets, so the layout is recomputed. Pass "" to clear.

func (*TranscriptView) SetTitle added in v0.2.0

func (t *TranscriptView) SetTitle(title string)

SetTitle updates the top-bar label. Title visibility affects how much vertical room the viewport gets, so the layout is recomputed.

func (*TranscriptView) View added in v0.2.0

func (t *TranscriptView) View() string

View renders the title + viewport + status into a single string sized to (width, height). Returns "" if the view has no room to draw.

Each visible chrome row is followed (title) or preceded (status) by a blank spacer line so the colored title strip and the status text never touch the body content. applyLayout reserves the matching rows.

Directories

Path Synopsis
Package syntax wraps chroma to emit ANSI-colored code that nests safely inside a lipgloss background-color span.
Package syntax wraps chroma to emit ANSI-colored code that nests safely inside a lipgloss background-color span.

Jump to

Keyboard shortcuts

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