feat(desktop): approval-mode zap shows on the status bar by default

Whether dangerous commands run unasked is state worth seeing at a glance,
so the approval pill (yolo lightning) leaves STATUSBAR_HIDDEN_BY_DEFAULT.

Existing stores were seeded with it hidden, so the hidden-set key moves to
`hermes.desktop.statusbarHidden.v2`, seeded from v1 minus `approval-mode`:
other customizations survive, the zap appears once on update, and hiding
it again persists under the new key.
This commit is contained in:
Teknium
2026-09-06 02:11:59 -07:00
parent cca26097c5
commit 2bb9c4e693
3 changed files with 58 additions and 9 deletions
@@ -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 () => {
@@ -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')
})
})
+30 -8
View File
@@ -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<string[]>(
STATUSBAR_HIDDEN_STORAGE_KEY,
[...STATUSBAR_HIDDEN_BY_DEFAULT],
Codecs.json<string[]>(value =>
Array.isArray(value) ? value.filter((id): id is string => typeof id === 'string' && id.length > 0) : []
)
legacyHiddenSeed(),
Codecs.json<string[]>(sanitizeHiddenIds)
)
export function setStatusbarItemVisible(id: string, visible: boolean) {