Add Codex status sign-in action (#3511)

* Add Codex status sign-in action

Implements the Codex status menu sign-in action and documents the design in docs/codex-status-sign-in-action.md.

* rm design doc
This commit is contained in:
Jinjing
2026-05-30 00:14:34 -07:00
committed by GitHub
parent 4fe3b761d8
commit 3ba1e95df4
@@ -7,11 +7,13 @@ import {
Plug,
ChevronDown,
ChevronRight,
Loader2,
PanelsTopLeft,
RefreshCw,
Server
} from 'lucide-react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import {
DropdownMenu,
@@ -858,6 +860,54 @@ function InlineUsageBars({
)
}
function isUnavailableInactiveUsage(limits: ProviderRateLimits | null | undefined): boolean {
return limits?.status === 'error' && !limits.session && !limits.weekly
}
function InlineUsageSignInAction({
isFetching,
isSigningIn,
disabled,
onSignInPointerDown,
onSignIn
}: {
isFetching: boolean
isSigningIn: boolean
disabled: boolean
onSignInPointerDown?: () => void
onSignIn: () => void
}): React.JSX.Element {
return (
<div className={`flex w-full items-center gap-2 ${isFetching ? 'animate-pulse' : ''}`}>
<span className="min-w-0 flex-1 text-[10px] text-muted-foreground">Sign in to see usage</span>
<Button
type="button"
variant="ghost"
size="xs"
disabled={disabled}
className="h-6 shrink-0 px-2 text-muted-foreground hover:text-foreground"
onPointerDown={(event) => {
event.preventDefault()
event.stopPropagation()
onSignInPointerDown?.()
}}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onSignIn()
}}
>
{isSigningIn ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
Sign in
</Button>
</div>
)
}
function InlineUsageSkeleton(): React.JSX.Element {
return (
<div className="flex w-full animate-pulse items-center gap-2">
@@ -995,6 +1045,17 @@ function CodexSwitcherMenu({
activeAccountId: null
})
const [isSwitching, setIsSwitching] = useState(false)
const [reauthenticatingAccountId, setReauthenticatingAccountId] = useState<string | null>(null)
const accountsExpandedRef = useRef(accountsExpanded)
// Why: Radix item selection is separate from the nested button click, so
// propagation stops alone do not prevent the row switch action.
const suppressNextAccountSelectRef = useRef(false)
const suppressNextAccountSelect = useCallback(() => {
suppressNextAccountSelectRef.current = true
window.setTimeout(() => {
suppressNextAccountSelectRef.current = false
}, 0)
}, [])
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const fetchSettings = useAppStore((s) => s.fetchSettings)
@@ -1021,6 +1082,10 @@ function CodexSwitcherMenu({
setAccounts(next)
}, [])
useEffect(() => {
accountsExpandedRef.current = accountsExpanded
}, [accountsExpanded])
useEffect(() => {
// Why: the status bar keeps its own lightweight account snapshot for the
// dropdown. Settings account actions mutate the main-process store outside
@@ -1035,7 +1100,7 @@ function CodexSwitcherMenu({
accountId: string | null,
target: CodexStatusRuntimeTarget
): Promise<void> => {
if (isSwitching) {
if (isSwitching || reauthenticatingAccountId !== null) {
return
}
const previousActiveAccountId = getCodexStatusActiveId(accountState, target)
@@ -1068,6 +1133,26 @@ function CodexSwitcherMenu({
}
}
const handleSignInAccount = async (accountId: string): Promise<void> => {
if (isSwitching || reauthenticatingAccountId !== null) {
return
}
setReauthenticatingAccountId(accountId)
try {
const next = await window.api.codexAccounts.reauthenticate({ accountId })
recordFeatureInteraction('codex-account-switching')
setAccounts(next)
await fetchSettings()
if (accountsExpandedRef.current) {
await fetchInactiveCodexAccountUsage()
}
} catch (error) {
console.error('Failed to re-authenticate Codex account from status bar:', error)
} finally {
setReauthenticatingAccountId(null)
}
}
const handleSelectRuntime = async (group: CodexStatusSwitchGroup): Promise<void> => {
const currentKey = getCodexStatusRuntimeKey(
normalizeCodexStatusRuntimeTarget(accountState, toCodexStatusRuntimeTarget(codexTarget))
@@ -1161,6 +1246,12 @@ function CodexSwitcherMenu({
const inactiveUsage = target.id
? inactiveCodexAccounts.find((a) => a.accountId === target.id)
: null
const showSignInAction =
!target.active &&
target.id !== null &&
isUnavailableInactiveUsage(inactiveUsage?.claude)
const isSigningIn = reauthenticatingAccountId === target.id
const isBusy = isSwitching || reauthenticatingAccountId !== null
return (
<DropdownMenuItem
@@ -1171,11 +1262,15 @@ function CodexSwitcherMenu({
// auto-closing so that prompt can stay within the same
// account-switcher interaction instead of jumping elsewhere.
event.preventDefault()
if (suppressNextAccountSelectRef.current) {
suppressNextAccountSelectRef.current = false
return
}
if (!target.active) {
void handleSelectAccount(target.id, target.runtimeTarget)
}
}}
disabled={isSwitching || target.active}
disabled={isBusy || target.active}
>
<div className="flex w-full min-w-0 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2">
@@ -1188,6 +1283,19 @@ function CodexSwitcherMenu({
</div>
{inactiveUsage?.isFetching && !inactiveUsage.claude ? (
<InlineUsageSkeleton />
) : showSignInAction ? (
<InlineUsageSignInAction
isFetching={inactiveUsage?.isFetching ?? false}
isSigningIn={isSigningIn}
disabled={isBusy}
onSignInPointerDown={suppressNextAccountSelect}
onSignIn={() => {
suppressNextAccountSelect()
if (target.id !== null) {
void handleSignInAccount(target.id)
}
}}
/>
) : inactiveUsage?.claude ? (
<InlineUsageBars
limits={inactiveUsage.claude}