Repository navigation
feat(desktop): assignable themes per profile - #42286
Merged
Merged
Conversation
The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage.
🔎 Lint report:
|
The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback).
Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC.
"default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
OutThisLife
enabled auto-merge (squash)
June 8, 2026 17:39
changman
pushed a commit
to changman/hermes-agent
that referenced
this pull request
Jun 10, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
davidgut1982
pushed a commit
to davidgut1982/hermes-agent
that referenced
this pull request
Jun 17, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
1 task
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
donbowman
pushed a commit
to donbowman/hermes-agent
that referenced
this pull request
Jul 13, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
11 of 18 tasks
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
fsca8
pushed a commit
to fsca8/hermes-agent
that referenced
this pull request
Oct 6, 2026
* feat(desktop): assignable themes per profile The desktop skin was a single global preference, so every profile shared one look. Make the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles paints that profile's own skin. A profile with no assignment inherits the global default, so single-profile installs and existing setups are unchanged. - themes/context.tsx: per-profile skin record in localStorage; ThemeProvider follows $activeGatewayProfile; boot paint uses the last active profile's theme to avoid a flash on a non-default relaunch; setTheme assigns to the live profile (default profile also seeds the legacy global fallback). - settings/appearance-settings.tsx: caption noting the theme is saved per profile, shown only when more than one profile exists. - i18n: themeProfileNote string across en/zh/zh-hant/ja. - themes/profile-theme.test.ts: resolution + inheritance coverage. * feat(desktop): make light/dark mode per profile too The command palette / theme picker sets skin + mode together on each pick, so leaving mode global meant a profile couldn't actually remember the full look it was given (e.g. "Ember Dark" in one profile would render Ember Light if another profile last flipped the global mode). Mirror the per-profile skin record for light/dark mode: ThemeProvider resolves and applies the active profile's mode on switch, the boot paint uses it, and setMode assigns to the live profile (default profile also seeds the legacy global mode fallback). * refactor(desktop): collapse per-profile skin/mode into one helper Skin and mode were near-identical resolve/assign pairs with hand-rolled try/catch around localStorage. Fold both into a single profilePref<T> factory (resolve + assign, default profile seeds the legacy global) and lean on storedString/persistString for the error-swallowing. Tests go table-driven over both prefs since they share one contract. No behavior change; -89 LOC. * refactor(desktop): treat default profile as the global slot directly "default" isn't a real profile — it is the legacy global value. Stop double-writing (record['default'] + global) on assign; route default straight to the global. resolve is unchanged: a profile with no record entry already falls back to the global, so default reads it for free.
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.
Summary
Requested: "pls to have assignable themes per profile 👀"
The desktop skin was a single global localStorage preference, so every profile shared one look. This makes the theme assignment per profile: picking a theme assigns it to the profile that's currently live, and switching profiles automatically paints that profile's own skin.
Backwards-compatible by construction — a profile with no assignment inherits the global default, so single-profile installs and existing setups look identical to before.
How it works
{ [profileKey]: skinName }record in localStorage (hermes-desktop-profile-themes-v1), alongside the existing per-profile color/order prefs.ThemeProvidersubscribes to$activeGatewayProfile; on a profile switch it repaints with that profile's assigned skin.setThemewrites to whichever profile is live. The default profile also seeds the legacy global skin (hermes-desktop-theme-v2), so unassigned profiles inherit a sensible fallback.UX
The existing Appearance → Theme picker now assigns to the current profile. A caption ("Saved for the profile — each profile keeps its own theme.") appears only when more than one profile exists, so single-profile users see no change.
/skinand the slash popover continue to work and are likewise per-profile.Changes
themes/context.tsx— per-profile resolution/assignment helpers; profile-awareThemeProvider+ boot paint.app/settings/appearance-settings.tsx— per-profile caption (multi-profile only).i18n/*—themeProfileNoteacross en / zh / zh-hant / ja (+ types).themes/profile-theme.test.ts— resolution, per-profile isolation, inheritance, and unknown-skin normalization.Validation
vitest run src/themes/profile-theme.test.ts— 4/4vitest run src/i18n/runtime.test.ts src/themes/presets.test.ts— 17/17tsc -b— cleaneslinton changed files — 0 errors/warnings