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:
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user