diff --git a/apps/desktop/src/app/shell/statusbar-controls.tsx b/apps/desktop/src/app/shell/statusbar-controls.tsx
index 7718682c74..a245d1dcd2 100644
--- a/apps/desktop/src/app/shell/statusbar-controls.tsx
+++ b/apps/desktop/src/app/shell/statusbar-controls.tsx
@@ -17,7 +17,13 @@ import { ContribRender } from '@/contrib/react/boundary'
import { useI18n } from '@/i18n'
import { useKeybindHint } from '@/lib/keybinds/use-keybind-hint'
import { cn } from '@/lib/utils'
-import { $statusbarHiddenIds, setStatusbarItemVisible, toggleStatusbarVisible } from '@/store/statusbar-prefs'
+import {
+ $statusbarHiddenIds,
+ isStatusbarLayoutDefault,
+ resetStatusbarLayout,
+ setStatusbarItemVisible,
+ toggleStatusbarVisible
+} from '@/store/statusbar-prefs'
// Shared chrome styling for interactive statusbar items (button / link / menu
// trigger). The 'text' variant intentionally omits hover/transition/disabled.
@@ -176,6 +182,18 @@ function StatusbarVisibilityMenu({
))}
+ {/* Disabled rather than hidden when nothing is customized: the row is
+ also how you find out there IS a shipped layout to get back to.
+ Groups with the hide row below — both act on the bar, not an item. */}
+ {
+ event.preventDefault()
+ resetStatusbarLayout()
+ }}
+ >
+ {copy.resetStatusbar}
+
>
)}
diff --git a/apps/desktop/src/app/shell/statusbar-visibility.test.tsx b/apps/desktop/src/app/shell/statusbar-visibility.test.tsx
index b4bce684bb..e65f272e05 100644
--- a/apps/desktop/src/app/shell/statusbar-visibility.test.tsx
+++ b/apps/desktop/src/app/shell/statusbar-visibility.test.tsx
@@ -125,6 +125,68 @@ describe('statusbar item visibility', () => {
})
})
+describe('reset to defaults', () => {
+ it('puts a customized bar back to the shipped show/hide set', async () => {
+ $statusbarHiddenIds.set(['gateway-health'])
+
+ const statusbar = bar([item('cron', 'Cron'), item('gateway-health', 'Gateway')])
+
+ expect(screen.queryByText('Gateway')).toBeNull()
+ expect(within(statusbar).getByText('Cron')).toBeTruthy()
+
+ openContextMenu(statusbar)
+ fireEvent.click(await screen.findByRole('menuitem', { name: /reset to defaults/i }))
+
+ expect($statusbarHiddenIds.get()).toEqual([...STATUSBAR_HIDDEN_BY_DEFAULT])
+ expect(within(statusbar).getByText('Gateway')).toBeTruthy()
+ // Scoped to the bar: the menu stays open after a reset, so an unscoped query
+ // matches its still-listed 'Cron' checkbox row rather than a bar item.
+ expect(within(statusbar).queryByText('Cron')).toBeNull()
+ })
+
+ it('disables the row when the layout is already default', async () => {
+ const statusbar = bar([item('cron', 'Cron'), item('gateway-health', 'Gateway')])
+
+ openContextMenu(statusbar)
+
+ const row = await screen.findByRole('menuitem', { name: /reset to defaults/i })
+ expect(row.getAttribute('data-disabled')).not.toBeNull()
+ })
+
+ it('enables the row as soon as one item differs, in either direction', async () => {
+ const statusbar = bar([item('cron', 'Cron'), item('gateway-health', 'Gateway')])
+
+ // Showing a default-hidden item counts…
+ $statusbarHiddenIds.set(STATUSBAR_HIDDEN_BY_DEFAULT.filter(id => id !== 'cron'))
+ openContextMenu(statusbar)
+ expect(
+ (await screen.findByRole('menuitem', { name: /reset to defaults/i })).getAttribute('data-disabled')
+ ).toBeNull()
+
+ // …and so does hiding a default-shown one.
+ $statusbarHiddenIds.set([...STATUSBAR_HIDDEN_BY_DEFAULT, 'gateway-health'])
+ expect(
+ (await screen.findByRole('menuitem', { name: /reset to defaults/i })).getAttribute('data-disabled')
+ ).toBeNull()
+ })
+
+ it('leaves whole-bar visibility alone — reset is about items, not the bar', async () => {
+ // Set to the NON-default so a reset that wrongly restored bar visibility too
+ // would flip this back to true and fail. StatusbarControls doesn't read the
+ // atom (the controller gates the mount), so the menu is still reachable here.
+ $statusbarVisible.set(false)
+ $statusbarHiddenIds.set([])
+
+ const statusbar = bar([item('gateway-health', 'Gateway')])
+
+ openContextMenu(statusbar)
+ fireEvent.click(await screen.findByRole('menuitem', { name: /reset to defaults/i }))
+
+ expect($statusbarHiddenIds.get()).toEqual([...STATUSBAR_HIDDEN_BY_DEFAULT])
+ expect($statusbarVisible.get()).toBe(false)
+ })
+})
+
describe('whole-bar visibility', () => {
it('hides the bar from the context menu, leaving the keybind as the way back', async () => {
const statusbar = bar([item('gateway-health', 'Gateway')])
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index 35916ae65c..b0b3d46fbe 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -2561,6 +2561,7 @@ export const en: Translations = {
gatewayTitle: 'Gateway',
customizeTitle: 'Show in status bar',
hideStatusbar: 'Hide status bar',
+ resetStatusbar: 'Reset to defaults',
toggleApprovalMode: 'Approvals',
toggleBackendVersion: 'Backend version',
toggleCommandCenter: 'Command Center',
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index 4b34a851b0..4772ef6561 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -2153,6 +2153,7 @@ export interface Translations {
gatewayTitle: string
customizeTitle: string
hideStatusbar: string
+ resetStatusbar: string
toggleApprovalMode: string
toggleBackendVersion: string
toggleCommandCenter: string
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index 471fa8767a..d33f5e1b59 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -2739,6 +2739,7 @@ export const zh: Translations = {
gatewayTitle: '网关',
customizeTitle: '在状态栏中显示',
hideStatusbar: '隐藏状态栏',
+ resetStatusbar: '恢复默认设置',
toggleApprovalMode: '审批',
toggleBackendVersion: '后端版本',
toggleCommandCenter: '命令中心',
diff --git a/apps/desktop/src/store/statusbar-prefs.ts b/apps/desktop/src/store/statusbar-prefs.ts
index 612c9be18e..7318e18d7e 100644
--- a/apps/desktop/src/store/statusbar-prefs.ts
+++ b/apps/desktop/src/store/statusbar-prefs.ts
@@ -51,3 +51,20 @@ export function setStatusbarItemVisible(id: string, visible: boolean) {
$statusbarHiddenIds.set(visible ? hidden.filter(entry => entry !== id) : [...hidden, id])
}
+
+/** Pure so the menu can derive its reset row's disabled state from the hidden
+ * list it already subscribes to, rather than reading the atom out of band.
+ * Set-compared: order is incidental (items are appended as they're hidden) and
+ * a duplicated id shouldn't read as a customization. */
+export function isStatusbarLayoutDefault(hidden: readonly string[]) {
+ const ids = new Set(hidden)
+
+ return ids.size === STATUSBAR_HIDDEN_BY_DEFAULT.length && STATUSBAR_HIDDEN_BY_DEFAULT.every(id => ids.has(id))
+}
+
+/** Put the show/hide set back to what ships. Only touches item layout — whole-bar
+ * visibility is a separate preference, and resetting from the bar's own menu
+ * shouldn't make the bar the user is right-clicking disappear. */
+export function resetStatusbarLayout() {
+ $statusbarHiddenIds.set([...STATUSBAR_HIDDEN_BY_DEFAULT])
+}