Included
- Appearance and Font in Settings
- First frame, React hydration and system color-scheme changes
- Main window, desktop overlays and new windows
- Surfaces, text, borders, states, focus, composer and DAG
This answers three questions: where themes are selected and persisted, which surfaces a setting affects, and how theme additions or edits avoid updating only some components, windows or states.
dark, light .Appearance has four layers: Mode, named theme pack, accent color and CSS overlay. Font options come from font-pref.ts.
Schema 3 writes to agentic_theme_style + agentic_theme_mode; accent and overlay belong to the same preferences family. Fonts also maintain a cookie for the first frame.
The first-frame script and hook set data-theme, data-custom-css, accent tokens and --font-sans.
Every theme pack defines the same token contract; accent overrides only three accent tokens; CSS overlays apply on top of the current pack.
The Tailwind bridge, CSS modules and desktop overlays all read tokens without checking specific theme names.
Desktop startup and system color-scheme updates prefer the complete valid theme-prefs.json preferences saved by the main UI. Legacy browser-storage migration is used only when the file is missing or its mode/style is invalid. Raw browser-database byte scans cannot identify the latest record, must not override valid desktop preferences and must not restore cleared accent colors. After loading, the web page uses its own localStorage as authority and synchronizes the desktop cache.
The desktop main page records successful message-send times, first-screen initialization, settings changes, system color events, cross-window preference synchronization and HTML theme-attribute changes. Records contain only timestamps, fixed event names, theme enums and system light/dark state, excluding chat content, page URLs, CSS and user paths. Under desktop userData, theme-events.json retains at most 64 records; write failures do not affect theme rendering. The log identifies the source of intermittent switches and does not establish that unknown triggers are fixed.
Theme files provide values; components define usage. The same purpose uses the same token name across every theme.
--bg-primary / secondary / tertiary--bg-input / hover / selected--surface-popover / tooltip--text-bright / primary--text-secondary / muted--nav-color / hover--border / border-light--border-popover--shadow-* / --scrim-*--theme-accent--theme-accent-fill / fill-hover--accent-green / red / yellow--success-soft / warning-soft / danger-soft--selection-bg / --focus-ring--chip-bg / --chip-ring--meter-fill / --meter-track--composer-surface / shadow / filter--provider-icon-bg--dag-ghost| Setting | Direct state | Affected scope | Prohibited |
|---|---|---|---|
| Appearance | data-theme | Backgrounds, text, borders, shadows, state colors, tab focus, composer, popover, DAG and desktop overlays | Writing component-level [data-theme="light"] |
| Accent | --accent-orange / fill / hover | Primary-action color, selection borders and link colors on desktop error pages and directory listings | Adding a fourth accent token name |
| Font | --font-sans | body, button, input, select, textarea and optgroup, except explicit mono regions | Redeclaring system font stacks in ordinary components |
| CSS overlay | #user-custom-css + data-custom-css | Optional snippet over the current theme pack; unspecified values still come from that pack | Turning overlay into a fourth skin card |
| Theme families | Theme | Primary color | Scope |
|---|---|---|---|
| Beige | beige-dark / beige-light | Orange / coral | Same component roles; only theme values change |
| Neutral | dark / light | Blue | Default Button, primary-action fills, selected states, links and accent borders |
| Aurora | aurora / aurora-light | Teal | Same component roles, with dark foreground for filled-element contrast |
| Option | Adopted | Rationale and boundaries | Missing capability |
|---|---|---|---|
CSS custom properties + data-theme | Adopted | Native cascading covers Web, CSS modules, Tailwind arbitrary values and same-origin desktop overlays. | CSS alone cannot validate missing theme tokens, so add a static contract check. |
Tailwind @theme | As a bridge | Expose CSS tokens as utility names without storing another set of color values. | Cannot dynamically generate CSS imports; a check script aligns theme files with TS ids. |
| JavaScript theme object / Context | Rejected | Duplicates color values and cannot naturally cover native CSS, the first frame or Electron overlays. | No runtime per-component theme props. |
Component-level [data-theme] branches | Rejected | New themes would inevitably miss edits; theme files should provide component-role tokens instead. | Components must not know built-in theme ids. |
light-dark() Only color-scheme | Rejected | Expresses only light and dark, not the three Beige, Neutral and Aurora packs. | Keep explicit values for each theme. |
Specification basis:CSS Custom Properties Level 1, CSS Color Adjustment color-scheme. Existing implementation entry points: apps/web/lib/prefs/theme-pref.ts, apps/web/app/globals.css and apps/web/app/styles/themes/.
custom-light.css is only a legacy-snippet fallback with values matching beige-light.--accent-orange / --accent-fill / --accent-orange-hover; an empty value uses the current pack default. Desktop error-page and directory-listing link colors follow the override, while the window background remains the pack's --bg-primary.[data-theme=custom] selectors are rewritten to data-custom-css.--bg-primary.--font-sans, with explicit exceptions for mono regions.--tab-active-bg blends the content background with 12% white, shared by ordinary tabs and split groups. The selected background remains stable on hover; light mode and browser tabs retain the content background.Text-entry controls and native selects use one existing border in the theme accent color on focus. Focus adds no outline, offset ring or box-shadow frame and does not change control dimensions. Search and composer wrappers own their boundary; their inner editor stays borderless. Navigation, checkboxes, radios, sliders and embedded third-party document surfaces retain their own keyboard-focus treatments.
The current implementation uses Obsidian-style Appearance: Mode × named theme pack × accent color × CSS overlay.
Schema 3's THEME_STYLE_PAIRS remains the light/dark matrix of Beige / Neutral / Aurora. Custom is no longer a fourth style card; existing custom / custom-light migrates to beige, saved CSS enables the overlay, and old selectors are rewritten to data-custom-css. Accent color persists independently and overrides only three accent tokens. Composer, provider icons and DAG consume role tokens only. Desktop BrowserWindow backgrounds still use the pack's --bg-primary; error-page and directory-listing links use the accent override when present. Theme-contract checks are included in the complete Web checks.
Windows tests use Node.js fileURLToPath to convert module URLs into local paths, avoiding an extra slash before drive paths; theme behavior and assertions remain unchanged.