mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 08:03:13 +00:00
feat(status-bar): toggle compact usage summary
This commit is contained in:
@@ -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
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) => (
|
||||
<React.Fragment key={bucket.name}>
|
||||
{index > 0 ? <span className="text-muted-foreground">·</span> : null}
|
||||
<span className="tabular-nums">
|
||||
{bucket.name} {formatUsagePercentageLabel(bucket.usedPercent, display)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{visibleBuckets.length === 0 && p.session ? (
|
||||
<WindowLabel
|
||||
w={p.session}
|
||||
label={formatRateLimitWindowChipLabel(p.session)}
|
||||
display={display}
|
||||
/>
|
||||
) : 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 ? (
|
||||
<MiniBar usedPct={clampUsedPercent(p.session.usedPercent)} display={display} />
|
||||
) : null}
|
||||
{visibleWindows.map((window, index) => (
|
||||
<React.Fragment key={window.key}>
|
||||
{index > 0 ? <span className="text-muted-foreground">·</span> : null}
|
||||
<WindowLabel w={window.window} label={window.label} display={display} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ProviderIcon provider={provider} />
|
||||
{tightest && !compact ? (
|
||||
<MiniBar usedPct={clampUsedPercent(tightest.window.usedPercent)} display={display} />
|
||||
) : null}
|
||||
{tightest ? (
|
||||
<WindowLabel
|
||||
w={tightest.window}
|
||||
label={tightest.label}
|
||||
display={display}
|
||||
showLabel={!compact}
|
||||
/>
|
||||
) : null}
|
||||
{mode === 'verbose' ? (
|
||||
<VerboseProviderUsage p={p} compact={compact} display={display} />
|
||||
) : (
|
||||
<>
|
||||
{tightest && !compact ? (
|
||||
<MiniBar usedPct={clampUsedPercent(tightest.window.usedPercent)} display={display} />
|
||||
) : null}
|
||||
{tightest ? (
|
||||
<WindowLabel
|
||||
w={tightest.window}
|
||||
label={tightest.label}
|
||||
display={display}
|
||||
showLabel={!compact}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{isStale && <AlertTriangle size={11} className="text-muted-foreground/80" />}
|
||||
</span>
|
||||
)
|
||||
@@ -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
|
||||
<UsageRosterPanel
|
||||
providers={rosterProviders}
|
||||
display={usagePercentageDisplay}
|
||||
statusBarUsageMode={statusBarUsageMode}
|
||||
onStatusBarUsageModeChange={setStatusBarUsageMode}
|
||||
isRefreshing={isRefreshing || anyFetching}
|
||||
onRefresh={handleRefresh}
|
||||
onOpenProvider={handleOpenProviderAccounts}
|
||||
|
||||
@@ -17,6 +17,21 @@ vi.mock('@/hooks/useResetCountdownClock', () => ({
|
||||
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
|
||||
}>) => (
|
||||
<div data-checked={checked} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
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"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
)
|
||||
})}
|
||||
<div className="border-t border-border/70" />
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={statusBarUsageMode === 'verbose'}
|
||||
onCheckedChange={(checked) => {
|
||||
onStatusBarUsageModeChange(checked ? 'verbose' : 'compact')
|
||||
}}
|
||||
onSelect={(event) => event.preventDefault()}
|
||||
className="w-full cursor-pointer rounded-none py-2 pl-8 pr-3.5"
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
<span className="text-[13px] text-foreground">
|
||||
{translate(
|
||||
'auto.components.status.bar.UsageRosterPanel.verboseFooter',
|
||||
'Verbose footer'
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[11px] font-normal text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.status.bar.UsageRosterPanel.verboseFooterDescription',
|
||||
'Show every usage window'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<div className="border-t border-border/70" />
|
||||
<DropdownMenuItem
|
||||
onSelect={onUsageDetails}
|
||||
className="w-full cursor-pointer justify-between rounded-none px-3.5 py-2.5 text-[13px] text-foreground"
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('ProviderSegment monthly window', () => {
|
||||
const { ProviderSegment } = await import('./StatusBar')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={grokMonthlyLimits('ok')} compact={false} display="used" />
|
||||
<ProviderSegment p={grokMonthlyLimits('ok')} compact={false} display="used" mode="compact" />
|
||||
)
|
||||
|
||||
expect(markup).toContain('25% used 30d')
|
||||
@@ -58,7 +58,12 @@ describe('ProviderSegment monthly window', () => {
|
||||
const { ProviderSegment } = await import('./StatusBar')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={grokMonthlyLimits('fetching')} compact={false} display="used" />
|
||||
<ProviderSegment
|
||||
p={grokMonthlyLimits('fetching')}
|
||||
compact={false}
|
||||
display="used"
|
||||
mode="compact"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('25% used 30d')
|
||||
@@ -78,7 +83,7 @@ describe('ProviderSegment monthly window', () => {
|
||||
status: 'ok'
|
||||
}
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={limits} compact={false} display="used" />
|
||||
<ProviderSegment p={limits} compact={false} display="used" mode="compact" />
|
||||
)
|
||||
|
||||
expect(markup).toContain('30% used 30d')
|
||||
@@ -102,7 +107,7 @@ describe('ProviderSegment monthly window', () => {
|
||||
}
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={limits} compact={false} display="used" />
|
||||
<ProviderSegment p={limits} compact={false} display="used" mode="compact" />
|
||||
)
|
||||
|
||||
expect(markup).toContain('80% used Pro')
|
||||
@@ -127,7 +132,7 @@ describe('ProviderSegment monthly window', () => {
|
||||
status: 'ok'
|
||||
}
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={limits} compact={false} display="used" />
|
||||
<ProviderSegment p={limits} compact={false} display="used" mode="compact" />
|
||||
)
|
||||
|
||||
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(
|
||||
<ProviderSegment p={limits} compact={false} display="used" mode="verbose" />
|
||||
)
|
||||
|
||||
expect(markup).toContain('10% used 5h')
|
||||
expect(markup).toContain('20% used wk')
|
||||
expect(markup).toContain('30% used Fable')
|
||||
expect(markup).not.toContain('40% used')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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<string, WorkspacePortScanResult>
|
||||
workspacePortScanRefreshing: boolean
|
||||
@@ -2109,6 +2116,12 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (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<AppState, [], [], UISlice> = (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),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user