Skip to content

feat(desktop): let the theme be shared across profiles and gateways - #96271

Open
Zeus-Deus wants to merge 5 commits into
NousResearch:mainfrom
Zeus-Deus:feat/desktop-theme-scope
Open

Zeus-Deus wants to merge 5 commits into
NousResearch:mainfrom
Zeus-Deus:feat/desktop-theme-scope

Conversation

@Zeus-Deus

Copy link
Copy Markdown

What does this PR do?

Adds a Theme Scope setting to Desktop → Settings → Appearance so the theme and light/dark mode can be shared across every profile and gateway instead of saved per profile.

Since #42286 the desktop skin and mode are assigned per profile, and switching profiles (or gateways) repaints the window in that profile's look. That's a good default for people who want each bot to feel distinct — but on one desktop used for several bots there is currently no way to say "one theme, everywhere": you have to re-pick the same theme in every profile, and it drifts again the moment one of them changes.

The new row sits directly under the theme grid and only renders when the device has more than one profile (single-profile installs see nothing new):

Per profile (default — today's behaviour, unchanged) Each profile and gateway keeps its own theme and light/dark mode.
Shared Every profile reads and writes the one global slot — the same slot the default profile already uses — so a pick on any profile paints all of them, on every gateway.

Behaviour details:

  • Flipping to Shared promotes the look you're currently on to the shared one (read through the live profile before the flip, written to the global slot after), so the window never changes under you.
  • Flipping back to Per profile finds every profile's earlier assignment intact: Shared only ever writes the global slot, never the per-profile record.
  • The boot-time paint and the cross-window storage sync (HUD, quick entry) go through the same resolver, so the choice holds across relaunch and across windows with no extra wiring.
  • The caption under the theme grid flips with the scope, and the row is a Cmd-K settings-search target (theme, scope, shared, sync, gateway).

Every other Appearance setting (UI scale, fonts, density, tab strip, backdrop, translucency, …) is already global — only theme and mode diverged — so the row is scoped to those two and its description says so.

Screenshots

Per profile (default):

Theme Scope — per profile

Shared:

Theme Scope — shared

Full Appearance page (Shared)

Appearance page with Theme Scope

Related Issue

No existing issue. Builds on #42286 (per-profile themes). Related discussion: #50190 (desktop ↔ dashboard skin sync), #91107 (accent override per profile).

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • apps/desktop/src/themes/context.tsx — $themeScope atom persisted at hermes-desktop-theme-scope-v1; skinPref / modePref route through slotFor() (under Shared every profile maps to the global slot); setThemeScope() performs the promote; scope key added to the cross-window storage sync.
  • apps/desktop/src/app/settings/appearance-settings.tsx — the Theme Scope row (segmented control) and the scope-aware caption under the theme grid.
  • apps/desktop/src/app/settings/settings-search.ts, use-settings-search.ts — stable setting id appearance.theme-scope and a search catalog entry.
  • apps/desktop/src/i18n/{types,en,zh,zh-hant,ja,ar}.ts — six new strings.
  • apps/desktop/src/themes/profile-theme.test.ts — promotion on flip, routing under Shared, restore on flipping back, no-op on same scope, persistence and junk-value normalisation (table-driven over skin and mode).
  • apps/desktop/e2e/theme-scope.spec.ts — real Electron + real gateway with three profiles, switching through the statusbar rail: per-profile isolation, promote-without-repaint, shared painting across profiles, restore, light/dark following the scope, and survival across a relaunch.

How to Test

  1. Create at least two profiles (hermes profile create work), open Desktop → Settings → Appearance. A Theme Scope row appears under the theme grid, on Per profile.
  2. Pick a theme on default, switch to work in the statusbar rail, pick a different theme — switch back and forth: each profile keeps its own (unchanged behaviour).
  3. On work, click Shared — the window does not change. Switch to default: it now shows work's theme. Pick another theme or toggle Dark anywhere; every profile follows.
  4. Click Per profile again — work gets its earlier theme back.
  5. Relaunch: the scope and the shared look persist.

Automated:

cd apps/desktop
npx vitest run                                   # 7887 passed
npx playwright test e2e/theme-scope.spec.ts      # 7 passed (needs npm run build + repo .venv)
npx tsc --noEmit -p tsconfig.json && npx tsc --noEmit -p tsconfig.e2e.json

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — N/A, desktop-only change (no Python touched); desktop unit + e2e suites run instead
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Linux (Arch)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A (renderer-only, localStorage)

Skin and light/dark mode are saved per profile (NousResearch#42286), so on a device
with several profiles or gateways every switch repaints the window in that
profile's look. That is the right default for people who want each bot to
feel distinct, but someone using one desktop for all their bots has no way
to say "one theme, everywhere" — they have to re-pick the same theme in
every profile and it drifts again the moment one is changed.

Add a Theme Scope setting under Appearance → Theme, shown only when the
device has more than one profile:

- Per profile (default, unchanged): each profile keeps its own skin + mode.
- Shared: every profile reads and writes the one global slot — the same
  slot the default profile already uses — so a pick on any profile paints
  all of them, including on other gateways.

Flipping to Shared promotes the look you are on to the shared one (read
through the live profile before the flip, written to the global slot
after), so the window never changes under you. Flipping back finds every
profile's earlier assignment intact: Shared only ever writes the global
slot, never the per-profile record.

- themes/context.tsx: `$themeScope` atom persisted at
  `hermes-desktop-theme-scope-v1`; `skinPref`/`modePref` route through
  `slotFor()`, so the boot paint and the cross-window `storage` sync pick
  the scope up without further wiring; `setThemeScope()` does the promote.
- settings/appearance-settings.tsx: the scope row (segmented control) and a
  caption under the theme grid that flips with it.
- settings search: the row is a Cmd-K target (theme, scope, shared, sync…).
- i18n: en, zh, zh-hant, ja, ar.
- themes/profile-theme.test.ts: promotion, routing under Shared, restore on
  flipping back, no-op on same scope, persistence + junk normalisation.
- e2e/theme-scope.spec.ts: real Electron + gateway with three profiles,
  driving switches through the statusbar rail — isolation, promotion,
  shared painting, restore, mode, and survival across a relaunch.

Every other Appearance setting (scale, fonts, density, backdrop, …) is
already global; only theme and mode diverged, so the row is scoped to them
and says so.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) area/profiles Multi-profile isolation, HERMES_HOME scoping sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 27, 2026
# Conflicts:
#	apps/desktop/src/themes/context.tsx
Preserve shared theme routing alongside named-profile fallback seeding and legacy migration. Update scope regressions and regenerate the desktop locale key catalog.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants