From 3ba1e95df4d71bba9ae4569f800bfe102d423d5d Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 30 May 2026 00:14:34 -0700 Subject: [PATCH] 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 --- .../src/components/status-bar/StatusBar.tsx | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index feda8c73787..a7b4cce58b6 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -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 ( +
+ Sign in to see usage + +
+ ) +} + function InlineUsageSkeleton(): React.JSX.Element { return (
@@ -995,6 +1045,17 @@ function CodexSwitcherMenu({ activeAccountId: null }) const [isSwitching, setIsSwitching] = useState(false) + const [reauthenticatingAccountId, setReauthenticatingAccountId] = useState(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 => { - if (isSwitching) { + if (isSwitching || reauthenticatingAccountId !== null) { return } const previousActiveAccountId = getCodexStatusActiveId(accountState, target) @@ -1068,6 +1133,26 @@ function CodexSwitcherMenu({ } } + const handleSignInAccount = async (accountId: string): Promise => { + 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 => { 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 (
@@ -1188,6 +1283,19 @@ function CodexSwitcherMenu({
{inactiveUsage?.isFetching && !inactiveUsage.claude ? ( + ) : showSignInAction ? ( + { + suppressNextAccountSelect() + if (target.id !== null) { + void handleSignInAccount(target.id) + } + }} + /> ) : inactiveUsage?.claude ? (