ui/theme package. It defines a semantic palette (colors named by role, not by hue), detects the terminal’s color capability, and ships 12 ready-made themes (dark, light, grafite + 9 community palettes) that re-skin the entire UI — chat, /coder and /agent cards, borders, markdown, code blocks, and spinners — with no restart.
The theme is process-global state. Switching applies on the next render, no restart needed. Unlike
CHATCLI_CODER_UI (the timeline style), which the renderer re-reads from the environment on every call.Switch themes at runtime
/config ui) shows the active theme, the source of the value (environment variable vs. default), the detected color profile, and the list of themes with the active one marked by →.
Available themes
There are 12 themes in total. Each strip shows the name and the palette’s real colors, in the order model · accent (reasoning) · ok · warn · error:dark, light and grafite are ChatCLI’s calibrated variants; the other nine are adaptations of the classic community palettes mapped onto ChatCLI’s semantic roles (same structure, different colors). All of them degrade to 256 and 16 colors while keeping the roles distinguishable.Colorize and the ansiColorToLip converter route through the active theme — there’s no call-site churn.
Semantic palette
Colors are named by the role they serve, not by hue. That’s what makes swapping the whole theme a matter of touching only the palette:The input line and the completion dropdown
The REPL prompt is drawn bygo-prompt, which owns its own render loop and never sees the ANSI strings the rest of the UI builds. Until v1.199 its colors were literals — the text you typed was hardcoded to white — so a light theme on a light terminal printed white on white. Every one of those colors now comes from the active palette:
Ink is always chosen against the fill it lands on, never against the terminal —
Background is the theme’s own ground, so it is the readable ink on a saturated row under a dark and a light palette.
go-prompt copies these onto its renderer when the prompt is built and never re-reads them, so /config ui theme rebuilds the prompt on the spot. That is the one surface where a theme switch does more than wait for the next render.Legibility contract
Every built-in palette is held to a machine-checked floor, so “opinionated about hue” never becomes “unreadable”:- Body text (
Text,TextStrong) clears 4.5:1 against the theme’s own ground;Muted— secondary text — clears 3:1. - On a 16-color terminal a light palette never reaches into the bright range (9–15, which is painted for a dark ground), and a dark palette never uses index 0.
- Each dropdown fill/ink pair clears 3:1 and never collapses onto the same 16-color index.
Border is deliberately exempt from the contrast floor: Nord, Solarized, Catppuccin and One Dark all define their border tone as a near-ground fill on purpose, and raising it would replace the palette’s identity with ours.
Themed markdown
Markdown is rendered by glamour with aStyleConfig derived from the palette (replacing the legacy glamour.WithStandardStyle("dark")), so markdown and code-block colors share the UI’s hues. Syntax highlighting via chroma and a language chip above each code block. The document is rendered whole (not block by block), so reference links, footnotes, and paragraph spacing all resolve correctly.
Color profile and graceful degradation
ChatCLI detects the terminal’s capability and degrades gracefully. In pipes, CI, ordumb terminals, output becomes clean plain text with no color codes.
Detection honors the usual signals (
NO_COLOR, CLICOLOR_FORCE, TERM, COLORTERM). The dark theme keeps the ANSI16 = 10 index for green, so 16-color terminals are identical to legacy behavior.
Persistence
A runtime switch applies only to the current process. To pin a default across sessions, add to your.env:
.env on its own. See CHATCLI_THEME in the environment-variable reference.
Parallel change in v1.125: the chat reply envelope gained a footer with per-turn cost and context usage, and spinners were unified into a single themed braille spinner, shown only when a terminal is present.