fix(desktop): persist custom backend skins across restarts - #71447
fix(desktop): persist custom backend skins across restarts#71447asimons81 wants to merge 4 commits into
Conversation
12cf9d7 to
5124835
Compare
|
This also affects runtime Desktop SDK theme contributions, and the current implementation still misses per-profile selections.
A smaller generic split worked in testing: preserve any non-retired stored skin name in Focused evidence on current |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the startup normalization path. The reported restart failure is present on current main: apps/desktop/src/themes/context.tsx:47-48 rejects a skin before it is registered, and gateway.ready deliberately calls ingestBackendSkin(..., { apply: false }) at apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts:285-288.
Problems
apps/desktop/src/themes/known-skins.ts:35seeds the known set from the legacy stored value itself. A corrupt non-retired global value therefore passes the newnormalizeSkin()known-name check instead of falling back, contrary to the stated junk-value behavior.- The cache is populated only by
backend-sync.ts, but named profiles persist viacontext.tsx:58-69and SDK themes resolve from the contribution registry atuser-themes.ts:159-178. The documented profile/runtime-plugin gap remains. - No regression tests are added;
profile-theme.test.ts:44-47covers only a named-profile junk value.
Suggested changes
- Separate lenient persisted-value recovery from strict live validation for
setTheme()and apply that recovery throughprofilePref. - Add global, named-profile, and late SDK-registration restart coverage.
This is an automated hermes-sweeper review.
|
Hi maintainers — this fixes #71446: custom backend skins in |
|
Hi @asimons81 — I independently hit this exact bug (backend skins resetting to Your File: import { beforeEach, describe, expect, it } from 'vitest'
import { knownSkinNames, rememberSkinName } from './known-skins'
import { skinPref } from './context'
import { DEFAULT_SKIN_NAME } from './presets'
// The bug: a backend skin (user YAML in ~/.hermes/skins/) is registered into
// the live registry only AFTER the gateway connects. But the desktop's
// boot-time paint reads the persisted skin BEFORE that, so normalizeSkin
// would reject the name and reset every launch to the default. known-skins
// remembers which names resolved last session so they survive the boot
// window. These tests pin that contract.
const KNOWN_KEY = 'hermes-desktop-known-skin-names-v1'
const SKIN_KEY = 'hermes-desktop-theme-v2'
beforeEach(() => {
window.localStorage.clear()
})
describe('knownSkinNames', () => {
it('returns an empty set when nothing was ever remembered', () => {
expect(knownSkinNames().size).toBe(0)
})
it('reads back names that were remembered', () => {
rememberSkinName('trt')
rememberSkinName('ares')
expect(knownSkinNames().has('trt')).toBe(true)
expect(knownSkinNames().has('ares')).toBe(true)
})
it('auto-seeds the currently persisted legacy skin so the first launch after the fix survives', () => {
window.localStorage.setItem(SKIN_KEY, 'ember')
expect(knownSkinNames().has('ember')).toBe(true)
})
it('does not auto-seed retired/empty names', () => {
window.localStorage.setItem(SKIN_KEY, 'default')
expect(knownSkinNames().size).toBe(0)
window.localStorage.setItem(SKIN_KEY, '')
expect(knownSkinNames().size).toBe(0)
})
it('ignores corrupt storage gracefully', () => {
window.localStorage.setItem(KNOWN_KEY, '{not-json')
expect(knownSkinNames().size).toBe(0)
})
})
describe('rememberSkinName', () => {
it('persists across reads', () => {
rememberSkinName('ares')
// Fresh read, same storage — the name must still be there.
expect(knownSkinNames().has('ares')).toBe(true)
})
it('is idempotent', () => {
rememberSkinName('trt')
rememberSkinName('trt')
const names = knownSkinNames()
expect(names.size).toBe(1)
expect(names.has('trt')).toBe(true)
})
})
describe('skinPref boot-window persistence (the known-skins fix)', () => {
it('keeps a remembered backend skin even before the registry resolves it', () => {
// The user's persisted skin from last session, remembered in the known
// set. The live registry is empty (gateway not connected yet) — but the
// name was valid last session, so it must survive, not fall back.
rememberSkinName('trt')
window.localStorage.setItem(SKIN_KEY, 'trt')
expect(skinPref.resolve('default')).toBe('trt')
})
it('still rejects names that were never valid', () => {
window.localStorage.setItem(SKIN_KEY, 'nope')
expect(skinPref.resolve('default')).toBe(DEFAULT_SKIN_NAME)
})
it('still rejects retired names even if remembered', () => {
rememberSkinName('gold')
window.localStorage.setItem(SKIN_KEY, 'gold')
expect(skinPref.resolve('default')).toBe(DEFAULT_SKIN_NAME)
})
})One edge the tests surfaced — the auto-seed in const active = storedString(SKIN_KEY)
if (active && !RETIRED_SKINS.has(active) && !names.includes(active)) {
names.push(active)
}This trusts any non-retired value in the legacy skin key, even if it never resolved to a real skin. Test Happy to adjust the tests if you'd prefer a different structure, or open a PR against your branch if that's easier. Thanks for the fix — this was biting me daily. |
5124835 to
c9ce66e
Compare
|
Reworked this rather than bumping it again. The self-seeding |
What does this PR do?
Fixes #71446: custom backend/runtime skins can survive a Desktop restart even when their registry is populated only after the first paint.
Root cause
Desktop used the same strict
normalizeSkin()path for both:That caused a valid persisted custom skin to be rewritten to
nousduring the boot window.The original version of this PR tried to bridge that window with a
known-skinscache. Review correctly identified two problems with that approach: the cache auto-seeded validity from the value being validated, and it did not cover named-profile/runtime-plugin persistence.Fix
normalizeSkin()strict for livesetTheme()calls.skinPref, so it covers the legacy global slot and named profiles without maintaining a second cache of skin names.This removes the
known-skinscache entirely and covers backend YAML skins plus runtime Desktop SDK themes with one late-binding contract.Regression coverage
Added coverage for:
Manual test
display.skin: trtand create~/.hermes/skins/trt.yaml.trt, fully quit Desktop, then relaunch.trtautomatically without requiring/skin trtagain.Related
Fixes #71446
Thanks to @ildunari for identifying the generic persisted-vs-live split and the named-profile/runtime-plugin gap, and to @teknium1 for catching the self-seeding validation flaw.