Repository navigation
feat(web): let users add their own themes from ~/.thinkrail/themes - #937
Merged
Merged
Conversation
Rinat S (rsolmano)
requested review from
Olga Lavrichenko (OLavrik),
Rustam Sadykov (SBOne-Kenobi) and
danyaberezun
as code owners
October 26, 2026 22:11
Collaborator
|
Fix before merge – mostly spec-only, but this PR turns the manifest into a format users keep on disk, so these are cheapest to settle now.
|
Rinat S (rsolmano)
force-pushed
the
user-custom-themes
branch
from
October 27, 2026 10:28
246dfd9 to
aaf11ea
Compare
Collaborator
Author
|
Thanks danyaberezun — all five addressed; rebased onto current
Verification on the final tree: server |
Rinat S (rsolmano)
force-pushed
the
user-custom-themes
branch
from
October 27, 2026 12:05
aaf11ea to
24bec36
Compare
4 of 5 tasks
6 of 10 tasks
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.
Problem
Themes could only be added through a source PR plus a rebuild:
apps/web/src/themes/SPEC.mdlisted "users cannot add themes" as a deliberate non-goal. Users who want their own palette had no path at all, even though a theme is already one self-contained, declarative JSON manifest.Approach
User themes mirror how local language files already work (
~/.thinkrail/locales):~/.thinkrail/themes/<name>.theme.jsonin the same format as the bundled manifests (schema v2, strict, complete — copydark.theme.json, changeid,labeland colours). It appears in Settings → Appearance without a restart; editing the active file re-themes live.packages/server/src/themesonly scans, size-caps, parses and watches the folder (once-a-second signature poll, same as locales) and returns the raw JSON.apps/web/src/themesvalidates each file with the existingparseThemeManifest, so the schema keeps one owner. Rejected: moving the validator into a shared package — more churn for no user-visible gain.user.id namespace (a file with"id": "ocean"is catalog/config iduser.ocean; bundled ids may never use the prefix), so a theme we bundle later can never capture a user's saved choice and a copieddark.theme.jsonis simplyuser.dark. Two user files with one id →duplicate-id. Fallbacks and derived system-pair slots are picked from bundled themes only. The AA/AAA contrast gates stay merge gates for our own manifests only.initializeThemesinstalls them before React mounts. The bound matters on desktop, where that storage is Electrobun's native preference store (256 KB per value, 1 MB per document across windows): caching every user theme could have starved other desktop preferences.Changes
USER_THEMES_PROTOCOL_VERSION):themes.list→{ dir, files }(each{ fileName, ok: true, manifest: unknown } | { fileName, ok: false, error, detail? }),themes.reveal, and thethemes.changedinvalidation channel. Older hosts simply don't show the new settings group.packages/server/src/themes: 256 KB/file, 64 files, a string-aware nesting-depth limit (32) so a pathological file can't overflow serialization of the wholethemes.listresponse. Its small folder poller copies the locales one rather than adding a sibling edge.apps/web/src/themes:installUserThemes/clearUserThemes/getUserThemeIssues/onThemesChange;initializeBundledThemes→initializeThemes; own-property lookups so ids likeconstructorresolve normally.userThemessnapshot refreshed on welcome andthemes.changed; Shell installs it before re-applying the preference (re-applying republishesdata-theme, so Monaco/xterm rebuild even when the active id is unchanged);supportsUserThemesgates it.dark.theme.json, the folder path with Copy path / Open folder, and one warning per broken file. 10 newsettings.appearance.custom.*keys (English only; untranslated strings fall back to English).themes/SPEC.md(non-goal replaced by narrower ones), newserver/src/themes/SPEC.md, web/panels/transport/contracts/server specs, README "Your themes" bullet.themes/SPEC.md): the parser accepts every schema version released for user files (v2 is the first) and migrates older ones in code; a later key must be derivable from existing ones. Forward compatibility is not promised.themes/SPEC.md): partial overlays, external-format import (shadcn/tweakcn, VS Code), extension-provided themes, an in-app editor, contrast warnings.Screenshots
Settings → Appearance, same viewport, with one valid user theme (
ocean-dark.theme.json) and one broken file (broken.theme.json) in the themes folder.main)Checklist
bun run lint,bun run typecheck,bun run testbun run e2e, orbun run e2e:fullwhen touching agent behavior)SPEC.md/ top-level specs updated to reflect any boundary, contract, or behavior changeTesting
bun run e2e(rebased head) → all 8 shards passed, including the newe2e/user-themes.spec.ts(dropped file listed and applied, live re-theme on edit, survives reload, duplicate-id notice, fallback and recovery after deletion).bun run e2e:desktop(pre-rebase) → all theme/language specs passed; 24 unrelated failures (terminals ×20, workspace-tabs ×2, layout, model-picker) that pass 61/61 on rerun both on this branch and on its base.bun run smoke:desktop(pre-rebase) → smoke OK.packages/serverbun test→ 1839 pass / 0 fail; all other packages via turbo → 21/21.bun run typecheck,biome check .,check:deps/boundaries/seams/spec-surface/i18n/compiler→ green (pre-commit).bun testserver → 1887 pass; other packages → 21/21;bun run e2e→ all 8 shards passed;bun run typecheck→ 22/22 after the final rebase.