fix(usage): show CLI-owned session recovery steps (#9069)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-07-16 14:12:47 -07:00 committed by GitHub
parent 64181fdd42
commit 4e232a030f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 222 additions and 29 deletions

View File

@ -241,6 +241,13 @@ describe('fetchGrokRateLimits', () => {
const result = await fetchGrokRateLimits()
expect(result.status).toBe('error')
expect(result.error).toMatch(/expired/i)
expect(result.error).toMatch(/run grok on the computer running Orca/i)
expect(result.error).toMatch(/sign in if prompted/i)
expect(result.error).toMatch(/no chat message is needed/i)
expect(result.usageMetadata).toEqual({
failureKind: 'delegated-refresh-required',
source: 'oauth'
})
// Why: a stored-but-expired access token is refreshed by Grok CLI on next
// use (a genuine sign-out returns 'missing'), so the message must not tell
// users to re-run `grok login` (#8497).

View File

@ -1,5 +1,9 @@
import { net } from 'electron'
import type { ProviderRateLimits, RateLimitWindow } from '../../shared/rate-limit-types'
import type {
ProviderRateLimits,
RateLimitWindow,
UsageRateLimitMetadata
} from '../../shared/rate-limit-types'
import {
isGrokAccessTokenFresh,
readGrokAuthSession,
@ -47,14 +51,19 @@ type GrokBillingResponse = GrokBillingConfig & {
config?: GrokBillingConfig
}
function result(status: ProviderRateLimits['status'], error: string | null): ProviderRateLimits {
function result(
status: ProviderRateLimits['status'],
error: string | null,
usageMetadata?: UsageRateLimitMetadata
): ProviderRateLimits {
return {
provider: 'grok',
session: null,
weekly: null,
updatedAt: Date.now(),
error,
status
status,
...(usageMetadata ? { usageMetadata } : {})
}
}
@ -225,7 +234,11 @@ export async function fetchGrokRateLimits(
// Why: a genuine sign-out returns 'missing' earlier, so reaching here always
// means a stored, refreshable session — Grok CLI refreshes the access token
// on its next run, so don't tell users to re-run `grok login` (#8497).
return result('error', 'Grok access token expired — Grok CLI will refresh it on next use')
return result(
'error',
'Grok sign-in expired — run grok on the computer running Orca; sign in if prompted. No chat message is needed.',
{ failureKind: 'delegated-refresh-required', source: 'oauth' }
)
}
try {

View File

@ -140,6 +140,11 @@ describe('fetchKimiRateLimits', () => {
const result = await fetchKimiRateLimits()
expect(result.status).toBe('error')
expect(result.error).toMatch(/expired/i)
expect(result.error).toMatch(/run kimi on the computer running Orca/i)
expect(result.usageMetadata).toEqual({
failureKind: 'delegated-refresh-required',
source: 'oauth'
})
expect(netFetchMock).not.toHaveBeenCalled()
})
})

View File

@ -2,7 +2,11 @@ import { existsSync, readFileSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { net } from 'electron'
import type { ProviderRateLimits, RateLimitWindow } from '../../shared/rate-limit-types'
import type {
ProviderRateLimits,
RateLimitWindow,
UsageRateLimitMetadata
} from '../../shared/rate-limit-types'
// Why: Kimi Code's managed coding plan exposes subscription usage at
// `${base}/usages` (see packages/oauth/src/managed-usage.ts in the CLI bundle).
@ -208,8 +212,20 @@ function mapUsageResponse(data: KimiUsageResponse): ProviderRateLimits {
}
}
function result(status: ProviderRateLimits['status'], error: string | null): ProviderRateLimits {
return { provider: 'kimi', session: null, weekly: null, updatedAt: Date.now(), error, status }
function result(
status: ProviderRateLimits['status'],
error: string | null,
usageMetadata?: UsageRateLimitMetadata
): ProviderRateLimits {
return {
provider: 'kimi',
session: null,
weekly: null,
updatedAt: Date.now(),
error,
status,
...(usageMetadata ? { usageMetadata } : {})
}
}
/**
@ -239,7 +255,11 @@ export async function fetchKimiRateLimits(): Promise<ProviderRateLimits> {
// Why: don't refresh — the CLI owns the token lifecycle. Report a transient
// error so the rate-limit service keeps the last good snapshot (stale
// policy) until the user next runs Kimi and the CLI refreshes the file.
return result('error', 'Kimi token expired — open Kimi to refresh')
return result(
'error',
'Kimi session expired — run kimi on the computer running Orca, then retry usage.',
{ failureKind: 'delegated-refresh-required', source: 'oauth' }
)
}
try {

View File

@ -0,0 +1,69 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import React from 'react'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
getStatus: vi.fn(),
refreshGrokRateLimits: vi.fn()
}))
vi.mock('@/lib/agent-catalog', () => ({
AgentIcon: () => React.createElement('span', { 'data-testid': 'grok-icon' })
}))
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string, values?: Record<string, string>) => {
let result = fallback
for (const [key, value] of Object.entries(values ?? {})) {
result = result.replace(`{{${key}}}`, value)
}
return result
}
}))
vi.mock('../../store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
refreshGrokRateLimits: mocks.refreshGrokRateLimits,
rateLimits: { grok: null }
})
}))
import { GrokAccountsSection } from './GrokAccountsSection'
describe('GrokAccountsSection', () => {
beforeEach(() => {
mocks.getStatus.mockResolvedValue({
signedIn: true,
email: 'dev@example.com',
teamId: null,
tokenFresh: false,
error: null
})
mocks.refreshGrokRateLimits.mockResolvedValue(undefined)
Object.defineProperty(window, 'api', {
configurable: true,
value: { grokAccounts: { getStatus: mocks.getStatus } }
})
})
afterEach(() => {
cleanup()
vi.clearAllMocks()
})
it('explains the host-scoped Grok recovery flow without requiring a chat message', async () => {
render(<GrokAccountsSection />)
expect(
await screen.findByText(
'Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed.'
)
).toBeInTheDocument()
expect(screen.queryByText(/grok login/i)).not.toBeInTheDocument()
})
})

View File

@ -109,12 +109,12 @@ export function GrokAccountsSection(): React.JSX.Element {
<p className="text-xs text-muted-foreground">
{tokenFresh
? translate(
'auto.components.settings.GrokAccountsSection.c3d4e5f6a7',
'Signed in. Orca only reads that file on disk — run grok login again if usage fails.'
'auto.components.settings.GrokAccountsSection.b36fa2c908',
'Signed in. Orca reads the Grok CLI session stored on disk.'
)
: translate(
'auto.components.settings.GrokAccountsSection.d4e5f6a7b8',
'Session expired — run grok login in a terminal to refresh.'
'auto.components.settings.GrokAccountsSection.f08c41de73',
'Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed.'
)}
</p>
</>

View File

@ -135,18 +135,36 @@ describe('provider usage error copy', () => {
)
})
it('keeps the reworded Grok expired-token error classified as an auth failure (#8497)', () => {
// Why: the fix (grok-fetcher.ts) dropped the "run grok login" wording that
// used to trigger auth classification; this pins that the new copy still
// resolves to the softer refresh message instead of leaking the raw string.
it('shows the exact Grok CLI recovery flow for an expired refreshable session (#8497)', () => {
const grok = provider({
provider: 'grok',
error: 'Grok access token expired — Grok CLI will refresh it on next use'
error:
'Grok sign-in expired — run grok on the computer running Orca; sign in if prompted. No chat message is needed.',
usageMetadata: {
failureKind: 'delegated-refresh-required',
source: 'oauth'
}
})
expect(getProviderUsageStatusLabel(grok)).toBe('Refresh failed')
expect(getProviderUsageStatusLabel(grok)).toBe('Run Grok to refresh')
expect(getProviderUsageErrorMessage(grok)).toBe(
'Grok usage could not be refreshed. Agent sessions may still be signed in.'
'Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.'
)
})
it('shows the exact Kimi CLI recovery flow for an expired read-only session', () => {
const kimi = provider({
provider: 'kimi',
error: 'Kimi session expired — run kimi on the computer running Orca, then retry usage.',
usageMetadata: {
failureKind: 'delegated-refresh-required',
source: 'oauth'
}
})
expect(getProviderUsageStatusLabel(kimi)).toBe('Run Kimi to refresh')
expect(getProviderUsageErrorMessage(kimi)).toBe(
'Run kimi in a terminal on the computer running Orca and wait for it to start, then retry usage.'
)
})

View File

@ -64,7 +64,25 @@ function isUsageAuthError(message: string | null): boolean {
return Boolean(message && USAGE_AUTH_ERROR_PATTERNS.some((pattern) => pattern.test(message)))
}
function getDelegatedCliRefreshProvider(
p: ProviderRateLimits
): Extract<ProviderRateLimits['provider'], 'grok' | 'kimi'> | null {
if (p.usageMetadata?.failureKind !== 'delegated-refresh-required') {
return null
}
// Why: only these providers require a user-run CLI to rotate the read-only
// session Orca consumes; Claude handles the same failure kind in-app.
return p.provider === 'grok' || p.provider === 'kimi' ? p.provider : null
}
export function getProviderUsageStatusLabel(p: ProviderRateLimits): string {
const delegatedCliProvider = getDelegatedCliRefreshProvider(p)
if (delegatedCliProvider === 'grok') {
return translate('auto.components.status.bar.tooltip.e2c6a4f917', 'Run Grok to refresh')
}
if (delegatedCliProvider === 'kimi') {
return translate('auto.components.status.bar.tooltip.f90b3d7a16', 'Run Kimi to refresh')
}
if (p.provider === 'claude') {
switch (p.usageMetadata?.failureKind) {
case 'deferred-by-live-session':
@ -107,6 +125,19 @@ export function getProviderUsageErrorMessage(p: ProviderRateLimits): string {
if (!p.error) {
return fallback
}
const delegatedCliProvider = getDelegatedCliRefreshProvider(p)
if (delegatedCliProvider === 'grok') {
return translate(
'auto.components.status.bar.tooltip.d1b7f509ac',
'Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.'
)
}
if (delegatedCliProvider === 'kimi') {
return translate(
'auto.components.status.bar.tooltip.a37e8c15d4',
'Run kimi in a terminal on the computer running Orca and wait for it to start, then retry usage.'
)
}
if (p.provider === 'claude') {
switch (p.usageMetadata?.failureKind) {
case 'deferred-by-live-session':

View File

@ -3224,7 +3224,11 @@
"42fdd4da1d": "Claude sign-in is being refreshed. Agent sessions may still be signed in.",
"c06c1d215d": "Claude usage could not be refreshed because the network request failed.",
"cabdc2a9e0": "Claude sign-in credentials could not be read.",
"a7517cccb6": "Claude usage is unavailable right now."
"a7517cccb6": "Claude usage is unavailable right now.",
"e2c6a4f917": "Run Grok to refresh",
"d1b7f509ac": "Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.",
"f90b3d7a16": "Run Kimi to refresh",
"a37e8c15d4": "Run kimi in a terminal on the computer running Orca and wait for it to start, then retry usage."
},
"SshTargetStatusRow": {
"sshHost": "SSH Host"
@ -9037,7 +9041,9 @@
"b7e2d9f0a3": "Same weekly credit % as the grok /usage screen in the terminal.",
"c6d1a8f4e2": "Resets {{when}}",
"e6dadc1e2b": "Monthly usage",
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
"75e396bf42": "Included monthly usage for Grok unified-billing accounts.",
"b36fa2c908": "Signed in. Orca reads the Grok CLI session stored on disk.",
"f08c41de73": "Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed."
},
"AppearanceWindowSidebarSection": {
"usagePercentageDisplayUsed": "Used",

View File

@ -3224,7 +3224,11 @@
"42fdd4da1d": "Se está actualizando el inicio de sesión de Claude. Las sesiones de agentes pueden seguir iniciadas.",
"c06c1d215d": "No se pudo actualizar el uso de Claude porque falló la solicitud de red.",
"cabdc2a9e0": "No se pudieron leer las credenciales de inicio de sesión de Claude.",
"a7517cccb6": "El uso de Claude no está disponible ahora."
"a7517cccb6": "El uso de Claude no está disponible ahora.",
"e2c6a4f917": "Run Grok to refresh",
"d1b7f509ac": "Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.",
"f90b3d7a16": "Run Kimi to refresh",
"a37e8c15d4": "Run kimi in a terminal on the computer running Orca and wait for it to start, then retry usage."
},
"SshTargetStatusRow": {
"sshHost": "Host SSH"
@ -9037,7 +9041,9 @@
"b7e2d9f0a3": "El mismo porcentaje de créditos semanales que la pantalla grok /usage en la terminal.",
"c6d1a8f4e2": "Se restablece {{when}}",
"e6dadc1e2b": "Monthly usage",
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
"75e396bf42": "Included monthly usage for Grok unified-billing accounts.",
"b36fa2c908": "Signed in. Orca reads the Grok CLI session stored on disk.",
"f08c41de73": "Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed."
},
"AppearanceWindowSidebarSection": {
"usagePercentageDisplayUsed": "Usado",

View File

@ -3224,7 +3224,11 @@
"42fdd4da1d": "Claude サインインを更新中です。エージェントセッションはまだサインイン済みの場合があります。",
"c06c1d215d": "ネットワークリクエストに失敗したため、Claude 使用量を更新できませんでした。",
"cabdc2a9e0": "Claude サインイン認証情報を読み取れませんでした。",
"a7517cccb6": "Claude 使用量は現在利用できません。"
"a7517cccb6": "Claude 使用量は現在利用できません。",
"e2c6a4f917": "Run Grok to refresh",
"d1b7f509ac": "Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.",
"f90b3d7a16": "Run Kimi to refresh",
"a37e8c15d4": "Run kimi in a terminal on the computer running Orca and wait for it to start, then retry usage."
},
"SshTargetStatusRow": {
"sshHost": "SSH ホスト"
@ -9037,7 +9041,9 @@
"b7e2d9f0a3": "ターミナルの grok /usage 画面と同じ週次クレジット率です。",
"c6d1a8f4e2": "{{when}} にリセット",
"e6dadc1e2b": "Monthly usage",
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
"75e396bf42": "Included monthly usage for Grok unified-billing accounts.",
"b36fa2c908": "Signed in. Orca reads the Grok CLI session stored on disk.",
"f08c41de73": "Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed."
},
"AppearanceWindowSidebarSection": {
"usagePercentageDisplayUsed": "使用済み",

View File

@ -3224,7 +3224,11 @@
"42fdd4da1d": "Claude 로그인을 새로 고치는 중입니다. 에이전트 세션은 여전히 로그인되어 있을 수 있습니다.",
"c06c1d215d": "네트워크 요청 실패로 Claude 사용량을 새로 고칠 수 없습니다.",
"cabdc2a9e0": "Claude 로그인 자격 증명을 읽을 수 없습니다.",
"a7517cccb6": "현재 Claude 사용량을 사용할 수 없습니다."
"a7517cccb6": "현재 Claude 사용량을 사용할 수 없습니다.",
"e2c6a4f917": "Run Grok to refresh",
"d1b7f509ac": "Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.",
"f90b3d7a16": "Run Kimi to refresh",
"a37e8c15d4": "Run kimi in a terminal on the computer running Orca and wait for it to start, then retry usage."
},
"SshTargetStatusRow": {
"sshHost": "SSH 호스트"
@ -9037,7 +9041,9 @@
"b7e2d9f0a3": "터미널의 grok /usage 화면과 같은 주간 크레딧 비율입니다.",
"c6d1a8f4e2": "{{when}}에 재설정",
"e6dadc1e2b": "Monthly usage",
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
"75e396bf42": "Included monthly usage for Grok unified-billing accounts.",
"b36fa2c908": "Signed in. Orca reads the Grok CLI session stored on disk.",
"f08c41de73": "Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed."
},
"AppearanceWindowSidebarSection": {
"usagePercentageDisplayUsed": "사용",

View File

@ -3224,7 +3224,11 @@
"42fdd4da1d": "正在刷新 Claude 登录。智能体会话可能仍处于登录状态。",
"c06c1d215d": "由于网络请求失败,无法刷新 Claude 用量。",
"cabdc2a9e0": "无法读取 Claude 登录凭据。",
"a7517cccb6": "Claude 用量目前不可用。"
"a7517cccb6": "Claude 用量目前不可用。",
"e2c6a4f917": "Run Grok to refresh",
"d1b7f509ac": "Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.",
"f90b3d7a16": "Run Kimi to refresh",
"a37e8c15d4": "Run kimi in a terminal on the computer running Orca and wait for it to start, then retry usage."
},
"SshTargetStatusRow": {
"sshHost": "SSH 主机"
@ -9037,7 +9041,9 @@
"b7e2d9f0a3": "与终端中 grok /usage 屏幕显示的每周额度百分比相同。",
"c6d1a8f4e2": "{{when}} 重置",
"e6dadc1e2b": "Monthly usage",
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
"75e396bf42": "Included monthly usage for Grok unified-billing accounts.",
"b36fa2c908": "Signed in. Orca reads the Grok CLI session stored on disk.",
"f08c41de73": "Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed."
},
"AppearanceWindowSidebarSection": {
"usagePercentageDisplayUsed": "已用",