diff --git a/apps/desktop/src/app/shell/statusbar-visibility.test.tsx b/apps/desktop/src/app/shell/statusbar-visibility.test.tsx index 1a078ba38b..2010ed1cdf 100644 --- a/apps/desktop/src/app/shell/statusbar-visibility.test.tsx +++ b/apps/desktop/src/app/shell/statusbar-visibility.test.tsx @@ -57,11 +57,12 @@ describe('statusbar item visibility', () => { item('gateway-health', 'Gateway') ]) - for (const label of ['Cron', 'Webhooks', 'Agents', 'Terminal', 'Approvals']) { + for (const label of ['Cron', 'Webhooks', 'Agents', 'Terminal']) { expect(screen.queryByText(label)).toBeNull() } expect(screen.getByText('Gateway')).toBeTruthy() + expect(screen.getByText('Approvals')).toBeTruthy() }) it('shows an item once the user enables it from the bar context menu', async () => { diff --git a/apps/desktop/src/store/statusbar-prefs.test.ts b/apps/desktop/src/store/statusbar-prefs.test.ts index 2d41618cd3..a8f67417d9 100644 --- a/apps/desktop/src/store/statusbar-prefs.test.ts +++ b/apps/desktop/src/store/statusbar-prefs.test.ts @@ -30,3 +30,29 @@ describe('statusbar whole-bar visibility', () => { expect(reloaded.$statusbarVisible.get()).toBe(false) }) }) + +describe('statusbar hidden items', () => { + beforeEach(() => { + window.localStorage.clear() + vi.resetModules() + }) + + it('surfaces the approval pill for installs that hid it under the v1 defaults, keeping their other choices', async () => { + window.localStorage.setItem('hermes.desktop.statusbarHidden', JSON.stringify(['approval-mode', 'cron', 'gateway-health'])) + + const { $statusbarHiddenIds } = await loadStore() + + expect($statusbarHiddenIds.get()).toEqual(['cron', 'gateway-health']) + }) + + it('still honours hiding the approval pill after the update', async () => { + const first = await loadStore() + + first.setStatusbarItemVisible('approval-mode', false) + + vi.resetModules() + const reloaded = await loadStore() + + expect(reloaded.$statusbarHiddenIds.get()).toContain('approval-mode') + }) +}) diff --git a/apps/desktop/src/store/statusbar-prefs.ts b/apps/desktop/src/store/statusbar-prefs.ts index 16fc197353..c1857368c4 100644 --- a/apps/desktop/src/store/statusbar-prefs.ts +++ b/apps/desktop/src/store/statusbar-prefs.ts @@ -1,6 +1,12 @@ import { Codecs, persistentAtom } from '@/lib/persisted' +import { readKey } from '@/lib/storage' -const STATUSBAR_HIDDEN_STORAGE_KEY = 'hermes.desktop.statusbarHidden' +// v1 (`hermes.desktop.statusbarHidden`) was seeded with the approval pill +// hidden, so every existing store carries an `approval-mode` the user never +// chose. v2 seeds from v1 minus that id: other customizations survive, the +// pill appears once on update, and hiding it again persists here. +const STATUSBAR_HIDDEN_STORAGE_KEY = 'hermes.desktop.statusbarHidden.v2' +const LEGACY_HIDDEN_STORAGE_KEY = 'hermes.desktop.statusbarHidden' // v1 (`hermes.desktop.statusbarVisible`) shipped a stretch where the bar was // opt-in, so many stores hold a `false` the user never chose. v2 is read fresh // and the v1 key is deliberately NOT seeded from: every existing install comes @@ -18,14 +24,15 @@ export function toggleStatusbarVisible() { // Items the bar hides until the user turns them on from its context menu. The // bar's job is to answer "is the backend healthy, where am I, what's it doing" — -// route shortcuts (cron/webhooks/agents), the terminal toggle, and the approval -// pill are navigation, not status, so they start out of the way. The per-turn +// route shortcuts (cron/webhooks/agents) and the terminal toggle are +// navigation, not status, so they start out of the way. The approval pill +// (the yolo zap) stays: whether dangerous commands run unasked is state the +// user should see at a glance. The per-turn // session readouts (running/session timers, context meter, cache hit rate, // tokens/sec) are diagnostics most users don't watch, so they start hidden too // and the bar stays quiet mid-turn. export const STATUSBAR_HIDDEN_BY_DEFAULT: readonly string[] = [ 'agents', - 'approval-mode', 'cache-hit-rate', 'context-usage', 'cron', @@ -42,12 +49,27 @@ export const STATUSBAR_HIDDEN_BY_DEFAULT: readonly string[] = [ // staying off. An empty array is a real value — the user turned everything on — // so this uses a sanitizing json codec rather than Codecs.stringArray, which // drops the key when empty and would resurrect the defaults on next launch. +const sanitizeHiddenIds = (value: unknown): string[] => + Array.isArray(value) ? value.filter((id): id is string => typeof id === 'string' && id.length > 0) : [] + +function legacyHiddenSeed(): string[] { + const raw = readKey(LEGACY_HIDDEN_STORAGE_KEY) + + if (raw === null) { + return [...STATUSBAR_HIDDEN_BY_DEFAULT] + } + + try { + return sanitizeHiddenIds(JSON.parse(raw)).filter(id => id !== 'approval-mode') + } catch { + return [...STATUSBAR_HIDDEN_BY_DEFAULT] + } +} + export const $statusbarHiddenIds = persistentAtom( STATUSBAR_HIDDEN_STORAGE_KEY, - [...STATUSBAR_HIDDEN_BY_DEFAULT], - Codecs.json(value => - Array.isArray(value) ? value.filter((id): id is string => typeof id === 'string' && id.length > 0) : [] - ) + legacyHiddenSeed(), + Codecs.json(sanitizeHiddenIds) ) export function setStatusbarItemVisible(id: string, visible: boolean) {