mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 16:02:35 +00:00
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user