feat: theme support — Light, IntelliJ Darcula + Gruvbox, server-synced in Settings#84
Merged
Merged
Conversation
…ings Add user-selectable UI themes, chosen in Settings → Appearance and persisted host-side so the choice follows the user across devices. Themes: Dark (existing default, unchanged) + a new Light theme + the classic IntelliJ Darcula (authentic surfaces #3c3f41/#2b2b2b, #a9b7c6 text, blue selection; ThinkRail violet kept as the interactive accent). Settings persistence (new): - contracts: Theme const-object enum + ThemeId/THEME_IDS, AppConfig, DEFAULT_CONFIG (plain-string values, no wire-boundary casts) - persistence: loadConfig/saveConfig over config.json (~/.thinkrail) - new `settings` host module: getConfig/updateConfig + broadcast seam - wire (PROTOCOL_VERSION 6→7): server.welcome carries `config`, `settings.update` persists, `settings.changed` broadcasts so clients converge Theme mechanism (web): - tokens.css [data-theme="light"|"darcula"] blocks override only semantic surface/text/status tokens + color-scheme; @theme inline re-themes all utils - utils/theme.applyTheme + localStorage first-paint hint (no flash; host stays the source of truth), store.theme fed by transport, Shell owns the DOM effect - Appearance settings section with the theme picker Code surfaces track the swap: Monaco picks vs/vs-dark + observes data-theme; shiki renders dual (dark+light) palettes flipped by CSS; xterm + mermaid already followed the tokens. Tests: settings module + store applyConfig unit tests; e2e/theme.spec.ts (switch + persist across reload). SPECs updated across contracts, persistence, settings, server, module-web, store, transport, panels, shell.
… terminal ANSI + Darcula syntax - lib.cssColorToHex: canonicalize token reads to hex (minified CSS serves #fff / gray equivalents; Monaco and xterm reject them). Fixes the Light-theme editor crash (defineTheme threw, opening any file hit the ErrorBoundary). - Monaco: syntax rules from per-theme --code-* tokens (Darcula's cc7832/6a8759 palette), define degrades instead of throwing. - Terminal: 16 --ansi-* tokens (Dark+ defaults, Light+ overrides) so shell colors stay legible on light surfaces. - shiki: tri-palette render (custom darcula registration in lib/shikiTheme), flipped by [data-theme] CSS — no re-highlighting. - Contrast: Darcula muted/hint lifted (3.0→4.5:1 on the 3c3f41 sidebar); Light hint + status colors darkened to ≥4.5:1 as text on white. - e2e: theme spec now opens a real file under each theme against the built (minified) CSS and asserts Monaco re-themes; SPECs updated.
The geek theme: morhetz's retro-groove palette as a fourth theme. Warm hard-contrast darks (#1d2021 editor inside #282828 tool windows), amber #ebdbb2 text, and — uniquely — the signature gruvbox orange as the interactive accent with dark-on-accent text (6.5:1). Everything rides the existing per-theme token mechanism: - tokens.css [data-theme="gruvbox"]: surfaces/text/status + accent family, the canonical gruvbox 16-color ANSI palette, and gruvbox vim syntax via --code-* (red keywords, green strings, gray comments) - shiki: gruvbox-dark-hard (shiki-shipped) joins the palette map; pure-CSS flip like the others - contracts: Theme.Gruvbox (additive, wire-compatible — unknown ids fall back to Dark's :root tokens) - e2e: the Monaco re-theme cycle now covers gruvbox All token pairs validated ≥4.5:1 on their resting surfaces (muted 5.3, statuses 4.8–9.7).
Rinat S (rsolmano)
requested review from
Olga Lavrichenko (OLavrik) and
danyaberezun
July 17, 2026 14:09
Olga Lavrichenko (OLavrik)
approved these changes
Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds user-selectable UI themes, chosen in Settings → Appearance and persisted host-side so the choice follows the user across devices/browsers dialing the same host.
#3c3f41tool windows around a#2b2b2beditor,#a9b7c6foreground, blue selection, authentic syntax colors in Monaco + shiki) with ThinkRail's violet kept as the interactive accent#1d2021editor inside#282828tool windows, amber#ebdbb2text, the canonical gruvbox 16-color terminal palette, gruvbox vim syntax colors — and, uniquely, the signature gruvbox orange as the interactive accent with dark-on-accent text (violet fights this palette)Screenshots
How
Settings persistence (new) — a
config.jsonunder~/.thinkrail, following theprojects.json/workspaces.jsonpattern:contracts:Themeconst-object enum +ThemeId/THEME_IDS,AppConfig,DEFAULT_CONFIG(plain-string values → cross JSON/the wire with no casts; adding a theme id is wire-compatible — an older client falls back to Dark's:roottokens)persistence:loadConfig/saveConfigsettingshost module:getConfig/updateConfig+ a publisher seamserver.welcomecarriesconfig,settings.updatepersists a partial,settings.changedbroadcasts the merged config so every client converges (same no-per-client-optimism pattern as the workspace lifecycle)Theme mechanism (web) — the
[data-theme]swap the spec already described:tokens.cssgains per-theme blocks overriding the semantic surface/text/status tokens +color-scheme(plus, where a theme calls for it, the accent family);@theme inlinere-themes every utility for free--ansi-*(the 16 xterm colors — Light and Gruvbox ship their own palettes; dark-tuned brights wash out on white) and optional--code-*syntax colors (Darcula + Gruvbox set them; other themes get the Monaco base palette)utils/theme.applyTheme+ a localStorage first-paint hint (avoids a flash beforeserver.welcome; the host stays the source of truth),store.themefed by transport, and the Shell owns the single DOM effectCode surfaces track the swap: Monaco picks
vs/vs-dark, observesdata-theme, and rebuilds chrome + syntax rules from the tokens; shiki renders all palettes in one pass (dark/light/darcula/gruvbox — darcula is a custom registration inlib/shikiTheme.ts, gruvbox is shiki-shipped) flipped by pure-CSS[data-theme]rules, no re-highlighting; xterm rebuilds its theme incl. the ANSI palette on the same observer; mermaid already followed the tokens.Hardening (found by a UX/theme review of the first cut)
#ffffff→#fff,#808080→gray), which Monaco/xterm reject — under Light this crashed the editor panel on any file open. All JS token reads now go throughlib.cssColorToHex(canvas-canonicalized; unparseable → treated as unset), and Monaco's define degrades to the base palette instead of throwing. Regression-covered by e2e against the built bundle.tokens.css): body ≥ 4.5:1,--muted≥ 4.5:1 on its worst resting surface,--hint≥ 3:1. Darcula's muted/hint were lifted (3.0 → 4.5:1 on the#3c3f41sidebar); Light's hint + status colors darkened to ≥ 4.5:1 as text on white (they double as diff badges / "M" markers / links).Design note
The localStorage paint-hint is a render cache only — the host's
config.jsonremains the source of truth for the server-synced setting.Tests / verification
typecheck✅ ·lint✅ ·check:deps✅ (husky pre-commit)settingsmodule tests, storeapplyConfig,cssColorToHex(full suite green)e2e/theme.spec.ts: switch + persist across reload, and open a real file under every theme against the built (minified) CSS, asserting Monaco actually re-themes (Light#ffffff→ Darcula#2b2b2b→ Gruvbox#1d2021→ Dark) with no editor ErrorBoundary#cc7832keywords / Gruvbox#fb4934keywords / shiki--shiki-*flips)SPECs
Updated across
contracts,persistence, newsettings/SPEC.md,server,module-web(styling & theming incl. the token vocabulary + contrast floors),store,transport,panels(code-surface re-theming),lib,shell— spec-graph validates clean.