tui

package
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: May 30, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Overview

chrome.go renders the live activity band between the scrollback and the status line, and owns the redraw-ticker lifecycle that drives it. The band is always reserved one row (in App.layout) so its appearance and disappearance never reflow the viewport.

The Chrome module encapsulates: spinner phase + caption (thinking / writing / tool), the "new content below" hint, and the boolean tickActive flag that keeps the App from scheduling redundant coalescing ticks.

completions.go is the inline popup that backs the `/` and `@` menus. It is a small, self-contained list component the input owns: not a full-screen overlay but a bordered card spliced into View()'s parts slice directly above the input box.

The component holds the full candidate set for the active trigger and a fuzzy-filtered index window; ranking reuses fuzzyMatch (fuzzy.go) so the menu and the filterable pickers share one matcher. Rows are rendered with the raised panel surface so they read as a card consistent with the beautify ladder, the cursor row gets the selection band, the fuzzy-matched runes in the label are bolded, and the detail column is dimmed.

fileindex.go backs the `@` file-mention menu. It is the file source for the completions popup: a depth-limited walk of the working directory producing repo-relative paths, skipping the usual noise dirs (.git, node_modules, vendor) and any dot-directory, capped so a giant tree can never blow up memory or the render path.

The walk is intentionally off the UI hot path: App kicks it off in a tea.Cmd (eagerly at startup, see Init) and the result is delivered as a fileIndexMsg that App caches. Until that message lands the popup shows a single "(indexing files…)" placeholder row. Ranking is left to fuzzyMatch over the relative path (the same matcher the `/` menu and pickers share), so no path-aware scoring lives here.

fuzzy.go is a dependency-free tiered subsequence matcher used to rank completion candidates (the `/` and `@` menus) and filterable pickers. It mirrors crush's tierExactName/tierPrefixName/tierPathSegment/tierFallback ordering: the tier is the dominant component of the score (lower is better), and within a tier earlier+denser matches on shorter candidates win.

history.go is a readline-style prompt-history ring plus pure file helpers for cross-session recall (G2). The ring mirrors crush's historyPrev/historyNext semantics: ↑ walks toward older entries, ↓ walks back toward newer and finally restores the live draft. Consecutive duplicates and empty strings are never stored, and the ring is capped to the newest `max` entries.

The file helpers (loadPromptHistory/appendPromptHistory) are intentionally pure — they touch no shared mutable state, so they are safe to call from a tea.Cmd off the UI goroutine (§10: the writer must not race Update).

logo.go renders the startup wordmark in the crush style: per-letter half-block letterforms joined horizontally, flanked by diagonal ╱ fields, with a meta row (brand name + version) above and a deep→light brand gradient applied per row. Mirrors the composition of crush's internal/ui/logo but with our own DeepSeek Ocean glyphs and colors.

markdown.go renders assistant text through Glamour (markdown-aware, ANSI-styled) and reasoning / tool output through word-aware wrap.

A single Glamour renderer is cached per (style, width) tuple — the renderer constructor parses a non-trivial style JSON, so reusing across refreshes keeps the per-keystroke render cost negligible.

Package tui implements the Bubble Tea TUI for deepseekcode.

Architecture in one paragraph: a single tea.Program owns the UI state. The agent runs in a goroutine; a separate consumer goroutine reads agent.Events() and wraps each event into a single agentEventMsg that is dispatched to the tea.Program. Update folds that one message type — type-switching on the inner Event — into the App model. Permission asks travel the same channel and carry their reply chan, so the agent goroutine blocks waiting on the user without any out-of-band wiring.

Components: items.go owns rendered chat-item types; scrollback.go owns the chat buffer + stream cursors + visual selection; app.go owns the model + Update; status.go renders the status line; theme.go owns Lip Gloss styles; keymap.go owns keybindings.

pager.go implements a fullscreen overlay for viewing tool results that would otherwise dominate the chat scrollback. The pager owns its own viewport so j/k/G/gg navigate the long output independently of the chat scroll position.

Activated by `p` in Normal mode (opens the last tool result) and by future callers like /paste, /diff. Dismissed by `q` or esc, which returns the user to Insert mode with input refocused.

permission.go owns the modal permission card: pending ask state, 2×2 button grid, key resolution, and rendering. App composes one PermissionFlow instead of carrying pendingPerm + permCursor as flat fields plus a 130-line renderPermissionPrompt method.

placeholder.go drives the dynamic textarea placeholder. The hint reflects state: a "Working…" caption while a run is active, otherwise one of a small rotation of friendly "Ready" hints. The rotation is deterministic — indexed by a turn counter, never a clock or random source — so golden/snapshot tests never flap.

question.go owns the modal question card: pending ask state, per-question option list, single/multi select, and rendering. Mirrors permission.go's Open/Resolve/Render pattern.

queue_paste.go owns two P2 input affordances:

  • G11 prompt queueing: a prompt submitted while a run is active is appended to a.queued instead of starting a second run; the active run's Done path drains the next queued prompt and submits it. The queued count is surfaced subtly via a toast on enqueue and on the status line.
  • G12 large-paste collapse: a bracketed paste over pasteCollapseLines lines is held in full in a.pasteStore and the DISPLAY collapses to a one-line "[pasted N lines]" chip. The chip is expanded back to the full text at submit time, so the model always sees the real paste while the input box stays a single readable line.

scrollback.go owns the chat history: items, in-progress stream cursors, the rendered line cache, and visual-mode selection.

Why a dedicated module. The bug-prone pattern this replaces was "raw pointers into the items slice" (streamText / streamThink), which dangled after slice realloc and needed a backward-walking rebind heuristic that itself had a cross-turn bleed bug. Indices stay valid because the items slice only ever appends: nothing reorders, nothing removes.

Selection invariant. visual-mode selection is captured at the rendered-line index space; if the underlying items mutate during a selection (e.g. streaming tokens extend the buffer), the seq number drifts and the selection auto-invalidates on the next Render. Keeps the indexing model honest without needing to re-map selections across content changes.

scrollbar.go overlays a one-cell vertical scrollbar onto the right edge of the rendered viewport body. The thumb is brand-light and the track rides the border token so the bar is clearly visible against the painted canvas while still reading as chrome rather than content. The thumb/track glyphs are the shared ScrollbarThumb / ScrollbarTrack symbols.

visual.go implements a Vim-style line-range selection mode for the scrollback. Activated by `v` in Normal mode.

Why we need this: alt-screen mode (tea.WithAltScreen) means the terminal shows only the currently rendered frame — no scrollback history above it. Terminal-native shift-drag selection therefore can't reach content that the user has scrolled past, which is exactly the case for long agent outputs that span multiple screens.

Visual mode solves that by tracking a line-space cursor that:

  • moves with j/k, ^U/^D, gg/G inside the in-app viewport
  • scrolls the viewport when it crosses the visible window
  • paints a reverse-video highlight on the [anchor, cursor] range
  • yanks the highlighted text (ANSI-stripped) on `y`

Selection survives content updates because indices live in "rendered line" space — newly appended lines extend the buffer but don't shift existing indices.

welcome.go renders the startup banner. Width-adaptive: a terminal narrower than the letterform wordmark falls back to a single-line greeting so nothing wraps mid-glyph. The wide banner is composed by logo.go (letterforms + diagonal fields + brand gradient).

yank.go implements clipboard copy via OSC 52 escape sequences.

OSC 52 ("Set Clipboard") is honored by iTerm2, kitty, alacritty, WezTerm, and recent tmux/screen. It is the only portable way to write the system clipboard from inside a TUI without taking a hard dependency on platform-specific paste daemons (xclip, pbcopy, wsl). On terminals that don't support it the sequence is silently ignored — no crash, no visible noise.

We deliberately write to stderr rather than stdout. Bubble Tea's renderer owns stdout and would otherwise risk interleaving the escape mid-frame. stderr connects to the same tty in interactive sessions, so the terminal sees the sequence either way.

Index

Constants

View Source
const (
	IconCheck       = "✓"
	IconToolPending = "●"
	IconToolOk      = "✓"
	IconToolErr     = "×"
	IconArrowRight  = "→"
	IconRadioOn     = "◉"
	IconRadioOff    = "○"
	BarThick        = "▌" // 工具卡片左竖边条
	BarThin         = "│"
	SectionRule     = "─"
	ScrollbarThumb  = "┃"
	ScrollbarTrack  = "│"
	IconSkill       = "▲"
	IconModel       = "◇"
	IconFoldClosed  = "▸" // collapsed reasoning fold glyph
	IconFoldOpen    = "▾" // expanded reasoning fold glyph
)
View Source
const Gutter = "  "

Gutter is the single 2-cell left gutter prepended to EVERY chat item. Components reference Theme.Gutter (a method returning this constant) so the indent is defined in exactly one place.

Variables

This section is empty.

Functions

func ApplyBoldForegroundGrad

func ApplyBoldForegroundGrad(base lipgloss.Style, input string, color1, color2 color.Color) string

ApplyBoldForegroundGrad renders a given string with a bold horizontal gradient foreground. Returns "" for empty input.

func ApplyForegroundGrad

func ApplyForegroundGrad(base lipgloss.Style, input string, color1, color2 color.Color) string

ApplyForegroundGrad renders a given string with a horizontal gradient foreground. Returns "" for empty input.

func ForegroundGrad

func ForegroundGrad(base lipgloss.Style, input string, bold bool, color1, color2 color.Color) []string

ForegroundGrad returns a slice of strings representing the input string rendered with a horizontal gradient foreground from color1 to color2. Each string in the returned slice corresponds to a grapheme cluster in the input string.

func Highlight

func Highlight(t Theme, source, lang string) string

Highlight uses chroma to syntax-highlight source by lang, returning an ANSI-colored string. Background is locked to the Ocean card body color. Returns source unchanged if lang is empty or unrecognized.

func LeftBarGlyph added in v0.3.0

func LeftBarGlyph() string

LeftBarGlyph returns the thick bar glyph the LeftBar style is meant to render. Exposed so components don't re-declare the rune.

func RenderHUD

func RenderHUD(data HUDData, width int) string

RenderHUD renders a single-line status HUD with width-aware truncation. Returns an empty string when data is empty.

func RenderToolSummary

func RenderToolSummary(tool, args, result string, isError bool, width int) string

RenderToolSummary renders a one-line tool summary with width-aware truncation. Different tools get different summary formats.

Types

type App

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

App is the root Bubble Tea Model.

One App per running TUI process. The Agent runs in a goroutine owned by the App; a separate pumpEvents goroutine consumes agent.Events() and forwards each event as an agentEventMsg via App.send (the tea.Program.Send func, captured in Run).

func New

func New(cfg Config) *App

New constructs an App. The returned App is a tea.Model; pass it to tea.NewProgram and call .Run().

func (*App) Init

func (a *App) Init() tea.Cmd

Init satisfies tea.Model. The first scrollback entry is an ASCII welcome banner (whale mascot + DEEPSEEKCODE wordmark); startup notices like resume confirmations follow.

func (*App) Run

func (a *App) Run() error

Run starts the Bubble Tea program. Blocks until quit.

Mouse cell-motion is enabled so we receive click + drag + release events. We use them to drive a TUI-managed selection (handleMouse): click-drag highlights lines, dragging past the top/bottom edge auto-scrolls the viewport so the selection can span the entire scrollback, and releasing the button auto-yanks the selected text to the clipboard via OSC 52. Mouse wheel still scrolls the viewport (bubbles/viewport handles that).

Why not native terminal drag-select: alt-screen mode means the terminal has no scrollback above our rendered frame, so native drag tops out at the visible window. TUI-managed selection is the only way to extend across content the user has scrolled past.

If the user wants native terminal behavior (mouse drag-select + terminal's own scrollback), `P` in Normal mode (or /export) opens the full session in $PAGER (default `less -R`) which owns the TTY completely while running.

func (*App) Update

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

Update folds incoming messages into the model.

func (*App) View

func (a *App) View() tea.View

View renders the whole UI.

Note: we do NOT paint a full-screen background. An outer lipgloss Background cannot reliably fill behind glamour / viewport output — those emit ANSI resets (\x1b[0m) that snap the background to the terminal default mid-line, leaving ragged black gaps (see ADR-0002). Backgrounds live only on explicit panels / badges / diff bands, which render their own full-width per-line fills and so never bleed; the screen sits on the terminal's own background.

type BadgeKind added in v0.3.0

type BadgeKind int

BadgeKind selects the fill color of a filled chip rendered by Theme.Badge.

const (
	// BadgeOk is a success chip (ok color).
	BadgeOk BadgeKind = iota
	// BadgeErr is an error chip (err color).
	BadgeErr
	// BadgeWarn is a warning chip (warn color).
	BadgeWarn
	// BadgeInfo is an informational chip (brandLight color).
	BadgeInfo
	// BadgeBrand is a primary/brand chip (brandDeep color).
	BadgeBrand
)

type Chrome

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

Chrome owns the live activity band — spinner, phase caption, and the tick-active flag that the App uses to avoid scheduling redundant redraw timers.

func NewChrome

func NewChrome() *Chrome

NewChrome returns an idle Chrome with no scheduled tick.

func (*Chrome) Active

func (c *Chrome) Active() bool

Active reports whether an activity is in progress (spinner shown).

func (*Chrome) AdvanceFrame

func (c *Chrome) AdvanceFrame()

AdvanceFrame moves the spinner one tick forward.

func (*Chrome) BeginThinking

func (c *Chrome) BeginThinking()

BeginThinking transitions the band to the "thinking…" caption.

func (*Chrome) BeginTool

func (c *Chrome) BeginTool(name string)

BeginTool transitions to the tool caption. It is called once per tool call; a turn's parallel tool calls each call it. The first call of a fresh batch (phase was not already tool) resets the batch counters and the elapsed clock; subsequent calls in the same batch only bump the started count so the caption can render "N ready" against the batch size.

func (*Chrome) BeginWriting

func (c *Chrome) BeginWriting()

BeginWriting transitions to the "writing…" caption.

func (*Chrome) EndToolBatch

func (c *Chrome) EndToolBatch()

EndToolBatch marks the current tool batch closed at a step boundary (called on EventStepFinish, once per step). The counters are NOT zeroed here — that is deferred to the next BeginTool so the completed "N ready" stays visible through the inter-step gap rather than flashing "0 ready".

func (*Chrome) MarkTickStarted

func (c *Chrome) MarkTickStarted()

MarkTickStarted / MarkTickStopped flip the tick-scheduled flag. Idempotent — handlers call MarkTickStarted defensively.

func (*Chrome) MarkTickStopped

func (c *Chrome) MarkTickStopped()

MarkTickStopped is called when the redraw tick concludes its follow-up scheduling — agent idle AND chrome idle.

func (*Chrome) Render

func (c *Chrome) Render(t Theme, showNewBelow bool) string

Render returns the one-line band content. When idle and the user has scrolled away from the bottom, it surfaces the "new content below" indicator instead. Returns "" when there's nothing to say — the reserved row renders as blank.

func (*Chrome) Reset

func (c *Chrome) Reset()

Reset returns the band to idle — called on agent done.

func (*Chrome) SetFirstTokenTimeout

func (c *Chrome) SetFirstTokenTimeout(d time.Duration)

SetFirstTokenTimeout records the agent's real first-token timeout so the cold-start caption can quote it. Called once at App construction.

func (*Chrome) TickActive

func (c *Chrome) TickActive() bool

TickActive reports whether a redraw ticker is currently scheduled.

func (*Chrome) ToolReady

func (c *Chrome) ToolReady()

ToolReady records that one tool call in the current batch has produced a result. Clamped to the started count. No-op outside a tool batch.

func (*Chrome) UpdateTokens

func (c *Chrome) UpdateTokens(n int)

UpdateTokens refreshes the rolling token estimate shown in the caption.

type Config

type Config struct {
	Agent    *agent.Agent
	Model    string
	Thinking bool
	Theme    string
	Cwd      string

	// TransparentBackground (ui.transparent_background) disables ALL background
	// fills — the bg-tier panels (tool results, diffs, reasoning) degrade to
	// left-bars / separators with no opaque fills. The full-screen canvas is no
	// longer painted regardless (it bled through glamour/viewport resets; see
	// ADR-0002), so this flag is now a "fully flat / fg-only" toggle.
	TransparentBackground bool

	// Optional persistence integration. Provide all three or none.
	SessionID    string
	UndoFn       func(n int) (int, error)
	ListSessions func() ([]session.Session, error)
	SetModelFn   func(model string) error
	SetThemeFn   func(theme string) error

	// StartupNotices are shown as info chat items at TUI start. Used for
	// resume confirmations, warnings about degraded persistence, etc. —
	// anything the caller would have written to stderr in CLI mode.
	StartupNotices []string

	// CompactionCount initialises the status-line compaction counter from
	// a resumed session's history. Populate from sess.CompactionCount.
	CompactionCount int

	// Commands are user-defined slash commands loaded from .deepseek/command/*.md.
	Commands map[string]commands.Command

	// HistoryPath is the per-project prompt-history ring file (G2). The caller
	// (cmd/dsc) owns the path construction so the TUI stays filesystem-light.
	// Empty disables persistence: history lives only for the session and no
	// file is read or written.
	HistoryPath string

	// HistorySeed folds extra recall entries (oldest → newest) into the ring on
	// top of whatever HistoryPath loads — e.g. a resumed session's prior user
	// prompts. Deduped against the file by the ring's push rules.
	HistorySeed []string

	// Language overrides the error message language. "" uses auto-detect.
	Language string

	// LSPReady reports whether at least one LSP server is attached.
	LSPReady bool
}

Config bundles construction params for New.

type HUDData

type HUDData struct {
	Model           string
	Effort          string
	ContextTokens   int
	ContextLimit    int
	CacheHitRatio   float64
	InputHitTokens  int
	InputMissTokens int
	SavedCNY        float64
	OutputTokens    int
	ReasoningTokens int
	StepCNY         float64
	SessionCNY      float64
	ActiveAgent     string
	RunningJobs     int
}

HUDData holds the data for rendering the status HUD.

type Overlay

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

Overlay owns the modal-picker state — which picker is up, where the cursor sits, and the picker-specific data rows. App composes one of these instead of carrying overlay/overlayCursor/models/ sessionsRows as four flat fields.

The filterable pickers (/models, /sessions) and the command palette share a single filterableList (G5/G6): it holds the live filter string and the fuzzy-ranked subset of visible row indices, so the cursor moves through the narrowed set rather than the raw rows. modeTape keeps the old direct-cursor model (no filter); modeHelp keeps a plain scroll offset.

func NewOverlay

func NewOverlay() *Overlay

NewOverlay returns an idle Overlay (mode = chat, nothing open).

func (*Overlay) Close

func (o *Overlay) Close()

Close returns to the chat view.

func (*Overlay) Cursor

func (o *Overlay) Cursor() int

Cursor returns the picker's current cursor index.

func (*Overlay) FilterBackspace added in v0.3.0

func (o *Overlay) FilterBackspace()

func (*Overlay) FilterClear added in v0.3.0

func (o *Overlay) FilterClear() bool

func (*Overlay) FilterCursor added in v0.3.0

func (o *Overlay) FilterCursor() int

func (*Overlay) FilterString added in v0.3.0

func (o *Overlay) FilterString() string

func (*Overlay) FilterType added in v0.3.0

func (o *Overlay) FilterType(r rune)

FilterType / FilterBackspace / FilterClear drive the shared filter for the filterable overlays. FilterClear reports whether there was a filter to clear (so the caller can implement "esc clears, then closes").

func (*Overlay) Filterable added in v0.3.0

func (o *Overlay) Filterable() bool

Filterable reports whether the active overlay routes typing into the shared filter (the pickers and the palette) rather than treating letters as nav keys. modeTape and modeHelp are not filterable.

func (*Overlay) HelpTab added in v0.3.2

func (o *Overlay) HelpTab() int

HelpTab returns the active help tab index (0..helpTabCount-1).

func (*Overlay) IsOpen

func (o *Overlay) IsOpen() bool

IsOpen reports whether a picker is currently visible.

func (*Overlay) Mode

func (o *Overlay) Mode() overlayMode

Mode returns the active overlay mode (modeChat when none is open).

func (*Overlay) Models

func (o *Overlay) Models() []modelOption

Models / SessionsRows / Palette return the picker-specific row slices for rendering. Returned slices are read-only.

func (*Overlay) MoveDown

func (o *Overlay) MoveDown()

MoveDown / MoveUp advance the cursor inside the active overlay. For the filterable pickers they walk the visible (narrowed) set; modeTape and modeHelp move the raw cursor / scroll offset.

func (*Overlay) MoveUp

func (o *Overlay) MoveUp()

func (*Overlay) NextHelpTab added in v0.3.2

func (o *Overlay) NextHelpTab()

NextHelpTab / PrevHelpTab cycle the active help tab with wrap-around (Next from the last tab -> first; Prev from the first -> last) and reset the scroll offset (cursor) to 0.

func (*Overlay) OpenHelp added in v0.3.0

func (o *Overlay) OpenHelp()

OpenHelp switches to the help overlay (G7), scrolled to the top.

func (*Overlay) OpenModels

func (o *Overlay) OpenModels(activeID string)

OpenModels switches to the /models picker. The filterableList is seeded with one label per model (short name + note); the cursor lands on the row that matches activeID, falling back to 0 if no match. Once the user filters, the cursor tracks the narrowed set.

func (*Overlay) OpenPalette added in v0.3.0

func (o *Overlay) OpenPalette(actions []paletteAction)

OpenPalette switches to the command palette (G5) over the supplied action list. The filter starts empty so every action is visible.

func (*Overlay) OpenSessions

func (o *Overlay) OpenSessions(rows []sessionRow)

OpenSessions switches to the /sessions picker with the supplied rows. The filter matches against the short id + summary of each row.

func (*Overlay) OpenTape

func (o *Overlay) OpenTape()

OpenTape switches to the /tape view with the cursor at the top.

func (*Overlay) OpenThemes added in v0.3.2

func (o *Overlay) OpenThemes(activeID string)

OpenThemes switches to the /theme picker. The filterableList is seeded with "Label Desc" per row; the cursor lands on the row that matches activeID, falling back to 0 if no match.

func (*Overlay) Palette added in v0.3.0

func (o *Overlay) Palette() []paletteAction

func (*Overlay) PrevHelpTab added in v0.3.2

func (o *Overlay) PrevHelpTab()

func (*Overlay) SelectedAction added in v0.3.0

func (o *Overlay) SelectedAction() (paletteAction, bool)

SelectedAction returns the palette action under the cursor and true, or the zero action and false when nothing matches the filter.

func (*Overlay) SelectedModelID

func (o *Overlay) SelectedModelID() string

SelectedModelID returns the model id under the cursor (mapped through the filter), or "" when nothing matches.

func (*Overlay) SelectedSessionID

func (o *Overlay) SelectedSessionID() string

SelectedSessionID returns the session id under the cursor (mapped through the filter), or "" when nothing matches.

func (*Overlay) SelectedThemeID added in v0.3.2

func (o *Overlay) SelectedThemeID() string

SelectedThemeID returns the theme id under the cursor (mapped through the filter), or "" when nothing matches.

func (*Overlay) SessionsRows

func (o *Overlay) SessionsRows() []sessionRow

func (*Overlay) SetHelpTab added in v0.3.2

func (o *Overlay) SetHelpTab(i int)

SetHelpTab sets the active help tab, clamped to [0, helpTabCount-1], and resets the scroll offset (cursor) to 0. A no-op-safe clamp: negative -> 0, >= helpTabCount -> helpTabCount-1.

func (*Overlay) Themes added in v0.3.2

func (o *Overlay) Themes() []themeOption

Themes returns the picker rows (read-only).

func (*Overlay) VisibleRows added in v0.3.0

func (o *Overlay) VisibleRows() []int

type PanelTier added in v0.3.0

type PanelTier int

PanelTier selects which background surface Theme.Panel fills. The tiers climb from the painted canvas up through inset wells, panels, and raised modals. Panel() degrades every tier to no-background when fills are disabled (transparent mode or non-truecolor terminal).

const (
	// TierBase is the painted canvas background (bgBase).
	TierBase PanelTier = iota
	// TierWell is the recessed code/diff inset (bgWell).
	TierWell
	// TierSurface backs tool / reasoning panels (bgSurface).
	TierSurface
	// TierRaised backs modals and selected rows (bgRaised).
	TierRaised
)

type PermissionFlow

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

PermissionFlow encapsulates the modal permission card. Mutations only through methods.

func NewPermissionFlow

func NewPermissionFlow() *PermissionFlow

NewPermissionFlow returns an inactive flow (no pending ask).

func (*PermissionFlow) Active

func (p *PermissionFlow) Active() bool

Active reports whether a permission ask is pending user input.

func (*PermissionFlow) MoveDown

func (p *PermissionFlow) MoveDown()

func (*PermissionFlow) MoveLeft

func (p *PermissionFlow) MoveLeft()

func (*PermissionFlow) MoveRight

func (p *PermissionFlow) MoveRight()

func (*PermissionFlow) MoveUp

func (p *PermissionFlow) MoveUp()

Nav — 2×2 grid keyed as:

0 1
2 3

func (*PermissionFlow) Open

Open stores the ask and resets the cursor to the safe default (allow once). Caller is responsible for switching App.mode to modePermission and re-laying out the screen.

func (*PermissionFlow) Render

func (p *PermissionFlow) Render(t Theme, width int) string

Render returns the modal permission card. Returns "" when there's nothing pending.

func (*PermissionFlow) Resolve

func (p *PermissionFlow) Resolve(key string) (resp agent.PermissionResponse, reply chan<- agent.PermissionResponse, check permissions.Check, ok bool)

Resolve maps a key to a button decision. Returns (resp, reply, check, ok=true) when the key resolved a button; ok=false means the key wasn't a button (caller should swallow). On ok=true the flow is cleared (Active becomes false) — the caller is responsible for sending resp on reply, restoring mode, and re-layouting.

func (*PermissionFlow) ShiftTab

func (p *PermissionFlow) ShiftTab()

func (*PermissionFlow) Tab

func (p *PermissionFlow) Tab()

func (*PermissionFlow) Tool

func (p *PermissionFlow) Tool() string

Tool returns the tool name behind the pending ask, or "" when no ask is pending.

type QuestionFlow

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

QuestionFlow encapsulates the modal question card.

func NewQuestionFlow

func NewQuestionFlow() *QuestionFlow

NewQuestionFlow returns an inactive flow.

func (*QuestionFlow) Active

func (q *QuestionFlow) Active() bool

Active reports whether a question ask is pending.

func (*QuestionFlow) Cancel

func (q *QuestionFlow) Cancel() chan<- tools.QuestionResponse

Cancel clears the pending flow and returns the reply channel so the caller can send an empty response to unblock the agent.

func (*QuestionFlow) MoveDelta

func (q *QuestionFlow) MoveDelta(delta int)

MoveDelta moves the option cursor up (negative) or down (positive).

func (*QuestionFlow) Next

func (q *QuestionFlow) Next() (done bool, resp tools.QuestionResponse, reply chan<- tools.QuestionResponse)

Next advances to the next question or finishes. Returns done=true when all questions are answered, along with the full response and the reply channel.

func (*QuestionFlow) Open

func (q *QuestionFlow) Open(ev agent.EventQuestionAsk)

Open stores the ask and resets state. Caller switches to modeQuestion.

func (*QuestionFlow) Render

func (q *QuestionFlow) Render(t Theme, width int) string

Render returns the modal question card.

func (*QuestionFlow) Toggle

func (q *QuestionFlow) Toggle()

Toggle flips the selection of the current option (multi-select).

type RenderCache

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

func NewRenderCache

func NewRenderCache(max int) *RenderCache

func (*RenderCache) Get

func (c *RenderCache) Get(key string) (string, bool)

func (*RenderCache) Put

func (c *RenderCache) Put(key, value string)

type Scrollback

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

Scrollback is the deep module that owns the chat scrollback. Mutations only through methods. Indices into items are stable — callers may take them away and reuse them later.

func NewScrollback

func NewScrollback() *Scrollback

NewScrollback returns an empty Scrollback with no in-progress streams and no selection.

func (*Scrollback) AppendError

func (s *Scrollback) AppendError(text string)

AppendError adds an error line and closes any in-progress stream.

func (*Scrollback) AppendHookFired

func (s *Scrollback) AppendHookFired(hookName, event, decision, reason string, dur time.Duration)

AppendHookFired adds a hook-execution line. Deny decisions are rendered in red; other decisions are informational.

func (*Scrollback) AppendInfo

func (s *Scrollback) AppendInfo(text string)

AppendInfo adds an out-of-band notice. Closes streams: an info line interleaved with streaming would visually split the stream.

func (*Scrollback) AppendReasoning

func (s *Scrollback) AppendReasoning(delta string) int

AppendReasoning extends the in-progress reasoning block. Defends against deltas without a prior Start by lazily starting one. Returns the rough token estimate (chars/4) for chrome counters.

func (*Scrollback) AppendRepair

func (s *Scrollback) AppendRepair(kind, tool, message string)

AppendRepair adds a repair receipt line. Kind is one of args_completed, args_invalid, recovered, suppressed, schema_complex.

func (*Scrollback) AppendStepFinish

func (s *Scrollback) AppendStepFinish(stopReason string, usage llm.Usage, model string)

AppendStepFinish closes the step's footer line with usage and stop-reason. Ends any in-progress stream.

func (*Scrollback) AppendText

func (s *Scrollback) AppendText(delta string) (created bool, tokens int)

AppendText extends the in-progress assistant-text block; creates one if none is in progress. Returns (createdNewBlock, runningTokens). Callers (chrome) flip to "writing" phase when createdNewBlock is true.

func (*Scrollback) AppendToolCall

func (s *Scrollback) AppendToolCall(callID, tool, args string)

AppendToolCall records a tool invocation and closes any in-progress text stream — tool calls partition assistant text into segments.

func (*Scrollback) AppendToolResult

func (s *Scrollback) AppendToolResult(callID string, result tools.Result, dur time.Duration)

AppendToolResult records the matching result for callID. Walks items backward to recover the tool name and args from the prior toolCall.

func (*Scrollback) AppendUser

func (s *Scrollback) AppendUser(text string)

AppendUser closes any in-progress stream (turn boundary) and adds the user prompt line.

func (*Scrollback) AppendWelcome

func (s *Scrollback) AppendWelcome()

AppendWelcome adds the startup banner. Does not touch streams (no stream is in progress at startup).

func (*Scrollback) BeginSelection

func (s *Scrollback) BeginSelection(line int)

BeginSelection starts a visual-mode selection anchored at line. Captures the current seq; subsequent index-space mutations invalidate the selection on the next Render.

func (*Scrollback) CancelSelection

func (s *Scrollback) CancelSelection()

CancelSelection drops any active selection without yanking.

func (*Scrollback) Clear

func (s *Scrollback) Clear()

Clear empties the scrollback and resets stream cursors + selection.

func (*Scrollback) EndReasoning

func (s *Scrollback) EndReasoning()

EndReasoning closes the in-progress reasoning block. No-op if none.

func (*Scrollback) EndSelection

func (s *Scrollback) EndSelection() string

EndSelection returns the selected text (ANSI-stripped) and clears the selection. Returns "" if nothing was selected.

func (*Scrollback) EndStreams

func (s *Scrollback) EndStreams()

EndStreams closes any in-progress stream cursors. Idempotent — safe to call at turn boundaries (agent done) and from every one-shot appender as a defensive reset.

func (*Scrollback) ExpandLastResult

func (s *Scrollback) ExpandLastResult() bool

ExpandLastResult expands the most recent collapsed (and currently truncated) tool result. Returns true if it found one.

func (*Scrollback) ExtendSelection

func (s *Scrollback) ExtendSelection(line int)

ExtendSelection moves the selection cursor to line. No-op if no selection is active.

func (*Scrollback) FullLines

func (s *Scrollback) FullLines() []string

FullLines returns the line split from the last Render call. Returns nil if Render hasn't been called yet. Used by mouse/visual cursor math to clamp into a valid index range.

func (*Scrollback) InvalidateRenderCache added in v0.3.2

func (s *Scrollback) InvalidateRenderCache()

InvalidateRenderCache resets the per-item render cache AND the package-level diffCache WITHOUT dropping items or resetting seq (unlike Clear()). Used by theme switching so cached renders in the old theme are evicted.

func (*Scrollback) Items

func (s *Scrollback) Items() []chatItem

Items returns a read-only snapshot of the items slice. The slice header may be retained for the caller's read but must not be mutated; mutations during streaming would race.

func (*Scrollback) LastAssistantText

func (s *Scrollback) LastAssistantText() string

LastAssistantText returns the text of the most recent assistant text item, or "" when none.

func (*Scrollback) LastToolResult

func (s *Scrollback) LastToolResult() (tool, content string, ok bool)

LastToolResult returns the (tool, content) of the most recent non-empty tool result, or ("","",false) when none exists. Used by the pager overlay.

func (*Scrollback) Len

func (s *Scrollback) Len() int

Len returns the number of items.

func (*Scrollback) Render

func (s *Scrollback) Render(t Theme, width int) string

Render returns the ANSI-styled scrollback at the given width. The underlying line-split is cached on (width, seq); selection highlight is re-applied on every call since cursor moves don't bump seq.

Side-effect: if the active selection's seq has drifted (the items changed underneath it), the selection clears here. See file header.

func (*Scrollback) SelectionActive

func (s *Scrollback) SelectionActive() bool

SelectionActive reports whether a selection is in progress.

func (*Scrollback) SelectionCursor

func (s *Scrollback) SelectionCursor() int

SelectionCursor returns the current cursor line, or -1 if no selection is active. Used to drive viewport auto-scroll.

func (*Scrollback) SelectionRange

func (s *Scrollback) SelectionRange() (lo, hi int, active bool)

SelectionRange returns (lo, hi, active). lo ≤ hi.

func (*Scrollback) Seq

func (s *Scrollback) Seq() uint64

Seq returns the mutation counter. Bumped on any state change that affects rendering. Useful for tests and for dirty-detection.

func (*Scrollback) SetModel added in v0.3.0

func (s *Scrollback) SetModel(model string)

SetModel records the active main-loop model so subsequently appended tool items carry it (for the per-model tool-bar accent). Stamped at append time, so a mid-session /models switch only colors tool cards produced afterward.

func (*Scrollback) StartReasoning

func (s *Scrollback) StartReasoning()

StartReasoning marks the start of a reasoning block. The next AppendReasoning calls extend that block until EndReasoning.

func (*Scrollback) SwapSelectionEnds

func (s *Scrollback) SwapSelectionEnds()

SwapSelectionEnds swaps anchor and cursor, mirroring Vim's `o`.

func (*Scrollback) ToggleAllReasoning

func (s *Scrollback) ToggleAllReasoning()

ToggleAllReasoning flips every reasoning block to a single target state — collapse if any are open, else expand.

func (*Scrollback) ToggleLastReasoning

func (s *Scrollback) ToggleLastReasoning()

ToggleLastReasoning expands/collapses the most recent reasoning block. No-op if none.

type Theme

type Theme struct {
	Name string

	// --- Raw token colors, exported for call sites (never hex inline). ---
	BrandDeep   color.Color
	BrandLight  color.Color
	AccentFlash color.Color
	AccentPro   color.Color

	// Selection pairing — the focused-row band color + its text (see selBg).
	SelBg color.Color
	SelFg color.Color

	BgBase    color.Color
	BgWell    color.Color
	BgSurface color.Color
	BgRaised  color.Color

	FgBase   color.Color
	FgMuted  color.Color
	FgSubtle color.Color
	FgFaint  color.Color

	BorderColor color.Color
	OkColor     color.Color
	ErrColor    color.Color
	WarnColor   color.Color
	OnAccent    color.Color

	// Diff band colors (hard-coded per mode — see diffBands).
	DiffAddFg color.Color
	DiffAddBg color.Color
	DiffDelFg color.Color
	DiffDelBg color.Color

	// --- Legacy semantic styles (re-derived from tokens). ---
	UserPrompt    lipgloss.Style
	AssistantText lipgloss.Style
	Reasoning     lipgloss.Style
	ReasoningFold lipgloss.Style

	ToolCall lipgloss.Style
	ToolOk   lipgloss.Style
	ToolErr  lipgloss.Style
	ToolBody lipgloss.Style

	HookInfo lipgloss.Style
	HookDeny lipgloss.Style

	Repair lipgloss.Style

	Status      lipgloss.Style
	StatusModel lipgloss.Style
	StatusGood  lipgloss.Style
	StatusBad   lipgloss.Style

	Info  lipgloss.Style
	Error lipgloss.Style
	Hint  lipgloss.Style

	InputBorder    lipgloss.Style
	InputBorderDim lipgloss.Style // Normal mode: dim border around textarea
	PermPrompt     lipgloss.Style
	PermFocus      lipgloss.Style // focused permission-card button (inverted)
	PermButton     lipgloss.Style // unfocused permission-card button

	// Ocean visual identity — brand gradient endpoints + card styles.
	CardBar    lipgloss.Style // left sidebar bar (foreground BrandDeep)
	CardHeader lipgloss.Style
	CardBody   lipgloss.Style
	// contains filtered or unexported fields
}

Theme holds the lipgloss styles used across the TUI, composed from a raw palette. We ship a dark theme as default and a light alt. Theme is selected at startup from config.

The layout is two-layer (crush-style): the unexported palette holds raw tokens; the exported fields below are semantic styles/colors derived from them. New components should prefer the token colors + the Panel/Badge/ LeftBar/Gutter helpers over the legacy named styles.

func AuroraTheme added in v0.3.2

func AuroraTheme() Theme

AuroraTheme returns the aurora theme — cool teal & green lean.

func DarkTheme

func DarkTheme() Theme

DarkTheme returns the default dark theme (DeepSeek Ocean).

func LightTheme

func LightTheme() Theme

LightTheme returns the light alt. Same semantics, inverted contrast.

func MidnightTheme added in v0.3.2

func MidnightTheme() Theme

MidnightTheme returns the midnight theme — azure on near-black, max contrast.

func NebulaTheme added in v0.3.2

func NebulaTheme() Theme

NebulaTheme returns the nebula theme — indigo & violet lean.

func PickTheme

func PickTheme(name string) Theme

PickTheme returns the configured theme by name; defaults to dark on unknown names. truecolor is detected from the environment so themes built here degrade fills automatically on limited terminals; callers may still override via WithTransparent (config opt-out).

func (Theme) Badge added in v0.3.0

func (t Theme) Badge(kind BadgeKind) lipgloss.Style

Badge returns a filled chip style: background = the semantic color for the kind, foreground = onAccent, Padding(0,1), Bold. Unlike Panel, a badge is a deliberate accent and stays filled even in transparent / degraded modes (it's a small chip, not a surface) so status semantics survive.

func (Theme) Gutter added in v0.3.0

func (t Theme) Gutter() string

Gutter returns the 2-cell left gutter string prepended to every chat item.

func (Theme) IsLight added in v0.3.2

func (t Theme) IsLight() bool

IsLight reports whether this is a light-canvas theme.

func (Theme) LeftBar added in v0.3.0

func (t Theme) LeftBar(c color.Color) lipgloss.Style

LeftBar returns the role-bar style: the thick bar glyph rendered in the given color. Callers render the glyph via this style (e.g. theme.LeftBar(theme.BrandDeep).Render(tui.LeftBarGlyph())).

func (Theme) Panel added in v0.3.0

func (t Theme) Panel(tier PanelTier) lipgloss.Style

Panel returns a lipgloss.Style backgrounded with the given tier's surface color. It returns NO background (a plain foreground-only style) when the Theme is in transparent mode OR the terminal is not truecolor — callers then degrade to left-bars / separators rather than opaque fills.

func (Theme) Transparent added in v0.3.0

func (t Theme) Transparent() bool

Transparent reports whether canvas painting / bg fills are disabled.

func (Theme) Truecolor added in v0.3.0

func (t Theme) Truecolor() bool

Truecolor reports whether the terminal can render 24-bit color.

func (Theme) WithTransparent added in v0.3.0

func (t Theme) WithTransparent(v bool) Theme

WithTransparent returns a copy of the theme with the transparent flag set. Used to thread the ui.transparent_background config opt-out into the theme.

func (Theme) WithTruecolor added in v0.3.0

func (t Theme) WithTruecolor(v bool) Theme

WithTruecolor returns a copy of the theme with the truecolor flag set. Used to thread color-profile detection into the theme.

Jump to

Keyboard shortcuts

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