Background
Multiple users have asked for built-in theme switching. Right now we ship exactly one dark palette, hardcoded at module-eval time across two parallel token sets (src/cli/ui/theme.ts and src/cli/ui/theme/tokens.ts). Users on light terminals, high-contrast setups, or with strong palette preferences have no way to opt out short of forking.
Goals
- Ship at least three first-party themes:
dark (current default), light, and high-contrast.
- Persist the selection across sessions in
~/.reasonix/config.json.
- Switch live from inside the chat UI via
/theme <name> and from the CLI via reasonix theme <name> / reasonix theme list.
- Lay the groundwork for user-defined themes (JSON file dropped in
~/.reasonix/themes/) without committing to that surface in v1.
Non-goals (v1)
- Per-card or per-component overrides.
- Hot-reloading user theme files on disk change.
- Light-mode redesign of cards that currently rely on dark-only contrast (tracked separately as visual polish once the plumbing lands).
Proposal
1. Unify the token surface. Collapse theme.ts and theme/tokens.ts into one canonical Theme shape (COLOR, SURFACE, FG, GRADIENT, CARD, GLYPH). The two-file split is a historical accident and blocks a single-source swap. This is a precondition, not a feature.
2. Theme registry. Move the current constants into themes/dark.ts, add themes/light.ts and themes/high-contrast.ts, and expose them through themes/index.ts keyed by name. The active theme is resolved once at startup from config and held in a small context.
3. Runtime access pattern. Replace direct import { COLOR } from \"./theme\" reads with a useTheme() hook (and a non-React getTheme() for plain modules). Components re-render on swap via context. Migration is mechanical across the ~57 call sites; it can land incrementally behind a default-resolved theme so partial migrations don't break.
4. Persistence. Add theme?: string to ReasonixConfig. Unknown names fall back to dark with a one-line warn on the toast rail.
5. User-facing surfaces.
- `/theme` — opens the existing `Select` picker, previews swatches, commits on Enter.
- `/theme ` — direct switch.
- `reasonix theme list` / `reasonix theme set ` — for users who want to script it or set a default before first launch.
6. Glyph parity. `GLYPH` stays theme-independent in v1. Themes only remap colors. Any glyph variation belongs in a separate compat axis (emoji-safe, ASCII-only) we already need for Windows CMD and CI logs.
Open questions
- Ship `solarized-dark` / `solarized-light` first-party, or wait for the user-theme file format and let the community PR them?
- Should the welcome banner gradient (`GRADIENT`) participate in theming, or stay brand-locked? Leaning brand-locked.
- Config key naming: `theme` vs `appearance` vs `colors`. Going with `theme` unless someone has a reason.
Rollout
- Token unification PR (no behavior change).
- Hook + context PR; migrate call sites in two or three batches.
- Add `light` + `high-contrast` themes + `/theme` slash + CLI subcommand.
- Document in README once the surface settles.
Background
Multiple users have asked for built-in theme switching. Right now we ship exactly one dark palette, hardcoded at module-eval time across two parallel token sets (
src/cli/ui/theme.tsandsrc/cli/ui/theme/tokens.ts). Users on light terminals, high-contrast setups, or with strong palette preferences have no way to opt out short of forking.Goals
dark(current default),light, andhigh-contrast.~/.reasonix/config.json./theme <name>and from the CLI viareasonix theme <name>/reasonix theme list.~/.reasonix/themes/) without committing to that surface in v1.Non-goals (v1)
Proposal
1. Unify the token surface. Collapse
theme.tsandtheme/tokens.tsinto one canonicalThemeshape (COLOR,SURFACE,FG,GRADIENT,CARD,GLYPH). The two-file split is a historical accident and blocks a single-source swap. This is a precondition, not a feature.2. Theme registry. Move the current constants into
themes/dark.ts, addthemes/light.tsandthemes/high-contrast.ts, and expose them throughthemes/index.tskeyed by name. The active theme is resolved once at startup from config and held in a small context.3. Runtime access pattern. Replace direct
import { COLOR } from \"./theme\"reads with auseTheme()hook (and a non-ReactgetTheme()for plain modules). Components re-render on swap via context. Migration is mechanical across the ~57 call sites; it can land incrementally behind a default-resolved theme so partial migrations don't break.4. Persistence. Add
theme?: stringtoReasonixConfig. Unknown names fall back todarkwith a one-line warn on the toast rail.5. User-facing surfaces.
6. Glyph parity. `GLYPH` stays theme-independent in v1. Themes only remap colors. Any glyph variation belongs in a separate compat axis (emoji-safe, ASCII-only) we already need for Windows CMD and CI logs.
Open questions
Rollout