Themes

Choose built-in Auto, Light or Dark terminal appearance; theme-file customization is disabled.

Manual for v0.9.5. Stable release: v0.9.5.

octet's compiled default theme keeps model-family accents, adapts to your terminal background and uses semantic status colors. Pick a theme with /theme, --theme NAME or OCTET_THEME.

Choose a theme#

At first run, the appearance picker offers:

  • Auto (recommended) detects the terminal background from reliable environment or terminal-capability signals, and uses a readable neutral fallback when detection isn't possible.
  • Light terminal and Dark terminal pick the matching contrast profile and override detection.

Moving through the picker previews each appearance without saving it. Confirming saves theme = "auto", "light" or "dark" in your user config without replacing other settings. Dismissing onboarding uses Auto. Existing configured installs don't reopen onboarding, and print, plain and RPC sessions, redirected output and TERM=dumb never open it.

Use /theme later to browse the built-in choices and valid .toml or Pi .json files from normal resource discovery: global ~/.octet/themes, a trusted project's .octet/themes, and directories or files passed with --theme-dir. The list shows each file stem and its optional metadata name and description, and typing filters it. Selecting a file previews it, and confirming saves its file stem (for example theme = "mine") in your user config, leaving other settings intact. /theme mine (or /theme mine.toml) picks a discovered, valid file directly; /theme Cards, /theme Still and /theme pi pick the built-ins described below. Cancelling restores the active theme without touching your config or session. The first-run picker still shows only Auto, Light and Dark.

Discovery goes global, then trusted project, then explicit path, and the later one wins. When two files share a stem, only the winner shows, even if it's invalid. Files that fail bounded reads or schema validation are left out of the list. The stems auto, light, dark, default, Cards, Still and pi are reserved for built-in selectors and never show up as file choices.

--theme and OCTET_THEME take the built-in choices, or a named file at startup. There's no theme marketplace. An unrecognized or malformed name at startup falls back to the compiled default. An explicit OCTET_COLOR_SCHEME also counts as an existing terminal-appearance choice, so automation and already-configured shells aren't interrupted by onboarding.

What the default theme does

Auto, Dark and Light share the same default geometry: a 16×4 model-blended startup mark, a full-cell model-adaptive card for each historical prompt (the stored color of the model that received it fills the whole cell, cushion rows and inline Markdown styling included), model-colored composer rules, one blinking tool-like subagent transcript row while workers run, two-row command previews, and full available prose width without narrowing code or tables. The footer may omit a redundant catalog-owned provider prefix, never a configured model name. These are compiled presentation policies, not new theme-file fields: custom surfaces, composer frames and loaded theme colors and geometry keep their own styling. Unknown-background and no-color terminals keep a readable, unpainted prompt.

A theme that sets content_max_width gets a centered reading column for its text, but its filled chrome still reaches the terminal edges. A shaded composer and borderless band or rail surfaces paint edge to edge instead of floating as a narrow island with unpainted bars beside them. Bordered cards, plain text, and themes that only inset their content without capping the column are unchanged.

Prompt provenance#

Every submitted prompt stores the exact color of the model that received it, so a session keeps its provenance after a model switch or a restart. The default theme turns that stored color into a full-cell card. The background covers the whole row (marker gutter, padding, trailing canvas, and the breathing rows above and below), and the rich renderer's bold, italic and inline-code runs are layered inside the card instead of flattened onto one color. Unknown-background and no-color terminals leave the prompt unpainted on the terminal canvas.

One top-level token changes that. prompt_wash = false keeps prompt rows on the surface's own fill (a [roles."surface.user"] background, a band, or the terminal canvas) while the chevron keeps its prompt color. Unset means the model-color wash. Cards and Still set it to false. Unknown background profiles are always unpainted, so prompt_wash never invents a fill where the terminal could be light, dark or custom.

Native Tern appearance#

Inside Tern, octet sends resolved dark and light semantic palettes and reapplies the active model accent on selection. Native projection uses the validated loaded theme snapshot, so switching models does not re-read or reinterpret custom theme files. The composer uses Tern's native editor layout with octet's controls and identity. The compiled default supplies a contrast-balanced model tint; prompt_wash = false preserves the custom user-surface fill, and loaded themes retain their selection fill. Stored historical prompt colors remain intact, but native prompt cards share the active Tern palette's tint; per-turn full-cell fills remain an ANSI-renderer feature. TSP owns fonts and widget geometry, so custom layouts and ANSI-only style flags are not exact cell-for-cell native contracts. See octet in Tern for compatibility limits and protocol verification.

Activity status contrast#

Thinking and Working use model-family foreground colors only, and the shimmer never paints a character background. Set OCTET_SHIMMER=classic for the legacy stepped shimmer, for A/B testing. Classic is also forced for ANSI16, unknown-background, no-color and reduced-motion profiles, and an unrecognized value uses the physical default when the terminal supports it.

The physical shimmer

On known Dark and Light TrueColor or ANSI256 profiles, the default physical shimmer uses a raised-cosine light field with a short leading edge, a longer trailing tail and a small central glint. Dark labels move from about 0.55 toward 0.99 luminance, and light labels from about 0.0085 toward 0.11. The motion takes roughly 0.85 to 1.0 seconds, then parks for two ticks before repeating. The margin dot shares that phase without changing its glyph or size. The qualification fixture checks representative Dark and Light composited surfaces, and doesn't measure arbitrary transparency. Custom ANSI16 palettes can also change perceived contrast.

Tool call and thinking verbosity#

A theme chooses how much of the agent's work the transcript shows by default. Both settings are boolean tokens in the theme's [colors] table:

Token Default Effect
quiet_tool_summaries false Groups consecutive read-only tool calls into one summary row such as Explored 3 files · 1 command, with edits, web, MCP and other activity summarized the same way. Still sets it. Press Ctrl+O to open the individual calls.
thinking_expanded false Shows the model's thinking trace as it streams, and when you resume a session, instead of a collapsed Thinking line. The built-in pi theme and every Pi palette projection set it, as Pi does.

The compiled default theme collapses thinking to a single Thinking line; Ctrl+O expands it. A theme that sets thinking_expanded = true starts every thinking trace open, and [layout] show_reasoning = false still hides thinking entirely.

toml
[colors]
quiet_tool_summaries = true
thinking_expanded = false

Startup and terminal replies#

The first branded frame waits for resolved model and setup state, workspace and appearance, and the welcome is static at that boundary. SSH alone doesn't reduce the advertised terminal color capability. Native Windows never sends the background query: the console host turns a reply into key records, and Windows Terminal's reply reached the composer as typed text. Auto uses the neutral fallback there, so pick --theme dark, --theme light or /theme for a contrast profile.

How the OSC 11 background query behaves

Auto can issue one OSC 11 background query without waiting before model/session initialization. An already-arrived reply is applied before the first branded frame; readiness never waits for the probe. If no reply has arrived, that frame uses the neutral fallback and the existing short post-ready detection wait can update it. Explicit Light/Dark choices and no-color output skip the probe. The shared input owner keeps recognizing a late reply after that deadline, including slowly fragmented bodies once the OSC 11 header is recognized. Genuine typing and bracketed paste are kept. Escape and Alt+] stay genuine keys: an incomplete opening header has a 250 ms ambiguity timeout, and a header fragmented more slowly can still pass through as input. Use explicit --theme dark or --theme light to skip the query on such terminals. This isn't a guarantee for arbitrary terminal-protocol corruption.

Variant reference#

octet ships a complete reference file for the compiled default theme at examples/themes/octet-default.toml. It isn't read at runtime, because the compiled default is always the fallback, but it documents the accepted sections in one place and is a valid starting point:

console
cp examples/themes/octet-default.toml ~/.octet/themes/mine.toml

pi#

pi reproduces Pi 1.0's default system palette, pinned to Pi 1.0.2 (cd32f7725fdbddbaecdff5b1e68491563394e0ca), not the older dark.json theme. It is built into the Rust binary and needs neither Node nor the Pi extension:

console
octet --theme pi
OCTET_THEME=pi octet

The palette uses the terminal's background, foreground and complete 16-color palette when reported. Pi's contrast curves preserve palette hues and cap chroma so muted themes stay muted. With only a background, it uses Pi's default color families; with no reported colors or known appearance, it uses ANSI/default colors and faint neutral text without inventing panel fills. Known light/dark appearance without RGB uses black/white reference backgrounds. Model switches do not recolor the Pi palette or replace its prompt fill with Octet's model wash. Pi presentation uses full-width, one-cell-padded prompt and tool fills, unboxed assistant text, and an 80 ms braille spinner beside a non-shimmering working label (ASCII fallback without Unicode; reduced motion keeps a static native marker). Completed tool fills use the palette's success/error colors. Imported Pi JSON and newly generated adapter snapshots use the same presentation policy. Tern requests native tool cards and rules around the real editor; its renderer still owns pixels, fonts and widget padding. Octet keeps its controls, settings and input ownership, so this is not a cell-for-cell copy of Pi's UI. Reviewed extension footers can replace stock footer chrome; selecting a palette does not activate those extensions or import your Pi setup.

Interactive Unix terminals are queried without delaying readiness. The existing single input owner consumes OSC 10/11/4 replies, including late and fragmented replies; only all 16 palette slots establish a complete palette. Focus return refreshes the active pi palette. Native Windows, non-TTY and no-color output keep the no-query policy; an explicit OCTET_COLOR_SCHEME skips querying too. Tern receives separately resolved light/dark palettes with Pi's thinking colors. The pi stem is reserved and cannot be shadowed by a local theme file.

Cards#

examples/themes/Cards.toml is compiled into every release as the built-in Cards selector, so it's available without copying anything:

console
octet --theme Cards
OCTET_THEME=Cards octet

Cards is a shaded-surfaces theme: flush-left transcript rails and prose with one-cell content padding inside the rails, dark and light fills, historical prompts' model-colored rails and chevrons (rather than amber rails), a quiet amber UI accent, a borderless shaded composer and a themed compact splash. Its slash-command choices align with the shaded composer's chevron and input text. It's offered in /theme alongside the terminal-appearance choices.

Cards rails, reserved stem and validation

The prompt_rail_model_adaptive = true token opts a file theme's user rails into each prompt's stored model color, falling back to its historical model family (or the active accent if none is known). Other surface border roles keep controlling their own rail colors. The Cards stem is reserved, so a local Cards.toml can neither shadow nor be shadowed by the built-in. Editing examples/themes/Cards.toml changes the built-in, and cards_example_theme_is_valid_for_every_background_profile in crates/octet-coding-agent/src/tui/theme.rs fails the build if it stops validating for any terminal-background profile.

Still#

examples/themes/Still.toml is compiled in the same way, as the built-in Still selector:

console
octet --theme Still
OCTET_THEME=Still octet

Still uses the available terminal width for its transcript and softly shaded composer. A quiet band distinguishes user prompts, and one extra breathing cell before transcript markers aligns prompt, activity and prose text. Prose and tool activity stay unboxed, with compact adjacent rows and monochrome dots. Consecutive reads, searches and commands share an exploration summary, and consecutive edits and writes show a distinct-file count. Web searches and fetches, MCP calls and computer-use actions each get their own quiet summary, and delegation keeps its existing subagent presentation. Ctrl+O reveals individual paths, commands, calls and failures. The dark UI uses a restrained blue accent over neutral grays, and reasoning is hidden by default. It's offered in /theme after Cards.

Still tokens, liveness and validation

Its model.use_lab_color = "true" token keeps the prompt chevron and composer marker model-adaptive, and splash_model_adaptive with splash_compact give it the compact 16x4 startup mark shaded from the active model family. The reserved row under its live Working indicator keeps the composer from bouncing as two-line Thinking takes over. While tools run, including collapsed commands, the Working indicator stays below their activity until the run settles. This liveness behavior also applies to the default theme. The Still stem is reserved, and still_example_theme_is_valid_for_every_background_profile in crates/octet-coding-agent/src/tui/theme.rs validates the embedded file for every terminal-background profile.

The startup welcome card follows the theme's content width and leading inset, just like the transcript, composer, and pickers. Width-capped custom themes center the splash; uncapped themes such as Still use the available width. The welcome fits the actual pane height after reserving the composer and gap. Short panes use a compact identity and permission disclosure instead of sending an oversized decorative card into native history. Late update hints replace existing hint rows rather than moving the composer.

Three optional top-level tokens shape the startup splash:

  • splash — colour for the byte-mark and splash text. On a truecolor terminal it uses a static column gradient.
  • splash_compact — true selects the default's smaller geometry (4-tall mark at 16 columns) instead of the larger file-theme presentation.
  • splash_model_adaptive — true keeps the default's model-adaptive splash: the byte-mark follows the active model family and the splash text uses the model accent. It claims the whole splash, so splash is not used. Cards uses it.

A native theme file is a bounded TOML document (256 KiB) with these typed sections:

  • [metadata]: name, description, author, version, terminal (light-dark, dark, light or any), and optional adaptive to rebalance RGB foregrounds and surfaces for the detected terminal background.
  • [colors] / [tokens]: flat or nested color and token values. "default" means the terminal's own color.
  • [roles.<name>]: per-role foreground, background, bold, dim, italic, underline, strikethrough, inverse and adaptive.
  • [glyphs] / [glyphs_ascii]: typed glyphs. Every structural glyph is one column, and ASCII fallbacks must be ASCII.
  • [surfaces.<kind>]: bounded layout recipes for the user, assistant, reasoning, tool, notice, outcome, shell and compaction transcript surfaces.
  • [layout]: density, shell visibility and narrow-terminal overrides.
  • [variants.*]: background overlays, described next.

[variants.universal], [variants.dark] and [variants.light] merge recursively over the base document using the same section shapes. universal applies to every terminal, then dark or light overlays it for the detected background profile, so one variant can override a single token without restating the whole table. [variants.unknown] applies when detection fails.

Theme files reject terminal control bytes, unknown sections and fields, invalid role names, non-ASCII ASCII-fallbacks, wide or empty structural glyphs and oversized values. Unknown or partial files fall back to the compiled default rather than starting with a broken shell.

Pi JSON themes#

An enabled, reviewed octet-pi-compat contributes your existing enabled Pi palettes to /theme at startup and /reload, including user themes and already installed package themes. This works without --mirror; enabling palettes does not execute additional Pi factories or activate their skills, models or settings. Project .pi resources still require workspace trust. Disabling or retiring the extension withdraws its paths; explicit Octet theme paths and selections retain precedence. Discovery never writes Pi settings. Malformed, linked, oversized or filtered themes are omitted with diagnostics, and the shared 64-path resource budget is enforced. See Pi setup.

The native loader also accepts bounded Pi JSON themes in the same discovery roots. It resolves hex colors, ANSI indices, terminal-default empty strings, variable references and optional-token fallbacks into native semantic roles. Pi oklch(...) and okhsl(...) colors are converted directly to sRGB using the pinned Pi 1.0.2 implementation (commit cd32f7725fdbddbaecdff5b1e68491563394e0ca), including OKLCH gamut mapping. For example, oklch(62% 0.1 200) and okhsl(250 60% 55%) work directly or through variable references, without the Node adapter or a conversion step. Malformed colors and out-of-range channels are rejected. Pi prompt/tool fills, static status labels and namespaced colors are preserved without model-accent recoloring, including normalized snapshots. HTML export colors are validated but not rendered. This does not reproduce arbitrary extension renderers, settings menus, or an unreviewed user's package setup.

JSON and TOML share trust, no-follow, size, reserved-name and later-root-wins rules. Directory entries are ordered lexically, so a same-stem TOML file wins inside a directory. A malformed winning file does not expose an older shadowed file. Reload uses the original source path; snapshots use normalized native TOML.

Semantic role vocabulary#

[roles.<name>] is typed. These names are the published, terminal-independent vocabulary that maps onto octet's semantic text roles. The published_semantic_role_vocabulary_is_closed_and_accepted test in crates/octet-coding-agent/src/tui/theme.rs keeps this list and SEMANTIC_ROLE_VOCABULARY in sync. Alias spellings (foreground and text, dim and subtle, code and md_code_block, diff_add and diff_added, and so on) resolve to the same underlying role.

The role names

text, foreground, muted, subtle, dim, accent, success, warning, error, heading, md_heading, emphasis, md_emphasis, strong, md_strong, inline_code, md_code, code, md_code_block, quote, md_quote, border, link, md_link, list_marker, md_list_bullet, diff_add, diff_added, diff_remove, diff_removed, diff_context, diff_hunk, diff_header, syntax_comment, syntax_keyword, syntax_function, syntax_variable, syntax_string, syntax_number, syntax_type, syntax_operator, syntax_punctuation.

Extension-contributed themes and roles#

Two channels are open to extensions:

  • Roles. Any name of the form extension.<namespace>.<role> is accepted in [roles] (up to 96 bytes, ASCII alphanumerics plus _, - and .). The schema is open but typed: an extension may add [roles."extension.git.branch"] without a host change, but can't inject the same name under a private, unnamespaced key. semantic_extension_roles_are_open_but_typed in theme_schema.rs tests this.
  • Theme files. An extension package may ship themes/<name>.toml. Install or copy it into a discovery root so the shared resolver can select it: ~/.octet/themes/ (global), .octet/themes/ (project, needs trust), or a directory passed with --theme-dir. Discovered files use the same bounded, no-follow reader as the compiled default and never run extension code.

A manifest-level contributes.themes channel is not implemented. An admitted API 0.4 process may instead contribute temporary roots through resource_paths_v1, when an active App consumer negotiates it. Startup/reload publication and owner retirement govern these roots; registration metadata alone grants no access. Copying a file into a normal discovery root remains supported.