From ecbbf786801ea2523ae6e8d33ee93ffff1b00fb2 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:21:08 -0700 Subject: [PATCH] feat(status-bar): toggle compact usage summary --- src/main/persistence.ts | 5 + .../runtime/rpc/methods/client-ui-schemas.ts | 1 + .../src/components/status-bar/StatusBar.tsx | 125 ++++++++++++++++-- .../status-bar/UsageRosterPanel.test.tsx | 19 +++ .../status-bar/UsageRosterPanel.tsx | 31 ++++- .../provider-segment-monthly-window.test.tsx | 38 +++++- src/renderer/src/i18n/locales/en.json | 4 +- src/renderer/src/i18n/locales/es.json | 4 +- src/renderer/src/i18n/locales/ja.json | 4 +- src/renderer/src/i18n/locales/ko.json | 4 +- src/renderer/src/i18n/locales/zh.json | 4 +- src/renderer/src/store/slices/ui.test.ts | 28 ++++ src/renderer/src/store/slices/ui.ts | 14 ++ src/renderer/src/web/web-preload-api.ts | 4 + src/shared/constants.ts | 2 + src/shared/status-bar-usage-mode.test.ts | 14 ++ src/shared/status-bar-usage-mode.ts | 7 + src/shared/types.ts | 3 + 18 files changed, 288 insertions(+), 23 deletions(-) create mode 100644 src/shared/status-bar-usage-mode.test.ts create mode 100644 src/shared/status-bar-usage-mode.ts diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 2e5b8255367..0fb057c8c7b 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -103,6 +103,7 @@ import { } from '../shared/constants' import { parseWorkspaceSession } from '../shared/workspace-session-schema' import { normalizeUsagePercentageDisplay } from '../shared/usage-percentage-display' +import { normalizeStatusBarUsageMode } from '../shared/status-bar-usage-mode' import { isExistingPersistedProfile } from '../shared/project-order-manual-default-notice' import { resolveUsagePercentageDisplayChangeNoticeDismissed } from '../shared/usage-percentage-display-change-notice' import { normalizePRBotAuthorOverrides } from '../shared/pr-bot-author-overrides' @@ -5181,6 +5182,7 @@ export class Store { usagePercentageDisplay: normalizeUsagePercentageDisplay( this.state.ui?.usagePercentageDisplay ), + statusBarUsageMode: normalizeStatusBarUsageMode(this.state.ui?.statusBarUsageMode), // Why: strict boolean coercion so a missing/legacy value reads as false (first-run notice still fires). trayMinimizeNoticeShown: this.state.ui?.trayMinimizeNoticeShown === true, markdownTocPanelWidth: clampMarkdownTocPanelWidth(this.state.ui?.markdownTocPanelWidth), @@ -5278,6 +5280,9 @@ export class Store { usagePercentageDisplay: normalizeUsagePercentageDisplay( sanitizedUpdates.usagePercentageDisplay ?? this.state.ui?.usagePercentageDisplay ), + statusBarUsageMode: normalizeStatusBarUsageMode( + sanitizedUpdates.statusBarUsageMode ?? this.state.ui?.statusBarUsageMode + ), markdownTocPanelWidth: clampMarkdownTocPanelWidth( sanitizedUpdates.markdownTocPanelWidth ?? this.state.ui?.markdownTocPanelWidth ), diff --git a/src/main/runtime/rpc/methods/client-ui-schemas.ts b/src/main/runtime/rpc/methods/client-ui-schemas.ts index b5adcf617b4..48d014b0de9 100644 --- a/src/main/runtime/rpc/methods/client-ui-schemas.ts +++ b/src/main/runtime/rpc/methods/client-ui-schemas.ts @@ -219,6 +219,7 @@ export const UiUpdate = z _grokStatusBarDefaultAdded: z.boolean().optional(), statusBarVisible: z.boolean().optional(), usagePercentageDisplay: z.enum(['used', 'remaining']).optional(), + statusBarUsageMode: z.enum(['verbose', 'compact']).optional(), dismissedUpdateVersion: NullableString.optional(), lastUpdateCheckAt: z.number().finite().nullable().optional(), pendingUpdateNudgeId: NullableString.optional(), diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index fa2b7d5805a..3fd5a3d72c0 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -94,6 +94,10 @@ import { type UsagePercentageDisplay } from '../../../../shared/usage-percentage-display' import { formatUsagePercentageLabel } from './usage-percentage-label' +import { + normalizeStatusBarUsageMode, + type StatusBarUsageMode +} from '../../../../shared/status-bar-usage-mode' type StatusBarProps = { floatingTerminalOpen: boolean @@ -1155,14 +1159,100 @@ function getProviderLetter(provider: ProviderRateLimits['provider']): string { // Provider segment // --------------------------------------------------------------------------- -export function ProviderSegment({ +// Why: Gemini exposes extra experimental buckets that made the pre-existing verbose footer noisy. +const STATUS_BAR_BUCKET_NAMES = new Set(['Flash', 'Pro', '1.5 Pro']) + +function VerboseProviderUsage({ p, compact, display +}: { + p: ProviderRateLimits + compact: boolean + display: UsagePercentageDisplay +}): React.JSX.Element { + if (p.buckets && p.buckets.length > 0) { + const visibleBuckets = p.buckets.filter((bucket) => STATUS_BAR_BUCKET_NAMES.has(bucket.name)) + return ( + <> + {visibleBuckets.map((bucket, index) => ( + + {index > 0 ? · : null} + + {bucket.name} {formatUsagePercentageLabel(bucket.usedPercent, display)} + + + ))} + {visibleBuckets.length === 0 && p.session ? ( + + ) : null} + + ) + } + + const visibleWindows = [ + p.session + ? { + key: 'session', + window: p.session, + label: formatRateLimitWindowChipLabel(p.session) + } + : null, + p.weekly + ? { + key: 'weekly', + window: p.weekly, + label: formatRateLimitWindowChipLabel(p.weekly) + } + : null, + p.fableWeekly + ? { + key: 'fableWeekly', + window: p.fableWeekly, + label: translate('auto.components.status.bar.StatusBar.a79c64f87e', 'Fable') + } + : null, + // Why: monthly stays inline for monthly-only providers; otherwise the detail panel carries it. + p.monthly && !p.session && !p.weekly + ? { + key: 'monthly', + window: p.monthly, + label: formatRateLimitWindowChipLabel(p.monthly) + } + : null + ].filter((window): window is { key: string; window: RateLimitWindow; label: string } => { + return window !== null + }) + + return ( + <> + {p.session && !compact ? ( + + ) : null} + {visibleWindows.map((window, index) => ( + + {index > 0 ? · : null} + + + ))} + + ) +} + +export function ProviderSegment({ + p, + compact, + display, + mode = 'verbose' }: { p: ProviderRateLimits | null compact: boolean display: UsagePercentageDisplay + mode?: StatusBarUsageMode }): React.JSX.Element { const provider = p?.provider ?? 'claude' const statusLabel = p ? getProviderUsageStatusLabel(p) : '' @@ -1215,17 +1305,23 @@ export function ProviderSegment({ return ( - {tightest && !compact ? ( - - ) : null} - {tightest ? ( - - ) : null} + {mode === 'verbose' ? ( + + ) : ( + <> + {tightest && !compact ? ( + + ) : null} + {tightest ? ( + + ) : null} + + )} {isStale && } ) @@ -1870,6 +1966,8 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele const usagePercentageDisplay = normalizeUsagePercentageDisplay( useAppStore((s) => s.usagePercentageDisplay) ) + const statusBarUsageMode = normalizeStatusBarUsageMode(useAppStore((s) => s.statusBarUsageMode)) + const setStatusBarUsageMode = useAppStore((s) => s.setStatusBarUsageMode) const [usageMenuOpen, setUsageMenuOpen] = useState(false) const usageMenuFocusHandoff = useStatusBarMenuFocusHandoff() const statusBarVisible = useAppStore((s) => s.statusBarVisible) @@ -2133,6 +2231,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele p={p} compact={compact} display={usagePercentageDisplay} + mode={statusBarUsageMode} /> ) )} @@ -2153,6 +2252,8 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele ({ useResetCountdownClock: mocks.useResetCountdownClock })) vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenuCheckboxItem: ({ + children, + checked, + onCheckedChange: _onCheckedChange, + onSelect: _onSelect, + ...props + }: React.PropsWithChildren<{ + checked?: boolean + onCheckedChange?: (checked: boolean) => void + onSelect?: () => void + }>) => ( +
+ {children} +
+ ), DropdownMenuItem: ({ children, onSelect: _onSelect, @@ -107,6 +122,8 @@ describe('UsageRow', () => { } ]} display="used" + statusBarUsageMode="verbose" + onStatusBarUsageModeChange={() => {}} isRefreshing={false} onRefresh={() => {}} onOpenProvider={() => {}} @@ -120,5 +137,7 @@ describe('UsageRow', () => { expect(mocks.useResetCountdownClock).toHaveBeenCalledOnce() expect(mocks.useResetCountdownClock).toHaveBeenCalledWith([sessionReset, weeklyReset]) expect(markup).toContain('Resets in 2m') + expect(markup).toContain('Verbose footer') + expect(markup).toContain('data-checked="true"') }) }) diff --git a/src/renderer/src/components/status-bar/UsageRosterPanel.tsx b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx index 2d441896108..cbfdbdd4869 100644 --- a/src/renderer/src/components/status-bar/UsageRosterPanel.tsx +++ b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx @@ -1,6 +1,6 @@ import React from 'react' import { ChevronRight, RefreshCw } from 'lucide-react' -import { DropdownMenuItem } from '@/components/ui/dropdown-menu' +import { DropdownMenuCheckboxItem, DropdownMenuItem } from '@/components/ui/dropdown-menu' import { useResetCountdownClock } from '@/hooks/useResetCountdownClock' import { translate } from '@/i18n/i18n' import { formatRateLimitWindowChipLabel, formatWindowLabel } from '@/lib/window-label-formatter' @@ -14,6 +14,7 @@ import { barColor, formatResetCountdown, getWindowSections, ProviderIcon } from import { getProviderDisplayName } from './usage-error-copy' import { formatPlanLabel, usageTextColorClass } from './usage-roster-formatting' import { getUsageRosterRowState, type UsageRosterRowState } from './usage-roster-row-state' +import type { StatusBarUsageMode } from '../../../../shared/status-bar-usage-mode' type ProviderId = ProviderRateLimits['provider'] export type UsageSection = { label: string; window: RateLimitWindow } @@ -156,6 +157,8 @@ export function UsageRow({ export function UsageRosterPanel({ providers, display, + statusBarUsageMode, + onStatusBarUsageModeChange, isRefreshing, onRefresh, onOpenProvider, @@ -167,6 +170,8 @@ export function UsageRosterPanel({ }: { providers: ProviderRateLimits[] display: UsagePercentageDisplay + statusBarUsageMode: StatusBarUsageMode + onStatusBarUsageModeChange: (mode: StatusBarUsageMode) => void isRefreshing: boolean onRefresh: () => void onOpenProvider: (provider: ProviderId) => void @@ -254,6 +259,30 @@ export function UsageRosterPanel({ ) })}
+ { + onStatusBarUsageModeChange(checked ? 'verbose' : 'compact') + }} + onSelect={(event) => event.preventDefault()} + className="w-full cursor-pointer rounded-none py-2 pl-8 pr-3.5" + > + + + {translate( + 'auto.components.status.bar.UsageRosterPanel.verboseFooter', + 'Verbose footer' + )} + + + {translate( + 'auto.components.status.bar.UsageRosterPanel.verboseFooterDescription', + 'Show every usage window' + )} + + + +
{ const { ProviderSegment } = await import('./StatusBar') const markup = renderToStaticMarkup( - + ) expect(markup).toContain('25% used 30d') @@ -58,7 +58,12 @@ describe('ProviderSegment monthly window', () => { const { ProviderSegment } = await import('./StatusBar') const markup = renderToStaticMarkup( - + ) expect(markup).toContain('25% used 30d') @@ -78,7 +83,7 @@ describe('ProviderSegment monthly window', () => { status: 'ok' } const markup = renderToStaticMarkup( - + ) expect(markup).toContain('30% used 30d') @@ -102,7 +107,7 @@ describe('ProviderSegment monthly window', () => { } const markup = renderToStaticMarkup( - + ) expect(markup).toContain('80% used Pro') @@ -127,7 +132,7 @@ describe('ProviderSegment monthly window', () => { status: 'ok' } const markup = renderToStaticMarkup( - + ) expect(markup).toContain('42% used 2h 33m') @@ -139,4 +144,27 @@ describe('ProviderSegment monthly window', () => { dateNow.mockRestore() } }) + + it('restores every inline window in verbose mode', async () => { + const { ProviderSegment } = await import('./StatusBar') + const limits: ProviderRateLimits = { + provider: 'claude', + session: windowOf(10, 300), + weekly: windowOf(20, 10_080), + fableWeekly: windowOf(30, 10_080), + monthly: windowOf(40, 43_200), + updatedAt: Date.now(), + error: null, + status: 'ok' + } + + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('10% used 5h') + expect(markup).toContain('20% used wk') + expect(markup).toContain('30% used Fable') + expect(markup).not.toContain('40% used') + }) }) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index f141804da5d..9a611c3314e 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -3308,7 +3308,9 @@ "usageDetails": "Usage details & history", "loadingUsage": "Loading usage…", "usageUnavailable": "Usage unavailable", - "noUsageData": "No usage data" + "noUsageData": "No usage data", + "verboseFooter": "Verbose footer", + "verboseFooterDescription": "Show every usage window" } } }, diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 0857b8da95b..9ecf549208c 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -3285,7 +3285,9 @@ "usageDetails": "Usage details & history", "loadingUsage": "Loading usage…", "usageUnavailable": "Usage unavailable", - "noUsageData": "No usage data" + "noUsageData": "No usage data", + "verboseFooter": "Verbose footer", + "verboseFooterDescription": "Show every usage window" } } }, diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index ad5fa07517e..bf1993ba500 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -3285,7 +3285,9 @@ "usageDetails": "Usage details & history", "loadingUsage": "Loading usage…", "usageUnavailable": "Usage unavailable", - "noUsageData": "No usage data" + "noUsageData": "No usage data", + "verboseFooter": "Verbose footer", + "verboseFooterDescription": "Show every usage window" } } }, diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 05f344e78c1..717ba022d1c 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -3285,7 +3285,9 @@ "usageDetails": "Usage details & history", "loadingUsage": "Loading usage…", "usageUnavailable": "Usage unavailable", - "noUsageData": "No usage data" + "noUsageData": "No usage data", + "verboseFooter": "Verbose footer", + "verboseFooterDescription": "Show every usage window" } } }, diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 0beabe342dd..80956df816b 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -3285,7 +3285,9 @@ "usageDetails": "Usage details & history", "loadingUsage": "Loading usage…", "usageUnavailable": "Usage unavailable", - "noUsageData": "No usage data" + "noUsageData": "No usage data", + "verboseFooter": "Verbose footer", + "verboseFooterDescription": "Show every usage window" } } }, diff --git a/src/renderer/src/store/slices/ui.test.ts b/src/renderer/src/store/slices/ui.test.ts index f7fa68279f1..b9d9454de08 100644 --- a/src/renderer/src/store/slices/ui.test.ts +++ b/src/renderer/src/store/slices/ui.test.ts @@ -1452,6 +1452,34 @@ describe('createUISlice hydratePersistedUI', () => { expect(store.getState().usagePercentageDisplay).toBe('used') }) + it('persists and hydrates the status bar usage mode', () => { + const setUI = vi.fn().mockResolvedValue(undefined) + vi.stubGlobal('window', { api: { ui: { set: setUI } } }) + const store = createUIStore() + + expect(store.getState().statusBarUsageMode).toBe('verbose') + + store.getState().setStatusBarUsageMode('compact') + + expect(store.getState().statusBarUsageMode).toBe('compact') + expect(setUI).toHaveBeenCalledWith({ statusBarUsageMode: 'compact' }) + + store.getState().hydratePersistedUI(makePersistedUI({ statusBarUsageMode: 'verbose' })) + expect(store.getState().statusBarUsageMode).toBe('verbose') + }) + + it('defaults invalid status bar usage modes to verbose', () => { + const store = createUIStore() + + store.getState().hydratePersistedUI( + makePersistedUI({ + statusBarUsageMode: 'expanded' as PersistedUIState['statusBarUsageMode'] + }) + ) + + expect(store.getState().statusBarUsageMode).toBe('verbose') + }) + it('clamps persisted workspace board column width', () => { const store = createUIStore() diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts index fc0d5eba962..1a46226d356 100644 --- a/src/renderer/src/store/slices/ui.ts +++ b/src/renderer/src/store/slices/ui.ts @@ -41,6 +41,11 @@ import { DEFAULT_USAGE_PERCENTAGE_DISPLAY, normalizeUsagePercentageDisplay } from '../../../../shared/usage-percentage-display' +import { + DEFAULT_STATUS_BAR_USAGE_MODE, + normalizeStatusBarUsageMode, + type StatusBarUsageMode +} from '../../../../shared/status-bar-usage-mode' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { LaunchSource } from '../../../../shared/telemetry-events' import type { TaskSourceContext } from '../../../../shared/task-source-context' @@ -878,6 +883,8 @@ export type UISlice = { setStatusBarVisible: (v: boolean) => void usagePercentageDisplay: UsagePercentageDisplay setUsagePercentageDisplay: (display: UsagePercentageDisplay) => void + statusBarUsageMode: StatusBarUsageMode + setStatusBarUsageMode: (mode: StatusBarUsageMode) => void workspacePortScan: { key: string; result: WorkspacePortScanResult } | null workspacePortScansByKey: Record workspacePortScanRefreshing: boolean @@ -2109,6 +2116,12 @@ export const createUISlice: StateCreator = (set, get) usagePercentageDisplayChangeNoticeDismissed: true }) }, + statusBarUsageMode: DEFAULT_STATUS_BAR_USAGE_MODE, + setStatusBarUsageMode: (mode) => { + const normalized = normalizeStatusBarUsageMode(mode) + window.api.ui.set({ statusBarUsageMode: normalized }).catch(console.error) + set({ statusBarUsageMode: normalized }) + }, workspacePortScan: null, workspacePortScansByKey: {}, workspacePortScanRefreshing: false, @@ -2377,6 +2390,7 @@ export const createUISlice: StateCreator = (set, get) statusBarItems: statusBarItemsWithGrok, statusBarVisible: ui.statusBarVisible ?? true, usagePercentageDisplay: normalizeUsagePercentageDisplay(ui.usagePercentageDisplay), + statusBarUsageMode: normalizeStatusBarUsageMode(ui.statusBarUsageMode), // Why: default true so existing users see the pet on first enabling the flag; only an explicit Hide persists false. petVisible: ui.petVisible ?? ui.sidekickVisible ?? true, petSize: clampPetSize(ui.petSize ?? ui.sidekickSize ?? PET_SIZE_DEFAULT), diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index 153839e27bf..2f33d2bacd4 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -75,6 +75,7 @@ import { normalizeTerminalCursorStyleDefault } from '../../../shared/terminal-cu import { normalizeTerminalCustomThemes } from '../../../shared/terminal-custom-themes' import { normalizeUiLanguage } from '../../../shared/ui-language' import { normalizeUsagePercentageDisplay } from '../../../shared/usage-percentage-display' +import { normalizeStatusBarUsageMode } from '../../../shared/status-bar-usage-mode' import type { RateLimitState } from '../../../shared/rate-limit-types' import type { RuntimeStatus, RuntimeSyncWindowGraph } from '../../../shared/runtime-types' import { @@ -3449,6 +3450,9 @@ function mergeWebUIState( ), usagePercentageDisplay: normalizeUsagePercentageDisplay( safeUpdates.usagePercentageDisplay ?? base.usagePercentageDisplay + ), + statusBarUsageMode: normalizeStatusBarUsageMode( + safeUpdates.statusBarUsageMode ?? base.statusBarUsageMode ) } } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 19074727e06..61bc2a088fa 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -32,6 +32,7 @@ import { DEFAULT_SOURCE_CONTROL_GROUP_ORDER } from './source-control-group-order import { DEFAULT_SETUP_AGENT_STARTUP_POLICY } from './setup-agent-startup-policy' import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT } from './terminal-scrollback-policy' import { DEFAULT_USAGE_PERCENTAGE_DISPLAY } from './usage-percentage-display' +import { DEFAULT_STATUS_BAR_USAGE_MODE } from './status-bar-usage-mode' export { DEFAULT_STATUS_BAR_ITEMS } from './status-bar-defaults' export { @@ -464,6 +465,7 @@ export function getDefaultUIState(): PersistedUIState { statusBarItems: [...DEFAULT_STATUS_BAR_ITEMS], statusBarVisible: true, usagePercentageDisplay: DEFAULT_USAGE_PERCENTAGE_DISPLAY, + statusBarUsageMode: DEFAULT_STATUS_BAR_USAGE_MODE, dismissedUpdateVersion: null, lastUpdateCheckAt: null, trustedOrcaHooks: {}, diff --git a/src/shared/status-bar-usage-mode.test.ts b/src/shared/status-bar-usage-mode.test.ts new file mode 100644 index 00000000000..f5d2e3e384c --- /dev/null +++ b/src/shared/status-bar-usage-mode.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { normalizeStatusBarUsageMode } from './status-bar-usage-mode' + +describe('normalizeStatusBarUsageMode', () => { + it('defaults missing and invalid values to verbose', () => { + expect(normalizeStatusBarUsageMode(undefined)).toBe('verbose') + expect(normalizeStatusBarUsageMode('expanded')).toBe('verbose') + }) + + it('preserves supported modes', () => { + expect(normalizeStatusBarUsageMode('verbose')).toBe('verbose') + expect(normalizeStatusBarUsageMode('compact')).toBe('compact') + }) +}) diff --git a/src/shared/status-bar-usage-mode.ts b/src/shared/status-bar-usage-mode.ts new file mode 100644 index 00000000000..aa780e92909 --- /dev/null +++ b/src/shared/status-bar-usage-mode.ts @@ -0,0 +1,7 @@ +export type StatusBarUsageMode = 'verbose' | 'compact' + +export const DEFAULT_STATUS_BAR_USAGE_MODE: StatusBarUsageMode = 'verbose' + +export function normalizeStatusBarUsageMode(value: unknown): StatusBarUsageMode { + return value === 'verbose' || value === 'compact' ? value : DEFAULT_STATUS_BAR_USAGE_MODE +} diff --git a/src/shared/types.ts b/src/shared/types.ts index fbb1288de3d..eed668cf04f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -41,6 +41,7 @@ import type { LocalWindowsRuntimePreference } from './project-execution-runtime' import type { UsagePercentageDisplay } from './usage-percentage-display' +import type { StatusBarUsageMode } from './status-bar-usage-mode' import type { PersistedNativeChatSessionOptions } from './native-chat-session-options' // Re-exported for backward compat with renderer call sites that import @@ -3213,6 +3214,8 @@ export type PersistedUIState = { statusBarVisible: boolean /** Why: this is client-side presentation, not a provider/account or execution-host setting. */ usagePercentageDisplay?: UsagePercentageDisplay + /** Client-side footer presentation; verbose preserves the pre-roster all-window default. */ + statusBarUsageMode?: StatusBarUsageMode dismissedUpdateVersion: string | null lastUpdateCheckAt: number | null pendingUpdateNudgeId?: string | null