fix(status-bar): preserve roster menu interactions

This commit is contained in:
Brennan Benson
2026-07-20 13:11:01 -07:00
parent e8e89de86c
commit ac36f57876
3 changed files with 52 additions and 17 deletions
@@ -1757,11 +1757,11 @@ export function ProviderDetailsMenu({
const usagePercentageDisplay = normalizeUsagePercentageDisplay(
useAppStore((s) => s.usagePercentageDisplay)
)
const skipCloseAutoFocusRef = useRef(false)
const menuFocusHandoff = useStatusBarMenuFocusHandoff()
const handleOpenChange = (nextOpen: boolean): void => {
if (nextOpen) {
skipCloseAutoFocusRef.current = false
menuFocusHandoff.reset()
recordFeatureInteraction('usage-tracking')
}
onOpenChange?.(nextOpen)
@@ -1825,17 +1825,8 @@ export function ProviderDetailsMenu({
align="start"
sideOffset={8}
className="w-[260px]"
onPointerDownOutside={() => {
skipCloseAutoFocusRef.current = true
}}
onCloseAutoFocus={(event) => {
if (!skipCloseAutoFocusRef.current) {
return
}
skipCloseAutoFocusRef.current = false
// Why: focus the clicked surface (esp. xterm) — Radix's default trigger restore steals that first click.
event.preventDefault()
}}
onPointerDownOutside={menuFocusHandoff.onPointerDownOutside}
onCloseAutoFocus={menuFocusHandoff.onCloseAutoFocus}
>
{panelBody}
</DropdownMenuContent>
@@ -1845,6 +1836,30 @@ export function ProviderDetailsMenu({
const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus'
function useStatusBarMenuFocusHandoff(): {
reset: () => void
onPointerDownOutside: () => void
onCloseAutoFocus: (event: Event) => void
} {
const skipCloseAutoFocusRef = useRef(false)
return {
reset: () => {
skipCloseAutoFocusRef.current = false
},
onPointerDownOutside: () => {
skipCloseAutoFocusRef.current = true
},
onCloseAutoFocus: (event) => {
if (!skipCloseAutoFocusRef.current) {
return
}
skipCloseAutoFocusRef.current = false
// Why: Radix trigger restoration steals the first click from surfaces such as xterm.
event.preventDefault()
}
}
}
function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Element | null {
const floatingTerminalShortcut = useShortcutLabel('floatingTerminal.toggle')
const rateLimits = useAppStore((s) => s.rateLimits)
@@ -1856,6 +1871,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
useAppStore((s) => s.usagePercentageDisplay)
)
const [usageMenuOpen, setUsageMenuOpen] = useState(false)
const usageMenuFocusHandoff = useStatusBarMenuFocusHandoff()
const statusBarVisible = useAppStore((s) => s.statusBarVisible)
const statusBarItems = useAppStore((s) => s.statusBarItems)
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
@@ -2059,6 +2075,13 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
openSettingsTarget({ pane: 'accounts', repoId: null, sectionId })
openSettingsPage()
}
const handleUsageMenuOpenChange = (nextOpen: boolean): void => {
if (nextOpen) {
usageMenuFocusHandoff.reset()
recordFeatureInteraction('usage-tracking')
}
setUsageMenuOpen(nextOpen)
}
return (
<div
@@ -2084,7 +2107,11 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
) : hasVisibleUsageMeters ? (
// Consolidated roster pill → opens the all-agents Usage popover (mock parity).
<UsagePercentageDisplayChangeNotice hasVisibleUsageMeters={hasVisibleUsageMeters}>
<DropdownMenu open={usageMenuOpen} onOpenChange={setUsageMenuOpen} modal={false}>
<DropdownMenu
open={usageMenuOpen}
onOpenChange={handleUsageMenuOpenChange}
modal={false}
>
<DropdownMenuTrigger asChild>
<button
type="button"
@@ -2120,6 +2147,8 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
// bar instead of overlapping it — bottom padding ≈ footer height.
collisionPadding={{ top: 8, bottom: 32, left: 8, right: 8 }}
className="w-[360px] p-0"
onPointerDownOutside={usageMenuFocusHandoff.onPointerDownOutside}
onCloseAutoFocus={usageMenuFocusHandoff.onCloseAutoFocus}
>
<UsageRosterPanel
providers={rosterProviders}
@@ -9,7 +9,8 @@ describe('formatPlanLabel', () => {
})
it('title-cases multi-token plans across separators', () => {
expect(formatPlanLabel('chatgpt_business')).toBe('Chatgpt Business')
expect(formatPlanLabel('chatgpt_business')).toBe('ChatGPT Business')
expect(formatPlanLabel('CHATGPT_PLUS')).toBe('ChatGPT Plus')
expect(formatPlanLabel('team-plus')).toBe('Team Plus')
expect(formatPlanLabel('pro trial')).toBe('Pro Trial')
})
@@ -1,7 +1,7 @@
// Pure formatting for the consolidated Usage roster, split out so it can be unit
// tested without pulling in React / UI dependencies.
// "plus" -> "Plus", "chatgpt_business" -> "Chatgpt Business". Codex is the only
// "plus" -> "Plus", "chatgpt_business" -> "ChatGPT Business". Codex is the only
// provider that reports a plan today; others render just the name.
export function formatPlanLabel(planType: string | null | undefined): string | null {
const trimmed = planType?.trim()
@@ -10,7 +10,12 @@ export function formatPlanLabel(planType: string | null | undefined): string | n
}
return trimmed
.split(/[\s_-]+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.map((word) => {
const normalized = word.toLowerCase()
return normalized === 'chatgpt'
? 'ChatGPT'
: normalized.charAt(0).toUpperCase() + normalized.slice(1)
})
.join(' ')
}