Documentation
¶
Overview ¶
Package ui holds the leaf terminal-UI helpers shared across the cmd/golder and internal/cli subpackages: ANSI color gating (color.go), turn-end Markdown rendering (markdown.go), and prompt image-reference parsing (imageref.go). These were moved verbatim from cmd/golder (US-002, #358) and exported so the repl, btw, status and goal layers style output through one owner.
This file holds the shared colored-diff renderer used wherever a tool result carries a unified diff (the edit tool today): the compact REPL/btw/goal output goes through RenderDiffLine, and the TUI reuses DiffFromDetails to pull the diff out of a result's Details metadata. Coloring follows the convention of git and Claude Code - red removals, green additions, dim context and file headers, cyan hunk markers - gated by the same NO_COLOR / non-terminal rules as every other ui color.
This file implements the local-image input syntax for prompts (US-010/#126). A prompt may reference local image files with either `@image:<path>` or the Markdown image form ``. Each reference is read from disk, base64-encoded, and attached to the user message as an agentcore.ImageContent block so a multimodal model can see it. The remaining (non-reference) text is kept as a TextContent block. References that cannot be read are reported as an error rather than silently dropped, so the user knows the image was not sent.
This file renders assistant replies as Markdown for interactive terminals. The line-oriented REPL streams model text token-by-token, but Markdown can only be laid out once the whole block is known (a table or fenced code span needs its full extent). So rendering is a turn-end concern: the caller buffers the streamed text and calls RenderMarkdown once the assistant turn closes.
Rendering is gated exactly like color (Enabled): only an interactive, NO_COLOR-unset stdout gets styled output. Pipes, files, CI, and tests receive the raw Markdown source unchanged, so machine consumers and golden tests are unaffected. Any renderer failure also falls back to the raw source — pretty output is never allowed to lose content.
This file holds the compact tool-activity renderers shared by the REPL, the /goal autonomous loop, and /btw side threads: a tool call is shown as a green "→ tool:" line and a tool result as a green "← result:" (or red "← error:") line, with multi-line output collapsed to one line. Two results are printed in full instead: the todo tool's (so the live checklist stays visible) and any result carrying a diff, e.g. edit's, which renders its colored diff (diffrender.go, #560).
Index ¶
- Constants
- func BuildUserContent(prompt string) (agentcore.ContentList, error)
- func Colorize(enabled bool, code, s string) string
- func DiffFromDetails(details any) (string, bool)
- func DiffPreviewLines(diff string) []string
- func DisplayHomePath(p string) string
- func Enabled() bool
- func OneLine(s string) string
- func RenderDiffLine(line string, enabled bool) string
- func RenderMarkdown(src string) string
- func RenderToolResult(out io.Writer, tr agentcore.ToolResultMessage)
- func StdoutIsTerminal() bool
- func ToolCallLabel(c agentcore.ToolCallContent) string
- func Width(s string) int
- func WithoutCodeBlockErrorChip(cfg gansi.StyleConfig) gansi.StyleConfig
Constants ¶
const ( Reset = "\033[0m" Bold = "\033[1m" Dim = "\033[2m" Cyan = "\033[36m" Green = "\033[32m" Red = "\033[31m" Yellow = "\033[33m" )
ANSI SGR escape sequences used by the REPL. This is a handful of codes, not a general-purpose styling library.
const ( // InterruptingNotice acknowledges the keypress while the run unwinds. The // cancellation is a request, so the notice must not promise the run has // already stopped. InterruptingNotice = "(interrupting…)" // InterruptingQueuedNotice is the same acknowledgement when input is queued: // the interrupt doubles as "send it now", so the notice says what comes next. InterruptingQueuedNotice = "(interrupting — queued input will be sent next)" // ShellInterruptingNotice acknowledges a press aimed at a `!` passthrough // command, which is a process of its own rather than an agent run. ShellInterruptingNotice = "(interrupting the shell command…)" // InterruptNotice is the settlement line: the run has actually ended because // it was interrupted. It is the one line both front-ends print for that, so // an interrupted turn is recognizable without reading the surrounding text. InterruptNotice = "Interrupted." // DraftDiscardedNotice reports the shell-like first stage of Ctrl+C: the // composer was thrown away, and nothing else happened. DraftDiscardedNotice = "(draft discarded)" // DraftDiscardedRunningNotice is that same first stage while a run is in // flight, where "nothing else happened" is the part the user needs told: a // press here is at least as likely to have been aimed at the run as at the // draft, so the notice says the run is untouched and names the key that // does stop it (Esc, or one more press). DraftDiscardedRunningNotice = "(draft discarded — Esc, or Ctrl+C again, interrupts the run)" // SelectionCopiedNotice reports the Ctrl+C copy branch, so the press is // acknowledged instead of silently doing nothing visible. SelectionCopiedNotice = "(selection copied)" // QuitArmedNotice reports the armed idle quit, which needs one more press. QuitArmedNotice = "Press Ctrl+C again to quit." )
Interrupt and quit notices, shared by both front-ends.
Ctrl+C used to speak differently in every branch and in every front-end: the TUI printed "(interrupting the current run…)" on the press and then "Run ended: context canceled" or "error: aborted" when the run settled, while the REPL printed "^C interrupted" or "error: aborted" — and two branches (discarding a draft, copying a selection) printed nothing at all. The same keystroke read as five different things, which is what made the behavior feel inconsistent even when it was working. These constants are the single source of that wording, so the press and its settlement read the same everywhere.
const MaxDiffPreviewLines = 50
MaxDiffPreviewLines caps how many diff lines the compact tool-result renderer prints before eliding the rest with a notice. Without it a large replace_all would scroll pages of diff through the REPL.
Variables ¶
This section is empty.
Functions ¶
func BuildUserContent ¶
func BuildUserContent(prompt string) (agentcore.ContentList, error)
BuildUserContent turns a raw prompt line into a content list. When the prompt contains no image references it returns a single TextContent (the common case). Otherwise it returns the interleaved text and image blocks, reading and base64-encoding each referenced file. It returns an error if any referenced file cannot be read, so the caller can surface it instead of sending a prompt that silently omits the image.
func Colorize ¶
Colorize wraps s in the given SGR code(s) and a reset when color is enabled, and returns s unchanged otherwise. Callers decide the code; an empty code returns s as-is so it is safe to call unconditionally.
func DiffFromDetails ¶
DiffFromDetails extracts the unified diff a tool reported in its Details metadata (the edit tool stores map[string]any{"diff": ...}). The boolean is false - and the string empty - when there is no diff to show, so callers can fall back to the generic rendering. Any tool that starts reporting a "diff" key gets colored output for free.
func DiffPreviewLines ¶
DiffPreviewLines splits a unified diff into at most MaxDiffPreviewLines lines, appending a "… (+N lines elided)" notice when more were dropped, so the compact renderer never scrolls pages of diff through the REPL.
func DisplayHomePath ¶ added in v1.2.20
DisplayHomePath shortens a path under $HOME to "~/…" for display. The path itself is never rewritten — a grant, a decision, or a file handle always uses the full path; this is only what the user reads.
func Enabled ¶
func Enabled() bool
Enabled reports whether ANSI color should be emitted. Color is on only when stdout is an interactive terminal and NO_COLOR is unset (mirrors the https://no-color.org convention). This keeps piped/redirected output and CI logs free of escape codes.
func OneLine ¶
OneLine collapses a possibly multi-line string into a single trimmed line, truncating very long values, for the compact tool-activity statuses.
func RenderDiffLine ¶
RenderDiffLine renders one line of a unified diff with ANSI colors: ---/+++ file headers dim, @@ hunk markers cyan, removals red, additions green, unchanged context dim, and elision notices (leading ellipsis) dim. Anything else (including empty lines) passes through unchanged. enabled gates the codes as usual.
func RenderMarkdown ¶
RenderMarkdown returns src rendered as styled terminal Markdown when output is an interactive terminal, and src unchanged otherwise. A nil/broken renderer or a render error also returns src, so content is never dropped in favor of styling. The returned string carries its own trailing newline from glamour; callers should not add another.
func RenderToolResult ¶
func RenderToolResult(out io.Writer, tr agentcore.ToolResultMessage)
RenderToolResult prints a tool result to out: the todo tool's result is shown in full (indented) so the live checklist stays visible; a result carrying a diff (edit today, #560) prints its summary line plus the colored diff; every other result is collapsed to a single "← result:"/"← error:" line.
func StdoutIsTerminal ¶
func StdoutIsTerminal() bool
StdoutIsTerminal reports whether stdout is an interactive terminal (not a pipe/file). It gates color output and is also used to decide print vs interactive mode.
func ToolCallLabel ¶
func ToolCallLabel(c agentcore.ToolCallContent) string
ToolCallLabel renders a tool call as "name args" for the compact "→ tool:" status. Empty or "{}" arguments collapse to just the name.
func Width ¶
Width reports the number of terminal cells the rendered string s occupies. It delegates to lipgloss v2's width measurement, which strips ANSI escape sequences and counts East Asian wide / fullwidth runes (CJK, emoji) as two columns. This is the single width primitive the TUI and REPL layers style through (per the tui-agent SPEC), so alignment stays consistent across the codebase instead of each caller hand-rolling its own East-Asian-width table.
For multi-line input, Width returns the width of the widest line (lipgloss measures the bounding box), matching how the renderer lays text out.
func WithoutCodeBlockErrorChip ¶ added in v1.2.10
func WithoutCodeBlockErrorChip(cfg gansi.StyleConfig) gansi.StyleConfig
WithoutCodeBlockErrorChip returns a copy of cfg whose code-block Error token style renders as plain block text instead of glamour's stock red chip.
glamour highlights every code block through chroma. A block with no language tag is auto-analysed, and the guess can land on a lexer that reads ordinary prose — Chinese annotations in a directory listing, say — as syntax errors, marking most of the block Error. The stock style paints Error on a saturated red background (#F05B5B dark / #FF5555 light), so a harmless listing renders as a wall of red. The tokens stay errors; they just reuse the block's Text style, mirroring how the inline-code override works.
CodeBlock.Chroma is a pointer shared with the package-level configs, so the Chroma struct is copied before Error is touched: callers can pass styles.DarkStyleConfig / LightStyleConfig without mutating the shared style.
Types ¶
This section is empty.