mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 08:02:31 +00:00
Fix Claude usage refresh error copy (#5155)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -35,7 +35,7 @@ import type {
|
||||
RateLimitRuntimeTarget,
|
||||
RateLimitWindow
|
||||
} from '../../../../shared/rate-limit-types'
|
||||
import { ProviderIcon, ProviderPanel, barColor } from './tooltip'
|
||||
import { ProviderIcon, ProviderPanel, barColor, getProviderUsageStatusLabel } from './tooltip'
|
||||
import { ClaudeIcon, GeminiIcon, OpenAIIcon, OpenCodeGoIcon } from './icons'
|
||||
import { AgentIcon } from '@/lib/agent-catalog'
|
||||
import { formatWindowLabel } from '@/lib/window-label-formatter'
|
||||
@@ -1024,7 +1024,7 @@ function ProviderSegment({
|
||||
compact: boolean
|
||||
}): React.JSX.Element {
|
||||
const provider = p?.provider ?? 'claude'
|
||||
const statusLabel = p?.error && /rate limit/i.test(p.error) ? 'Limited' : 'Unavailable'
|
||||
const statusLabel = p ? getProviderUsageStatusLabel(p) : ''
|
||||
|
||||
// Idle / initial load
|
||||
if (!p || p.status === 'idle') {
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
|
||||
import { formatResetCountdown, getWindowSections } from './tooltip'
|
||||
import {
|
||||
formatResetCountdown,
|
||||
getProviderUsageErrorMessage,
|
||||
getProviderUsageStatusLabel,
|
||||
getWindowSections
|
||||
} from './tooltip'
|
||||
|
||||
function provider(overrides: Partial<ProviderRateLimits> = {}): ProviderRateLimits {
|
||||
return {
|
||||
provider: 'claude',
|
||||
session: null,
|
||||
weekly: null,
|
||||
updatedAt: 0,
|
||||
error: null,
|
||||
status: 'error',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('formatResetCountdown', () => {
|
||||
it('uses natural copy when the reset time has arrived', () => {
|
||||
@@ -13,6 +30,101 @@ describe('formatResetCountdown', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('provider usage error copy', () => {
|
||||
it('frames Claude auth-shaped usage failures as usage refresh failures', () => {
|
||||
const p = provider({ error: 'Invalid authentication credentials' })
|
||||
|
||||
expect(getProviderUsageStatusLabel(p)).toBe('Refresh failed')
|
||||
expect(getProviderUsageErrorMessage(p)).toBe(
|
||||
'Claude usage could not be refreshed. Agent sessions may still be signed in.'
|
||||
)
|
||||
})
|
||||
|
||||
it('frames provider credential and session failures without showing raw auth details', () => {
|
||||
const codex = provider({
|
||||
provider: 'codex',
|
||||
error:
|
||||
'Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.'
|
||||
})
|
||||
const gemini = provider({
|
||||
provider: 'gemini',
|
||||
error: 'Gemini CLI credentials not found'
|
||||
})
|
||||
|
||||
expect(getProviderUsageStatusLabel(codex)).toBe('Refresh failed')
|
||||
expect(getProviderUsageErrorMessage(codex)).toBe(
|
||||
'Codex usage could not be refreshed. Agent sessions may still be signed in.'
|
||||
)
|
||||
expect(getProviderUsageErrorMessage(gemini)).toBe(
|
||||
'Gemini usage could not be refreshed. Agent sessions may still be signed in.'
|
||||
)
|
||||
})
|
||||
|
||||
it('frames credential-file and login failures as auth-shaped usage failures', () => {
|
||||
const kimi = provider({
|
||||
provider: 'kimi',
|
||||
error: 'Kimi credentials-file is invalid'
|
||||
})
|
||||
const opencodeGo = provider({
|
||||
provider: 'opencode-go',
|
||||
error: 'Please log in before refreshing usage.'
|
||||
})
|
||||
|
||||
expect(getProviderUsageErrorMessage(kimi)).toBe(
|
||||
'Kimi usage could not be refreshed. Agent sessions may still be signed in.'
|
||||
)
|
||||
expect(getProviderUsageErrorMessage(opencodeGo)).toBe(
|
||||
'OpenCode Go usage could not be refreshed. Agent sessions may still be signed in.'
|
||||
)
|
||||
})
|
||||
|
||||
it('frames known Codex auth refresh failures as auth-shaped usage failures', () => {
|
||||
const cases = [
|
||||
'Please reauthenticate before checking usage.',
|
||||
'Not logged in.',
|
||||
'Token data is not available.',
|
||||
'Auth is missing.',
|
||||
'Auth tokens are missing.',
|
||||
'Auth does not expose access tokens.'
|
||||
]
|
||||
|
||||
for (const error of cases) {
|
||||
expect(getProviderUsageErrorMessage(provider({ provider: 'codex', error }))).toBe(
|
||||
'Codex usage could not be refreshed. Agent sessions may still be signed in.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps rate-limit failures distinct from refresh failures', () => {
|
||||
const p = provider({ error: 'Claude usage is rate limited right now.' })
|
||||
|
||||
expect(getProviderUsageStatusLabel(p)).toBe('Limited')
|
||||
expect(getProviderUsageErrorMessage(p)).toBe('Claude usage is rate limited right now.')
|
||||
})
|
||||
|
||||
it('lets rate-limit copy win when the detail also mentions auth', () => {
|
||||
const p = provider({
|
||||
error: 'Rate limit reached while refreshing OAuth access token.'
|
||||
})
|
||||
|
||||
expect(getProviderUsageStatusLabel(p)).toBe('Limited')
|
||||
expect(getProviderUsageErrorMessage(p)).toBe(
|
||||
'Rate limit reached while refreshing OAuth access token.'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps generic OAuth and network failures as raw refresh details', () => {
|
||||
const oauth = provider({ error: 'OAuth API returned 500' })
|
||||
const network = provider({ error: 'Network error while refreshing OAuth usage: ECONNRESET' })
|
||||
|
||||
expect(getProviderUsageStatusLabel(oauth)).toBe('Refresh failed')
|
||||
expect(getProviderUsageErrorMessage(oauth)).toBe('OAuth API returned 500')
|
||||
expect(getProviderUsageErrorMessage(network)).toBe(
|
||||
'Network error while refreshing OAuth usage: ECONNRESET'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWindowSections', () => {
|
||||
it('returns buckets as sections when present', () => {
|
||||
const p: ProviderRateLimits = {
|
||||
|
||||
@@ -63,12 +63,90 @@ export function ProviderIcon({ provider }: { provider: string }): React.JSX.Elem
|
||||
return <ClaudeIcon size={13} />
|
||||
}
|
||||
|
||||
export function getProviderDisplayName(provider: ProviderRateLimits['provider']): string {
|
||||
if (provider === 'claude') {
|
||||
return 'Claude'
|
||||
}
|
||||
if (provider === 'codex') {
|
||||
return 'Codex'
|
||||
}
|
||||
if (provider === 'gemini') {
|
||||
return 'Gemini'
|
||||
}
|
||||
if (provider === 'opencode-go') {
|
||||
return 'OpenCode Go'
|
||||
}
|
||||
if (provider === 'kimi') {
|
||||
return 'Kimi'
|
||||
}
|
||||
return provider
|
||||
}
|
||||
|
||||
function isUsageRateLimitError(message: string | null): boolean {
|
||||
return Boolean(message && /\brate[- ]?limits?\b|\brate[- ]?limited\b/i.test(message))
|
||||
}
|
||||
|
||||
const USAGE_AUTH_ERROR_PATTERNS = [
|
||||
// Why: "OAuth" can be an upstream route label; only credential/session wording
|
||||
// should hide raw details behind the softer usage-refresh copy.
|
||||
/\binvalid (?:authentication )?credentials?\b/i,
|
||||
/\b(?:no|missing|invalid|expired|stale|unavailable) (?:oauth )?(?:access token|refresh token|token|credentials?|auth(?:entication)? session|auth cookie)\b/i,
|
||||
/\b(?:access token|refresh token|token|credentials?|auth(?:entication)? session|auth cookie) (?:is |are |was |were |could not be |cannot be |can't be )?(?:missing|unavailable|invalid|expired|stale|used|refreshed|loaded|found)\b/i,
|
||||
/\bcredentials?[ -]file (?:is |was )?(?:missing|unavailable|invalid|expired|stale)\b/i,
|
||||
/\b(?:access token|refresh token|token|credentials?|auth(?:entication)? session|auth cookie) not (?:found|available)\b/i,
|
||||
/\b(?:token data|tokens?) (?:is |are )?not available\b/i,
|
||||
/\bauth (?:is missing|tokens are missing|does not expose)\b/i,
|
||||
/\bunauthori[sz]ed\b/i,
|
||||
/\bunauthenticated\b/i,
|
||||
/\bplease reauthenticate\b/i,
|
||||
/\bsign in\b/i,
|
||||
/\blogged in to another account\b/i,
|
||||
/\bnot logged in\b/i,
|
||||
/\blog[ -]?in\b/i,
|
||||
/\blog(?:ged)? out\b/i
|
||||
]
|
||||
|
||||
function isUsageAuthError(message: string | null): boolean {
|
||||
return Boolean(message && USAGE_AUTH_ERROR_PATTERNS.some((pattern) => pattern.test(message)))
|
||||
}
|
||||
|
||||
export function getProviderUsageStatusLabel(p: ProviderRateLimits): string {
|
||||
if (isUsageRateLimitError(p.error)) {
|
||||
return translate('auto.components.status.bar.tooltip.7ad719c4bf', 'Limited')
|
||||
}
|
||||
return translate('auto.components.status.bar.tooltip.e740f92596', 'Refresh failed')
|
||||
}
|
||||
|
||||
export function getProviderUsageErrorMessage(p: ProviderRateLimits): string {
|
||||
const fallback = translate(
|
||||
'auto.components.status.bar.tooltip.2c35eca8d4',
|
||||
'Unable to fetch usage'
|
||||
)
|
||||
if (!p.error) {
|
||||
return fallback
|
||||
}
|
||||
if (isUsageRateLimitError(p.error)) {
|
||||
return p.error
|
||||
}
|
||||
if (isUsageAuthError(p.error)) {
|
||||
const name = getProviderDisplayName(p.provider)
|
||||
return translate(
|
||||
'auto.components.status.bar.tooltip.8418ec448d',
|
||||
'{{value0}} usage could not be refreshed. Agent sessions may still be signed in.',
|
||||
{ value0: name }
|
||||
)
|
||||
}
|
||||
return p.error
|
||||
}
|
||||
|
||||
function ErrorMessage({
|
||||
message,
|
||||
label,
|
||||
stale = false,
|
||||
inverted = false
|
||||
}: {
|
||||
message: string
|
||||
label?: string
|
||||
/** When true, prior data is still visible — show a softer "refresh failed" label. */
|
||||
stale?: boolean
|
||||
inverted?: boolean
|
||||
@@ -84,7 +162,7 @@ function ErrorMessage({
|
||||
'auto.components.status.bar.tooltip.a9a318b7a3',
|
||||
'Refresh failed — showing cached data'
|
||||
)
|
||||
: translate('auto.components.status.bar.tooltip.7567cd1c6b', 'Usage unavailable')}
|
||||
: (label ?? translate('auto.components.status.bar.tooltip.e740f92596', 'Refresh failed'))}
|
||||
</div>
|
||||
<div className={detailClass}>{message}</div>
|
||||
</div>
|
||||
@@ -171,18 +249,7 @@ export function ProviderPanel({
|
||||
)
|
||||
}
|
||||
|
||||
const name =
|
||||
p.provider === 'claude'
|
||||
? 'Claude'
|
||||
: p.provider === 'codex'
|
||||
? 'Codex'
|
||||
: p.provider === 'gemini'
|
||||
? 'Gemini'
|
||||
: p.provider === 'opencode-go'
|
||||
? 'OpenCode Go'
|
||||
: p.provider === 'kimi'
|
||||
? 'Kimi'
|
||||
: p.provider
|
||||
const name = getProviderDisplayName(p.provider)
|
||||
|
||||
if (p.status === 'unavailable') {
|
||||
return (
|
||||
@@ -207,10 +274,8 @@ export function ProviderPanel({
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<ErrorMessage
|
||||
message={
|
||||
p.error ??
|
||||
translate('auto.components.status.bar.tooltip.2c35eca8d4', 'Unable to fetch usage')
|
||||
}
|
||||
label={getProviderUsageStatusLabel(p)}
|
||||
message={getProviderUsageErrorMessage(p)}
|
||||
inverted={inverted}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2692,7 +2692,10 @@
|
||||
"2c35eca8d4": "Unable to fetch usage",
|
||||
"1292d4f2ee": "Unavailable",
|
||||
"7567cd1c6b": "Usage unavailable",
|
||||
"a9a318b7a3": "Refresh failed — showing cached data"
|
||||
"a9a318b7a3": "Refresh failed — showing cached data",
|
||||
"7ad719c4bf": "Limited",
|
||||
"e740f92596": "Refresh failed",
|
||||
"8418ec448d": "{{value0}} usage could not be refreshed. Agent sessions may still be signed in."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2692,7 +2692,10 @@
|
||||
"2c35eca8d4": "No se puede recuperar el uso",
|
||||
"1292d4f2ee": "Indisponible",
|
||||
"7567cd1c6b": "Uso no disponible",
|
||||
"a9a318b7a3": "Error al actualizar: se muestran datos almacenados en caché"
|
||||
"a9a318b7a3": "Error al actualizar: se muestran datos almacenados en caché",
|
||||
"7ad719c4bf": "Limitado",
|
||||
"e740f92596": "Error al actualizar",
|
||||
"8418ec448d": "No se pudo actualizar el uso de {{value0}}. Es posible que las sesiones de agente sigan iniciadas."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2692,7 +2692,10 @@
|
||||
"2c35eca8d4": "使用状況を取得できません",
|
||||
"1292d4f2ee": "利用不可",
|
||||
"7567cd1c6b": "使用不可",
|
||||
"a9a318b7a3": "更新に失敗しました - キャッシュされたデータが表示されています"
|
||||
"a9a318b7a3": "更新に失敗しました - キャッシュされたデータが表示されています",
|
||||
"7ad719c4bf": "制限中",
|
||||
"e740f92596": "更新に失敗しました",
|
||||
"8418ec448d": "{{value0}} の使用状況を更新できませんでした。エージェントセッションは引き続きサインイン済みの場合があります。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2692,7 +2692,10 @@
|
||||
"2c35eca8d4": "사용량을 가져올 수 없습니다.",
|
||||
"1292d4f2ee": "없는",
|
||||
"7567cd1c6b": "이용불가",
|
||||
"a9a318b7a3": "새로 고침 실패 - 캐시된 데이터 표시"
|
||||
"a9a318b7a3": "새로 고침 실패 - 캐시된 데이터 표시",
|
||||
"7ad719c4bf": "제한됨",
|
||||
"e740f92596": "새로 고침 실패",
|
||||
"8418ec448d": "{{value0}} 사용량을 새로 고칠 수 없습니다. 에이전트 세션은 여전히 로그인되어 있을 수 있습니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2692,7 +2692,10 @@
|
||||
"2c35eca8d4": "无法获取使用情况",
|
||||
"1292d4f2ee": "不可用",
|
||||
"7567cd1c6b": "无法使用",
|
||||
"a9a318b7a3": "刷新失败——显示缓存数据"
|
||||
"a9a318b7a3": "刷新失败——显示缓存数据",
|
||||
"7ad719c4bf": "受限",
|
||||
"e740f92596": "刷新失败",
|
||||
"8418ec448d": "无法刷新 {{value0}} 使用情况。代理会话可能仍已登录。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user