feat(rate-limits): Grok CLI OAuth weekly credit usage (#7869)
* feat(rate-limits): Grok CLI OAuth weekly credit usage in status bar Read ~/.grok/auth.json (read-only), fetch billing credits via cli-chat-proxy, and surface Grok in Settings, status bar toggles, and rate-limit polling alongside other usage providers. * fix(grok): clarify comments and address CodeRabbit review - Shorten Why comments per AGENTS.md; fix billing period end fallback. - Share GrokAccountStatus type; hash-based locale keys; reload why in Settings. * docs(grok): plain-language comments and Settings copy * feat(stats): subscription usage section with Grok in Stats & Usage Surface rate-limit weekly credits in Settings > Stats & Usage and link to Accounts for setup. * feat(stats): Grok tab in Usage Analytics dropdown * refactor(stats): drop Subscription usage block; align Grok pane with Codex * fix(grok): align settings copy and visibility tests * fix(grok): add localization catalog entries * fix(grok): avoid eager usage refresh fanout * fix(grok): harden usage refresh visibility * test(grok): cover account status privacy boundary * fix(grok): target refreshes and redact auth errors * fix(grok): hide usage UI for signed-out users and align empty states - Treat a token-less auth.json (e.g. after grok logout) as signed out instead of surfacing a permanent status-bar error. - Map billing responses without credit usage to 'unavailable' so plans with no weekly credits hide the bar like Claude API-key billing. - Gate the grok status-bar item and toggle on CLI PATH detection, matching claude/codex/gemini/kimi. - Guard an empty GROK_CLI_CHAT_PROXY_BASE_URL from producing a relative billing URL. - Drop dead minimax/kimi Stats & Usage search keywords left from the removed subscription section. - Add missing grok search keyword catalog entries and translate the English-stubbed stats keywords in es/ja/ko/zh. * test(ipc): mock grok account registrar in register-core-handlers test --------- Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
This commit is contained in:
parent
770a9d5dc3
commit
fa2b228ad3
|
|
@ -0,0 +1,69 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getGrokAccountStatus } from './status'
|
||||
import { isGrokAccessTokenFresh, readGrokAuthSession } from '../rate-limits/grok-auth'
|
||||
|
||||
vi.mock('../rate-limits/grok-auth', () => ({
|
||||
isGrokAccessTokenFresh: vi.fn(),
|
||||
readGrokAuthSession: vi.fn()
|
||||
}))
|
||||
|
||||
describe('getGrokAccountStatus', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(isGrokAccessTokenFresh).mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('reports unsigned status when the Grok auth file is missing', () => {
|
||||
vi.mocked(readGrokAuthSession).mockReturnValue({ status: 'missing' })
|
||||
|
||||
expect(getGrokAccountStatus()).toEqual({
|
||||
signedIn: false,
|
||||
email: null,
|
||||
teamId: null,
|
||||
tokenFresh: false,
|
||||
error: null
|
||||
})
|
||||
})
|
||||
|
||||
it('reports auth read errors without exposing token fields', () => {
|
||||
vi.mocked(readGrokAuthSession).mockReturnValue({
|
||||
status: 'error',
|
||||
error: 'Grok auth file is invalid'
|
||||
})
|
||||
|
||||
expect(getGrokAccountStatus()).toEqual({
|
||||
signedIn: false,
|
||||
email: null,
|
||||
teamId: null,
|
||||
tokenFresh: false,
|
||||
error: 'Grok auth file is invalid'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns non-secret signed-in metadata and freshness', () => {
|
||||
vi.mocked(readGrokAuthSession).mockReturnValue({
|
||||
status: 'ok',
|
||||
session: {
|
||||
accessToken: 'secret-token',
|
||||
email: 'dev@example.com',
|
||||
teamId: 'team-1',
|
||||
userId: 'user-1',
|
||||
expiresAtMs: null,
|
||||
oidcClientId: 'client-1'
|
||||
}
|
||||
})
|
||||
vi.mocked(isGrokAccessTokenFresh).mockReturnValue(false)
|
||||
|
||||
const status = getGrokAccountStatus()
|
||||
|
||||
expect(status).toEqual({
|
||||
signedIn: true,
|
||||
email: 'dev@example.com',
|
||||
teamId: 'team-1',
|
||||
tokenFresh: false,
|
||||
error: null
|
||||
})
|
||||
expect(JSON.stringify(status)).not.toContain('secret-token')
|
||||
expect(JSON.stringify(status)).not.toContain('client-1')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import type { GrokAccountStatus } from '../../shared/rate-limit-types'
|
||||
import { isGrokAccessTokenFresh, readGrokAuthSession } from '../rate-limits/grok-auth'
|
||||
|
||||
export function getGrokAccountStatus(): GrokAccountStatus {
|
||||
const readResult = readGrokAuthSession()
|
||||
if (readResult.status === 'missing') {
|
||||
return {
|
||||
signedIn: false,
|
||||
email: null,
|
||||
teamId: null,
|
||||
tokenFresh: false,
|
||||
error: null
|
||||
}
|
||||
}
|
||||
if (readResult.status === 'error') {
|
||||
return {
|
||||
signedIn: false,
|
||||
email: null,
|
||||
teamId: null,
|
||||
tokenFresh: false,
|
||||
error: readResult.error
|
||||
}
|
||||
}
|
||||
const session = readResult.session
|
||||
return {
|
||||
signedIn: true,
|
||||
email: session.email,
|
||||
teamId: session.teamId,
|
||||
tokenFresh: isGrokAccessTokenFresh(session),
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { ipcMain } from 'electron'
|
||||
import { getGrokAccountStatus } from '../grok-accounts/status'
|
||||
|
||||
export function registerGrokAccountHandlers(): void {
|
||||
ipcMain.handle('grokAccounts:getStatus', () => getGrokAccountStatus())
|
||||
}
|
||||
|
|
@ -16,11 +16,17 @@ import { registerRateLimitHandlers } from './rate-limits'
|
|||
import type { RateLimitService } from '../rate-limits/service'
|
||||
import type { RateLimitState } from '../../shared/rate-limit-types'
|
||||
|
||||
function makeService(): { service: RateLimitService; refresh: ReturnType<typeof vi.fn> } {
|
||||
function makeService(): {
|
||||
service: RateLimitService
|
||||
refresh: ReturnType<typeof vi.fn>
|
||||
refreshGrok: ReturnType<typeof vi.fn>
|
||||
} {
|
||||
const refresh = vi.fn(() => Promise.resolve({} as RateLimitState))
|
||||
const refreshGrok = vi.fn(() => Promise.resolve({} as RateLimitState))
|
||||
const service = {
|
||||
getState: vi.fn(() => ({}) as RateLimitState),
|
||||
refresh,
|
||||
refreshGrok,
|
||||
refreshCodexForTarget: vi.fn(() => Promise.resolve({} as RateLimitState)),
|
||||
refreshClaudeForTarget: vi.fn(() => Promise.resolve({} as RateLimitState)),
|
||||
consumeCodexRateLimitResetCredit: vi.fn(() =>
|
||||
|
|
@ -30,7 +36,7 @@ function makeService(): { service: RateLimitService; refresh: ReturnType<typeof
|
|||
fetchInactiveClaudeAccountsOnOpen: vi.fn(() => Promise.resolve()),
|
||||
fetchInactiveCodexAccountsOnOpen: vi.fn(() => Promise.resolve())
|
||||
}
|
||||
return { service: service as unknown as RateLimitService, refresh }
|
||||
return { service: service as unknown as RateLimitService, refresh, refreshGrok }
|
||||
}
|
||||
|
||||
describe('registerRateLimitHandlers', () => {
|
||||
|
|
@ -53,5 +59,15 @@ describe('registerRateLimitHandlers', () => {
|
|||
expect(ipcState.handleHandlers.has('rateLimits:get')).toBe(true)
|
||||
expect(ipcState.handleHandlers.has('rateLimits:refresh')).toBe(true)
|
||||
expect(ipcState.handleHandlers.has('rateLimits:refreshMiniMax')).toBe(true)
|
||||
expect(ipcState.handleHandlers.has('rateLimits:refreshGrok')).toBe(true)
|
||||
})
|
||||
|
||||
it('registers a refreshGrok channel that delegates to refreshGrok()', async () => {
|
||||
const { service, refreshGrok } = makeService()
|
||||
registerRateLimitHandlers(service)
|
||||
const handler = ipcState.handleHandlers.get('rateLimits:refreshGrok')
|
||||
expect(handler).toBeDefined()
|
||||
await handler!({})
|
||||
expect(refreshGrok).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ export function registerRateLimitHandlers(rateLimits: RateLimitService): void {
|
|||
rateLimits.fetchInactiveCodexAccountsOnOpen()
|
||||
)
|
||||
ipcMain.handle('rateLimits:refreshMiniMax', () => rateLimits.refresh())
|
||||
ipcMain.handle('rateLimits:refreshGrok', () => rateLimits.refreshGrok())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const {
|
|||
registerAgentTrustHandlersMock,
|
||||
registerClaudeAccountHandlersMock,
|
||||
registerMiniMaxCredentialsHandlersMock,
|
||||
registerGrokAccountHandlersMock,
|
||||
registerClipboardHandlersMock,
|
||||
setTrustedClipboardRendererWebContentsIdMock,
|
||||
registerUpdaterHandlersMock,
|
||||
|
|
@ -97,6 +98,7 @@ const {
|
|||
registerAgentTrustHandlersMock: vi.fn(),
|
||||
registerClaudeAccountHandlersMock: vi.fn(),
|
||||
registerMiniMaxCredentialsHandlersMock: vi.fn(),
|
||||
registerGrokAccountHandlersMock: vi.fn(),
|
||||
registerClipboardHandlersMock: vi.fn(),
|
||||
setTrustedClipboardRendererWebContentsIdMock: vi.fn(),
|
||||
registerUpdaterHandlersMock: vi.fn(),
|
||||
|
|
@ -309,6 +311,10 @@ vi.mock('./minimax-credentials', () => ({
|
|||
registerMiniMaxCredentialsHandlers: registerMiniMaxCredentialsHandlersMock
|
||||
}))
|
||||
|
||||
vi.mock('./grok-accounts', () => ({
|
||||
registerGrokAccountHandlers: registerGrokAccountHandlersMock
|
||||
}))
|
||||
|
||||
vi.mock('../window/attach-main-window-services', () => ({
|
||||
registerUpdaterHandlers: registerUpdaterHandlersMock
|
||||
}))
|
||||
|
|
@ -464,6 +470,7 @@ describe('registerCoreHandlers', () => {
|
|||
expect(registerPetHandlersMock).toHaveBeenCalled()
|
||||
expect(registerClaudeAccountHandlersMock).toHaveBeenCalledWith(claudeAccounts)
|
||||
expect(registerMiniMaxCredentialsHandlersMock).toHaveBeenCalledWith(rateLimits)
|
||||
expect(registerGrokAccountHandlersMock).toHaveBeenCalled()
|
||||
expect(registerRateLimitHandlersMock).toHaveBeenCalledWith(rateLimits)
|
||||
expect(registerGitHubHandlersMock).toHaveBeenCalledWith(store, stats)
|
||||
expect(registerLinearHandlersMock).toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import { registerAgentHookHandlers } from './agent-hooks'
|
|||
import { registerAgentTrustHandlers } from './agent-trust'
|
||||
import { registerClaudeAccountHandlers } from './claude-accounts'
|
||||
import { registerMiniMaxCredentialsHandlers } from './minimax-credentials'
|
||||
import { registerGrokAccountHandlers } from './grok-accounts'
|
||||
import { registerUpdaterHandlers } from '../window/attach-main-window-services'
|
||||
import {
|
||||
registerClipboardHandlers,
|
||||
|
|
@ -125,6 +126,7 @@ export function registerCoreHandlers(
|
|||
registerAgentTrustHandlers()
|
||||
registerClaudeAccountHandlers(claudeAccounts)
|
||||
registerMiniMaxCredentialsHandlers(rateLimits)
|
||||
registerGrokAccountHandlers()
|
||||
registerRateLimitHandlers(rateLimits)
|
||||
registerGitHubHandlers(store, stats)
|
||||
registerGitLabHandlers(store)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('readGrokAuthSession', () => {
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
vi.doUnmock('node:fs')
|
||||
})
|
||||
|
||||
it('redacts filesystem paths from auth read failures', async () => {
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() => {
|
||||
throw new Error(
|
||||
'EACCES: permission denied, open /Users/brennanbenson/private/.grok/auth.json'
|
||||
)
|
||||
})
|
||||
}))
|
||||
const { readGrokAuthSession } = await import('./grok-auth')
|
||||
|
||||
expect(readGrokAuthSession()).toEqual({
|
||||
status: 'error',
|
||||
error: 'Unable to read Grok auth file'
|
||||
})
|
||||
})
|
||||
|
||||
it('treats a token-less auth file as signed out, not an error', async () => {
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() => JSON.stringify({ 'https://auth.x.ai::client': { user_id: 'u1' } }))
|
||||
}))
|
||||
const { readGrokAuthSession } = await import('./grok-auth')
|
||||
|
||||
expect(readGrokAuthSession()).toEqual({ status: 'missing' })
|
||||
})
|
||||
|
||||
it('reports malformed auth JSON without parser details', async () => {
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() => '{')
|
||||
}))
|
||||
const { readGrokAuthSession } = await import('./grok-auth')
|
||||
|
||||
expect(readGrokAuthSession()).toEqual({
|
||||
status: 'error',
|
||||
error: 'Grok auth file is invalid'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
// Why: when GROK_HOME is set, auth.json must be the same path Grok CLI uses.
|
||||
export function getGrokHome(): string {
|
||||
return process.env.GROK_HOME?.trim() || join(homedir(), '.grok')
|
||||
}
|
||||
|
||||
export function getGrokAuthPath(): string {
|
||||
return join(getGrokHome(), 'auth.json')
|
||||
}
|
||||
|
||||
export type GrokAuthSession = {
|
||||
accessToken: string
|
||||
userId: string | null
|
||||
email: string | null
|
||||
teamId: string | null
|
||||
expiresAtMs: number | null
|
||||
oidcClientId: string | null
|
||||
}
|
||||
|
||||
type GrokAuthEntry = {
|
||||
key?: string
|
||||
user_id?: string
|
||||
email?: string
|
||||
team_id?: string
|
||||
expires_at?: string
|
||||
oidc_client_id?: string
|
||||
}
|
||||
|
||||
export type GrokAuthReadResult =
|
||||
| { status: 'missing' }
|
||||
| { status: 'error'; error: string }
|
||||
| { status: 'ok'; session: GrokAuthSession }
|
||||
|
||||
function getGrokAuthReadError(err: unknown): string {
|
||||
if (err instanceof SyntaxError) {
|
||||
return 'Grok auth file is invalid'
|
||||
}
|
||||
// Why: filesystem errors often include the full auth path; renderer/mobile
|
||||
// account state should not expose local usernames or custom GROK_HOME values.
|
||||
return 'Unable to read Grok auth file'
|
||||
}
|
||||
|
||||
function parseAuthEntry(value: unknown): GrokAuthEntry | null {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return null
|
||||
}
|
||||
const entry = value as GrokAuthEntry
|
||||
if (typeof entry.key !== 'string' || entry.key.length === 0) {
|
||||
return null
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
function parseExpiresAtMs(iso: string | undefined): number | null {
|
||||
if (!iso) {
|
||||
return null
|
||||
}
|
||||
const ms = Date.parse(iso)
|
||||
return Number.isFinite(ms) ? ms : null
|
||||
}
|
||||
|
||||
export function readGrokAuthSession(): GrokAuthReadResult {
|
||||
const path = getGrokAuthPath()
|
||||
if (!existsSync(path)) {
|
||||
return { status: 'missing' }
|
||||
}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(readFileSync(path, 'utf-8'))
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
return { status: 'error', error: 'Grok auth file is invalid' }
|
||||
}
|
||||
for (const entry of Object.values(parsed)) {
|
||||
const authEntry = parseAuthEntry(entry)
|
||||
if (!authEntry?.key) {
|
||||
continue
|
||||
}
|
||||
return {
|
||||
status: 'ok',
|
||||
session: {
|
||||
accessToken: authEntry.key,
|
||||
userId: typeof authEntry.user_id === 'string' ? authEntry.user_id : null,
|
||||
email: typeof authEntry.email === 'string' ? authEntry.email : null,
|
||||
teamId: typeof authEntry.team_id === 'string' ? authEntry.team_id : null,
|
||||
expiresAtMs: parseExpiresAtMs(authEntry.expires_at),
|
||||
oidcClientId:
|
||||
typeof authEntry.oidc_client_id === 'string' ? authEntry.oidc_client_id : null
|
||||
}
|
||||
}
|
||||
}
|
||||
// Why: a token-less file (e.g. after grok logout) means signed out, not a
|
||||
// failure — 'error' would keep a status-bar alert visible for that user.
|
||||
return { status: 'missing' }
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: getGrokAuthReadError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function hasGrokAuthSession(): boolean {
|
||||
return readGrokAuthSession().status === 'ok'
|
||||
}
|
||||
|
||||
const TOKEN_SKEW_MS = 5 * 60 * 1000
|
||||
|
||||
export function isGrokAccessTokenFresh(session: GrokAuthSession): boolean {
|
||||
if (session.expiresAtMs === null) {
|
||||
// Why: auth.json may lack expiry; a bad token still surfaces as billing HTTP 401.
|
||||
return true
|
||||
}
|
||||
return session.expiresAtMs - Date.now() > TOKEN_SKEW_MS
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const netFetchMock = vi.hoisted(() => vi.fn())
|
||||
const authState = vi.hoisted<{
|
||||
file: string | null
|
||||
readError: Error | null
|
||||
}>(() => ({ file: null, readError: null }))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
net: { fetch: netFetchMock }
|
||||
}))
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
existsSync: () => authState.file !== null,
|
||||
readFileSync: () => {
|
||||
if (authState.readError) {
|
||||
throw authState.readError
|
||||
}
|
||||
if (authState.file === null) {
|
||||
throw new Error('ENOENT')
|
||||
}
|
||||
return authState.file
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('node:os', () => ({ homedir: () => '/home/test' }))
|
||||
|
||||
import { fetchGrokRateLimits } from './grok-fetcher'
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body
|
||||
} as Response
|
||||
}
|
||||
|
||||
const BILLING_RESPONSE = {
|
||||
config: {
|
||||
creditUsagePercent: 42,
|
||||
currentPeriod: {
|
||||
type: 'USAGE_PERIOD_TYPE_WEEKLY',
|
||||
start: '2026-06-30T18:36:14.268512+00:00',
|
||||
end: '2026-07-07T18:36:14.268512+00:00'
|
||||
},
|
||||
subscriptionTier: 'SuperGrok',
|
||||
isUnifiedBillingUser: true
|
||||
}
|
||||
}
|
||||
|
||||
function freshAuthJson(): string {
|
||||
return JSON.stringify({
|
||||
'https://auth.x.ai::client': {
|
||||
key: 'access-token',
|
||||
user_id: 'user-1',
|
||||
email: 'dev@example.com',
|
||||
expires_at: '2099-01-01T00:00:00.000Z'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('fetchGrokRateLimits', () => {
|
||||
beforeEach(() => {
|
||||
netFetchMock.mockReset()
|
||||
authState.file = null
|
||||
authState.readError = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('returns unavailable when not signed in', async () => {
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.provider).toBe('grok')
|
||||
expect(result.status).toBe('unavailable')
|
||||
expect(netFetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('maps weekly credit usage from billing config', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock.mockResolvedValueOnce(jsonResponse(BILLING_RESPONSE))
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('ok')
|
||||
expect(result.weekly?.usedPercent).toBe(42)
|
||||
expect(result.weekly?.windowMinutes).toBe(10_080)
|
||||
expect(result.usageMetadata?.source).toBe('oauth')
|
||||
expect(result.usageMetadata?.authProvenance).toContain('SuperGrok')
|
||||
|
||||
expect(netFetchMock).toHaveBeenCalledWith(
|
||||
'https://cli-chat-proxy.grok.com/v1/billing?format=credits',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer access-token',
|
||||
'X-XAI-Token-Auth': 'xai-grok-cli',
|
||||
'x-userid': 'user-1'
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('returns unavailable when not signed in even if a token-less auth file exists', async () => {
|
||||
authState.file = JSON.stringify({})
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('unavailable')
|
||||
expect(netFetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns unavailable when billing has no credit usage', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock.mockResolvedValueOnce(jsonResponse({ config: { subscriptionTier: 'Enterprise' } }))
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('unavailable')
|
||||
expect(result.weekly).toBeNull()
|
||||
})
|
||||
|
||||
it('returns unavailable when billing response has no config', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock.mockResolvedValueOnce(jsonResponse({}))
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('unavailable')
|
||||
})
|
||||
|
||||
it('aborts the billing request when the caller aborts', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
const controller = new AbortController()
|
||||
let requestSignal: AbortSignal | undefined
|
||||
netFetchMock.mockImplementationOnce((_url, init: RequestInit) => {
|
||||
requestSignal = init.signal as AbortSignal
|
||||
return new Promise((_resolve, reject) => {
|
||||
requestSignal?.addEventListener('abort', () => reject(new Error('aborted')), {
|
||||
once: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const resultPromise = fetchGrokRateLimits({ signal: controller.signal })
|
||||
await Promise.resolve()
|
||||
|
||||
expect(requestSignal?.aborted).toBe(false)
|
||||
controller.abort()
|
||||
expect(requestSignal?.aborted).toBe(true)
|
||||
|
||||
const result = await resultPromise
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toBe('aborted')
|
||||
})
|
||||
|
||||
it('returns error when the session token is expired', async () => {
|
||||
authState.file = JSON.stringify({
|
||||
'https://auth.x.ai::client': {
|
||||
key: 'stale',
|
||||
expires_at: '2000-01-01T00:00:00.000Z'
|
||||
}
|
||||
})
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toMatch(/expired/i)
|
||||
expect(netFetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import { net } from 'electron'
|
||||
import type { ProviderRateLimits, RateLimitWindow } from '../../shared/rate-limit-types'
|
||||
import {
|
||||
isGrokAccessTokenFresh,
|
||||
readGrokAuthSession,
|
||||
type GrokAuthReadResult,
|
||||
type GrokAuthSession
|
||||
} from './grok-auth'
|
||||
|
||||
// Why: billing URL and headers must match Grok CLI or xAI rejects the request.
|
||||
const GROK_CLI_PROXY_BASE =
|
||||
process.env.GROK_CLI_CHAT_PROXY_BASE_URL?.trim().replace(/\/$/, '') ||
|
||||
'https://cli-chat-proxy.grok.com/v1'
|
||||
const BILLING_CREDITS_URL = `${GROK_CLI_PROXY_BASE}/billing?format=credits`
|
||||
const API_TIMEOUT_MS = 10_000
|
||||
const WEEKLY_WINDOW_MINUTES = 10_080
|
||||
|
||||
const GROK_CLI_AUTH_HEADER = 'xai-grok-cli'
|
||||
|
||||
type GrokMoneyVal = { val?: string | number }
|
||||
|
||||
type GrokUsagePeriod = {
|
||||
type?: string
|
||||
start?: string
|
||||
end?: string
|
||||
}
|
||||
|
||||
type GrokBillingConfig = {
|
||||
creditUsagePercent?: number
|
||||
currentPeriod?: GrokUsagePeriod
|
||||
billingPeriodStart?: string
|
||||
billingPeriodEnd?: string
|
||||
subscriptionTier?: string
|
||||
onDemandCap?: GrokMoneyVal
|
||||
onDemandUsed?: GrokMoneyVal
|
||||
prepaidBalance?: GrokMoneyVal
|
||||
isUnifiedBillingUser?: boolean
|
||||
}
|
||||
|
||||
type GrokBillingResponse = GrokBillingConfig & {
|
||||
config?: GrokBillingConfig
|
||||
}
|
||||
|
||||
function result(status: ProviderRateLimits['status'], error: string | null): ProviderRateLimits {
|
||||
return {
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly: null,
|
||||
updatedAt: Date.now(),
|
||||
error,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
function parseResetDescription(isoString: string | undefined): string | null {
|
||||
if (!isoString) {
|
||||
return null
|
||||
}
|
||||
const date = new Date(isoString)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null
|
||||
}
|
||||
const isToday = date.toDateString() === new Date().toDateString()
|
||||
return isToday
|
||||
? date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
|
||||
: date.toLocaleDateString(undefined, { weekday: 'short', hour: 'numeric', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function mapWeeklyCredits(config: GrokBillingConfig): RateLimitWindow | null {
|
||||
const usedPercent = config.creditUsagePercent
|
||||
if (typeof usedPercent !== 'number' || !Number.isFinite(usedPercent)) {
|
||||
return null
|
||||
}
|
||||
const periodEnd = config.currentPeriod?.end ?? config.billingPeriodEnd
|
||||
const resetsAt = periodEnd ? Date.parse(periodEnd) : null
|
||||
return {
|
||||
usedPercent: Math.min(100, Math.max(0, usedPercent)),
|
||||
windowMinutes: WEEKLY_WINDOW_MINUTES,
|
||||
resetsAt: resetsAt !== null && Number.isFinite(resetsAt) ? resetsAt : null,
|
||||
resetDescription: parseResetDescription(periodEnd)
|
||||
}
|
||||
}
|
||||
|
||||
function grokRequestHeaders(session: GrokAuthSession): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
'X-XAI-Token-Auth': GROK_CLI_AUTH_HEADER,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
if (session.userId) {
|
||||
headers['x-userid'] = session.userId
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
function resolveBillingConfig(data: GrokBillingResponse): GrokBillingConfig | null {
|
||||
if (data.config) {
|
||||
return data.config
|
||||
}
|
||||
if (typeof data.creditUsagePercent === 'number') {
|
||||
return data
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function mapBillingResponse(
|
||||
data: GrokBillingResponse,
|
||||
session: GrokAuthSession
|
||||
): ProviderRateLimits {
|
||||
const config = resolveBillingConfig(data)
|
||||
// Why: a 200 without credit usage means the plan has no weekly credits —
|
||||
// 'unavailable' hides the bar (like Claude on API-key billing); 'error'
|
||||
// would paint a permanent alert for a signed-in account that has no quota.
|
||||
if (!config) {
|
||||
return result('unavailable', 'Grok billing response did not include config')
|
||||
}
|
||||
const weekly = mapWeeklyCredits(config)
|
||||
const tier = config.subscriptionTier?.trim()
|
||||
const authLabel = session.email?.trim() || session.userId || 'Grok account'
|
||||
const provenance = tier ? `${authLabel} (${tier})` : authLabel
|
||||
return {
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly,
|
||||
updatedAt: Date.now(),
|
||||
error: weekly ? null : 'Grok billing response did not include credit usage',
|
||||
status: weekly ? 'ok' : 'unavailable',
|
||||
usageMetadata: {
|
||||
source: 'oauth',
|
||||
authProvenance: provenance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Why: Orca never runs grok login; it only reads the session file the CLI updates.
|
||||
export async function fetchGrokRateLimits(
|
||||
options: { signal?: AbortSignal; authReadResult?: GrokAuthReadResult } = {}
|
||||
): Promise<ProviderRateLimits> {
|
||||
const readResult = options.authReadResult ?? readGrokAuthSession()
|
||||
if (readResult.status === 'missing') {
|
||||
return result('unavailable', 'Not signed in to Grok — run grok login')
|
||||
}
|
||||
if (readResult.status === 'error') {
|
||||
return result('error', readResult.error)
|
||||
}
|
||||
const session = readResult.session
|
||||
if (!isGrokAccessTokenFresh(session)) {
|
||||
return result('error', 'Grok session expired — run grok login to refresh')
|
||||
}
|
||||
|
||||
try {
|
||||
const signal = options.signal
|
||||
? AbortSignal.any([options.signal, AbortSignal.timeout(API_TIMEOUT_MS)])
|
||||
: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||
const res = await net.fetch(BILLING_CREDITS_URL, {
|
||||
headers: grokRequestHeaders(session),
|
||||
signal
|
||||
})
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
return result('error', `Grok usage request unauthorized (HTTP ${res.status})`)
|
||||
}
|
||||
if (!res.ok) {
|
||||
return result('error', `Grok usage request failed (HTTP ${res.status})`)
|
||||
}
|
||||
const data: unknown = await res.json()
|
||||
return mapBillingResponse(
|
||||
typeof data === 'object' && data !== null ? (data as GrokBillingResponse) : {},
|
||||
session
|
||||
)
|
||||
} catch (err) {
|
||||
return result('error', err instanceof Error ? err.message : 'Grok usage request failed')
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,10 @@ import { RateLimitService } from './service'
|
|||
import { fetchClaudeRateLimits, fetchManagedAccountUsage } from './claude-fetcher'
|
||||
import { fetchCodexRateLimits } from './codex-fetcher'
|
||||
import { fetchGeminiRateLimits } from './gemini-usage-fetcher'
|
||||
import { fetchKimiRateLimits } from './kimi-fetcher'
|
||||
import { fetchMiniMaxRateLimits } from './minimax-fetcher'
|
||||
import { fetchGrokRateLimits } from './grok-fetcher'
|
||||
import { readGrokAuthSession } from './grok-auth'
|
||||
import { fetchOpenCodeGoRateLimits } from './opencode-go-usage-fetcher'
|
||||
import { hasMiniMaxSessionCookie } from '../minimax/minimax-cookie-store'
|
||||
|
||||
|
|
@ -30,10 +33,22 @@ vi.mock('./opencode-go-usage-fetcher', () => ({
|
|||
fetchOpenCodeGoRateLimits: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./kimi-fetcher', () => ({
|
||||
fetchKimiRateLimits: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./minimax-fetcher', () => ({
|
||||
fetchMiniMaxRateLimits: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./grok-fetcher', () => ({
|
||||
fetchGrokRateLimits: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./grok-auth', () => ({
|
||||
readGrokAuthSession: vi.fn(() => ({ status: 'missing' }))
|
||||
}))
|
||||
|
||||
vi.mock('../minimax/minimax-cookie-store', () => ({
|
||||
hasMiniMaxSessionCookie: vi.fn(() => false)
|
||||
}))
|
||||
|
|
@ -51,8 +66,14 @@ function deferred<T>(): Deferred<T> {
|
|||
return { promise, resolve }
|
||||
}
|
||||
|
||||
async function flushMicrotasks(times = 4): Promise<void> {
|
||||
for (let i = 0; i < times; i += 1) {
|
||||
await Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
function okProvider(
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'minimax',
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok',
|
||||
usedPercent: number,
|
||||
updatedAt = Date.now()
|
||||
): ProviderRateLimits {
|
||||
|
|
@ -72,7 +93,7 @@ function okProvider(
|
|||
}
|
||||
|
||||
function errorProvider(
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'minimax',
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok',
|
||||
message: string
|
||||
): ProviderRateLimits {
|
||||
return {
|
||||
|
|
@ -126,8 +147,72 @@ describe('RateLimitService', () => {
|
|||
vi.clearAllMocks()
|
||||
vi.mocked(fetchGeminiRateLimits).mockResolvedValue(okProvider('gemini', 0, Date.now()))
|
||||
vi.mocked(fetchOpenCodeGoRateLimits).mockResolvedValue(okProvider('opencode-go', 0, Date.now()))
|
||||
vi.mocked(fetchKimiRateLimits).mockResolvedValue(okProvider('kimi', 0, Date.now()))
|
||||
vi.mocked(fetchMiniMaxRateLimits).mockResolvedValue(okProvider('minimax', 0, Date.now()))
|
||||
vi.mocked(fetchGrokRateLimits).mockResolvedValue({
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly: null,
|
||||
updatedAt: Date.now(),
|
||||
error: null,
|
||||
status: 'unavailable'
|
||||
})
|
||||
vi.mocked(hasMiniMaxSessionCookie).mockReturnValue(false)
|
||||
vi.mocked(readGrokAuthSession).mockReturnValue({ status: 'missing' })
|
||||
})
|
||||
|
||||
it('does not reread Grok auth when callers read state snapshots', () => {
|
||||
vi.mocked(readGrokAuthSession).mockReturnValue({
|
||||
status: 'ok',
|
||||
session: {
|
||||
accessToken: 'token',
|
||||
userId: null,
|
||||
email: null,
|
||||
teamId: null,
|
||||
expiresAtMs: null,
|
||||
oidcClientId: null
|
||||
}
|
||||
})
|
||||
const service = new RateLimitService()
|
||||
vi.mocked(readGrokAuthSession).mockClear()
|
||||
|
||||
expect(service.getState().grokAuthConfigured).toBe(true)
|
||||
service.getState()
|
||||
|
||||
expect(readGrokAuthSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refreshes Grok without refreshing other providers', async () => {
|
||||
const authReadResult = {
|
||||
status: 'ok' as const,
|
||||
session: {
|
||||
accessToken: 'token',
|
||||
userId: null,
|
||||
email: 'dev@example.com',
|
||||
teamId: null,
|
||||
expiresAtMs: null,
|
||||
oidcClientId: null
|
||||
}
|
||||
}
|
||||
vi.mocked(readGrokAuthSession).mockReturnValue(authReadResult)
|
||||
vi.mocked(fetchGrokRateLimits).mockResolvedValueOnce(okProvider('grok', 42))
|
||||
const service = new RateLimitService()
|
||||
|
||||
await service.refreshGrok()
|
||||
|
||||
expect(fetchGrokRateLimits).toHaveBeenCalledTimes(1)
|
||||
expect(fetchGrokRateLimits).toHaveBeenCalledWith({
|
||||
authReadResult,
|
||||
signal: expect.any(AbortSignal)
|
||||
})
|
||||
expect(fetchClaudeRateLimits).not.toHaveBeenCalled()
|
||||
expect(fetchCodexRateLimits).not.toHaveBeenCalled()
|
||||
expect(fetchGeminiRateLimits).not.toHaveBeenCalled()
|
||||
expect(fetchOpenCodeGoRateLimits).not.toHaveBeenCalled()
|
||||
expect(fetchKimiRateLimits).not.toHaveBeenCalled()
|
||||
expect(fetchMiniMaxRateLimits).not.toHaveBeenCalled()
|
||||
expect(service.getState().grokAuthConfigured).toBe(true)
|
||||
expect(service.getState().grok?.status).toBe('ok')
|
||||
})
|
||||
|
||||
it('does not refetch Claude when a Codex account switch is queued during fetchAll', async () => {
|
||||
|
|
@ -345,9 +430,47 @@ describe('RateLimitService', () => {
|
|||
expect(fetchCodexRateLimits).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('publishes non-Grok provider results before a slow Grok fetch completes', async () => {
|
||||
const service = new RateLimitService()
|
||||
const grok = deferred<ProviderRateLimits>()
|
||||
let refreshResolved = false
|
||||
|
||||
vi.mocked(fetchClaudeRateLimits).mockResolvedValueOnce(okProvider('claude', 10, Date.now()))
|
||||
vi.mocked(fetchCodexRateLimits).mockResolvedValueOnce(okProvider('codex', 20, Date.now()))
|
||||
vi.mocked(fetchGeminiRateLimits).mockResolvedValueOnce(okProvider('gemini', 30, Date.now()))
|
||||
vi.mocked(fetchOpenCodeGoRateLimits).mockResolvedValueOnce(
|
||||
okProvider('opencode-go', 40, Date.now())
|
||||
)
|
||||
vi.mocked(fetchKimiRateLimits).mockResolvedValueOnce(okProvider('kimi', 50, Date.now()))
|
||||
vi.mocked(fetchMiniMaxRateLimits).mockResolvedValueOnce(okProvider('minimax', 60, Date.now()))
|
||||
vi.mocked(fetchGrokRateLimits).mockReturnValueOnce(grok.promise)
|
||||
|
||||
const refresh = service.refresh().then(() => {
|
||||
refreshResolved = true
|
||||
})
|
||||
await flushMicrotasks()
|
||||
|
||||
const pendingGrokState = service.getState()
|
||||
expect(pendingGrokState.claude?.status).toBe('ok')
|
||||
expect(pendingGrokState.codex?.status).toBe('ok')
|
||||
expect(pendingGrokState.gemini?.status).toBe('ok')
|
||||
expect(pendingGrokState.opencodeGo?.status).toBe('ok')
|
||||
expect(pendingGrokState.kimi?.status).toBe('ok')
|
||||
expect(pendingGrokState.minimax?.status).toBe('ok')
|
||||
expect(pendingGrokState.grok?.status).toBe('fetching')
|
||||
expect(refreshResolved).toBe(false)
|
||||
|
||||
grok.resolve(okProvider('grok', 70, Date.now()))
|
||||
await refresh
|
||||
|
||||
const completedState = service.getState()
|
||||
expect(completedState.grok?.status).toBe('ok')
|
||||
expect(refreshResolved).toBe(true)
|
||||
})
|
||||
|
||||
it('aborts the active fetch cycle and clears queued refreshes on stop', async () => {
|
||||
const service = new RateLimitService()
|
||||
const capturedSignals: { claude?: AbortSignal; codex?: AbortSignal } = {}
|
||||
const capturedSignals: { claude?: AbortSignal; codex?: AbortSignal; grok?: AbortSignal } = {}
|
||||
|
||||
vi.mocked(fetchClaudeRateLimits).mockImplementation(
|
||||
(options) =>
|
||||
|
|
@ -371,6 +494,17 @@ describe('RateLimitService', () => {
|
|||
)
|
||||
})
|
||||
)
|
||||
vi.mocked(fetchGrokRateLimits).mockImplementation(
|
||||
(options) =>
|
||||
new Promise((resolve) => {
|
||||
capturedSignals.grok = options?.signal
|
||||
options?.signal?.addEventListener(
|
||||
'abort',
|
||||
() => resolve(errorProvider('grok', 'aborted')),
|
||||
{ once: true }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const activeFetch = serviceInternals(service).fetchAll()
|
||||
await Promise.resolve()
|
||||
|
|
@ -383,12 +517,14 @@ describe('RateLimitService', () => {
|
|||
|
||||
expect(capturedSignals.claude?.aborted).toBe(true)
|
||||
expect(capturedSignals.codex?.aborted).toBe(true)
|
||||
expect(capturedSignals.grok?.aborted).toBe(true)
|
||||
|
||||
await queuedRefresh
|
||||
await activeFetch
|
||||
|
||||
expect(fetchClaudeRateLimits).toHaveBeenCalledTimes(1)
|
||||
expect(fetchCodexRateLimits).toHaveBeenCalledTimes(1)
|
||||
expect(fetchGrokRateLimits).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('aborts inactive Claude preview fetches on stop', async () => {
|
||||
|
|
@ -480,6 +616,10 @@ describe('RateLimitService', () => {
|
|||
expect(fetchGeminiRateLimits).toHaveBeenCalledWith(true)
|
||||
expect(fetchOpenCodeGoRateLimits).toHaveBeenCalledTimes(1)
|
||||
expect(fetchOpenCodeGoRateLimits).toHaveBeenCalledWith('session=abc123', undefined)
|
||||
expect(fetchGrokRateLimits).toHaveBeenCalledWith({
|
||||
signal: expect.any(AbortSignal),
|
||||
authReadResult: { status: 'missing' }
|
||||
})
|
||||
|
||||
const state = service.getState()
|
||||
expect(state.claude?.status).toBe('ok')
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import {
|
|||
} from '../claude-accounts/runtime-selection'
|
||||
import { fetchGeminiRateLimits } from './gemini-usage-fetcher'
|
||||
import { fetchKimiRateLimits } from './kimi-fetcher'
|
||||
import { fetchGrokRateLimits } from './grok-fetcher'
|
||||
import { readGrokAuthSession } from './grok-auth'
|
||||
import { hasMiniMaxSessionCookie } from '../minimax/minimax-cookie-store'
|
||||
import { fetchMiniMaxRateLimits } from './minimax-fetcher'
|
||||
import { fetchOpenCodeGoRateLimits } from './opencode-go-usage-fetcher'
|
||||
|
|
@ -78,6 +80,7 @@ type InternalRateLimitState = {
|
|||
opencodeGo: ProviderRateLimits | null
|
||||
kimi: ProviderRateLimits | null
|
||||
minimax: ProviderRateLimits | null
|
||||
grok: ProviderRateLimits | null
|
||||
}
|
||||
|
||||
function normalizePollingInterval(ms: number): number {
|
||||
|
|
@ -110,8 +113,10 @@ export class RateLimitService {
|
|||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
minimax: null
|
||||
minimax: null,
|
||||
grok: null
|
||||
}
|
||||
private grokAuthConfigured = readGrokAuthSession().status === 'ok'
|
||||
private pollInterval: number = DEFAULT_POLL_MS
|
||||
private timer: ReturnType<typeof setInterval> | null = null
|
||||
private deferredStartupRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
|
@ -122,6 +127,7 @@ export class RateLimitService {
|
|||
private fullFetchQueued = false
|
||||
private codexOnlyFetchQueued = false
|
||||
private claudeOnlyFetchQueued = false
|
||||
private grokOnlyFetchQueued = false
|
||||
private activeFetchAbortControllers = new Set<AbortController>()
|
||||
private fetchIdleResolvers: (() => void)[] = []
|
||||
private codexFetchGeneration = 0
|
||||
|
|
@ -269,6 +275,7 @@ export class RateLimitService {
|
|||
// its presence on the pushed state so the renderer keeps the MiniMax
|
||||
// bar visible across reloads and between snapshot refreshes.
|
||||
minimaxCookieConfigured: hasMiniMaxSessionCookie(),
|
||||
grokAuthConfigured: this.grokAuthConfigured,
|
||||
claudeTarget: this.claudeFetchTarget,
|
||||
codexTarget: this.codexFetchTarget,
|
||||
inactiveClaudeAccounts: this.buildInactiveArray(
|
||||
|
|
@ -291,6 +298,11 @@ export class RateLimitService {
|
|||
return this.getState()
|
||||
}
|
||||
|
||||
async refreshGrok(): Promise<RateLimitState> {
|
||||
await this.fetchGrokOnly({ force: true })
|
||||
return this.getState()
|
||||
}
|
||||
|
||||
invalidateMiniMaxCredentialState(): void {
|
||||
this.minimaxFetchGeneration += 1
|
||||
// Why: saving or forgetting the browser cookie can race an in-flight usage
|
||||
|
|
@ -740,6 +752,15 @@ export class RateLimitService {
|
|||
break
|
||||
}
|
||||
}
|
||||
if (this.grokOnlyFetchQueued) {
|
||||
this.grokOnlyFetchQueued = false
|
||||
const grokSignal = await this.runWithFetchAbortSignal((fetchSignal) =>
|
||||
this.runFetchGrokOnlyCycle(fetchSignal)
|
||||
)
|
||||
if (grokSignal.aborted) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isFetching = false
|
||||
|
|
@ -790,6 +811,15 @@ export class RateLimitService {
|
|||
break
|
||||
}
|
||||
}
|
||||
if (this.grokOnlyFetchQueued) {
|
||||
this.grokOnlyFetchQueued = false
|
||||
const grokSignal = await this.runWithFetchAbortSignal((fetchSignal) =>
|
||||
this.runFetchGrokOnlyCycle(fetchSignal)
|
||||
)
|
||||
if (grokSignal.aborted) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isFetching = false
|
||||
|
|
@ -840,6 +870,74 @@ export class RateLimitService {
|
|||
break
|
||||
}
|
||||
}
|
||||
if (this.grokOnlyFetchQueued) {
|
||||
this.grokOnlyFetchQueued = false
|
||||
const grokSignal = await this.runWithFetchAbortSignal((fetchSignal) =>
|
||||
this.runFetchGrokOnlyCycle(fetchSignal)
|
||||
)
|
||||
if (grokSignal.aborted) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isFetching = false
|
||||
this.resolveFetchIdleWaiters()
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchGrokOnly(options?: { force?: boolean }): Promise<void> {
|
||||
if (this.isFetching) {
|
||||
if (options?.force) {
|
||||
this.grokOnlyFetchQueued = true
|
||||
return this.waitForFetchIdle()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.isFetching = true
|
||||
|
||||
try {
|
||||
let shouldContinue = true
|
||||
while (shouldContinue) {
|
||||
const signal = await this.runWithFetchAbortSignal((fetchSignal) =>
|
||||
this.runFetchGrokOnlyCycle(fetchSignal)
|
||||
)
|
||||
shouldContinue = false
|
||||
if (signal.aborted) {
|
||||
break
|
||||
}
|
||||
if (this.fullFetchQueued) {
|
||||
this.fullFetchQueued = false
|
||||
const fullSignal = await this.runWithFetchAbortSignal((fetchSignal) =>
|
||||
this.runFetchAllCycle(fetchSignal)
|
||||
)
|
||||
if (fullSignal.aborted) {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (this.grokOnlyFetchQueued) {
|
||||
this.grokOnlyFetchQueued = false
|
||||
shouldContinue = true
|
||||
}
|
||||
if (this.codexOnlyFetchQueued) {
|
||||
this.codexOnlyFetchQueued = false
|
||||
const codexSignal = await this.runWithFetchAbortSignal((fetchSignal) =>
|
||||
this.runFetchCodexOnlyCycle(fetchSignal)
|
||||
)
|
||||
if (codexSignal.aborted) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.claudeOnlyFetchQueued) {
|
||||
this.claudeOnlyFetchQueued = false
|
||||
const claudeSignal = await this.runWithFetchAbortSignal((fetchSignal) =>
|
||||
this.runFetchClaudeOnlyCycle(fetchSignal)
|
||||
)
|
||||
if (claudeSignal.aborted) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isFetching = false
|
||||
|
|
@ -852,7 +950,8 @@ export class RateLimitService {
|
|||
!this.isFetching &&
|
||||
!this.fullFetchQueued &&
|
||||
!this.codexOnlyFetchQueued &&
|
||||
!this.claudeOnlyFetchQueued
|
||||
!this.claudeOnlyFetchQueued &&
|
||||
!this.grokOnlyFetchQueued
|
||||
) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
|
@ -869,7 +968,8 @@ export class RateLimitService {
|
|||
this.isFetching ||
|
||||
this.fullFetchQueued ||
|
||||
this.codexOnlyFetchQueued ||
|
||||
this.claudeOnlyFetchQueued
|
||||
this.claudeOnlyFetchQueued ||
|
||||
this.grokOnlyFetchQueued
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
@ -913,6 +1013,7 @@ export class RateLimitService {
|
|||
this.fullFetchQueued = false
|
||||
this.codexOnlyFetchQueued = false
|
||||
this.claudeOnlyFetchQueued = false
|
||||
this.grokOnlyFetchQueued = false
|
||||
}
|
||||
|
||||
private resolveAndClearFetchIdleWaiters(): void {
|
||||
|
|
@ -1025,7 +1126,7 @@ export class RateLimitService {
|
|||
|
||||
private withFetchingStatus(
|
||||
current: ProviderRateLimits | null,
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax'
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok'
|
||||
): ProviderRateLimits {
|
||||
if (!current) {
|
||||
return {
|
||||
|
|
@ -1064,6 +1165,10 @@ export class RateLimitService {
|
|||
const miniMaxGroupId = miniMaxConfigResult.config.groupId
|
||||
const miniMaxModels = miniMaxConfigResult.config.models
|
||||
const geminiCliOAuthEnabled = this.geminiCliOAuthEnabledResolver?.() ?? false
|
||||
// Why: getState() is used by renderer pushes and mobile snapshots; keep
|
||||
// Grok's sync auth-file probe on fetch cycles instead of every state read.
|
||||
const grokAuthReadResult = readGrokAuthSession()
|
||||
this.grokAuthConfigured = grokAuthReadResult.status === 'ok'
|
||||
|
||||
// Detect if configuration changed — if it did, we must discard any stale
|
||||
// data because it belongs to a different session/workspace.
|
||||
|
|
@ -1097,12 +1202,21 @@ export class RateLimitService {
|
|||
kimi: this.withFetchingStatus(previousState.kimi, 'kimi'),
|
||||
minimax: miniMaxConfigChanged
|
||||
? this.withFetchingStatus(null, 'minimax')
|
||||
: this.withFetchingStatus(previousState.minimax, 'minimax')
|
||||
: this.withFetchingStatus(previousState.minimax, 'minimax'),
|
||||
grok: this.withFetchingStatus(previousState.grok, 'grok')
|
||||
})
|
||||
|
||||
const missingWslCodexHome = codexHomePath
|
||||
? null
|
||||
: this.getMissingWslCodexHomeResult(codexTarget)
|
||||
const grokResultPromise = fetchGrokRateLimits({
|
||||
signal,
|
||||
authReadResult: grokAuthReadResult
|
||||
}).then(
|
||||
(value) => ({ status: 'fulfilled', value }) as const,
|
||||
(reason) => ({ status: 'rejected', reason }) as const
|
||||
)
|
||||
|
||||
const [claudeResult, codexResult, geminiResult, opencodeGoResult, kimiResult, miniMaxResult] =
|
||||
await Promise.allSettled([
|
||||
fetchClaudeRateLimits({
|
||||
|
|
@ -1237,7 +1351,7 @@ export class RateLimitService {
|
|||
// generation still match, otherwise an old account could overwrite the
|
||||
// newly selected account's quota state.
|
||||
this.updateState({
|
||||
...previousState,
|
||||
...this.state,
|
||||
claude: shouldApplyClaude
|
||||
? this.applyStalePolicy(claude, previousState.claude)
|
||||
: this.state.claude,
|
||||
|
|
@ -1258,6 +1372,26 @@ export class RateLimitService {
|
|||
: this.state.minimax
|
||||
})
|
||||
|
||||
const grokResult = await grokResultPromise
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
const grok =
|
||||
grokResult.status === 'fulfilled'
|
||||
? grokResult.value
|
||||
: ({
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly: null,
|
||||
updatedAt: Date.now(),
|
||||
error: grokResult.reason instanceof Error ? grokResult.reason.message : 'Unknown error',
|
||||
status: 'error'
|
||||
} satisfies ProviderRateLimits)
|
||||
this.updateState({
|
||||
...this.state,
|
||||
grok: this.applyStalePolicy(grok, previousState.grok)
|
||||
})
|
||||
|
||||
this.lastFetchAt = Date.now()
|
||||
}
|
||||
|
||||
|
|
@ -1374,6 +1508,45 @@ export class RateLimitService {
|
|||
this.lastFetchAt = Date.now()
|
||||
}
|
||||
|
||||
private async runFetchGrokOnlyCycle(signal: AbortSignal): Promise<void> {
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
const previousState = this.state
|
||||
const grokAuthReadResult = readGrokAuthSession()
|
||||
this.grokAuthConfigured = grokAuthReadResult.status === 'ok'
|
||||
|
||||
this.updateState({
|
||||
...previousState,
|
||||
grok: this.withFetchingStatus(previousState.grok, 'grok')
|
||||
})
|
||||
|
||||
const grok = await fetchGrokRateLimits({
|
||||
signal,
|
||||
authReadResult: grokAuthReadResult
|
||||
}).catch(
|
||||
(err): ProviderRateLimits => ({
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly: null,
|
||||
updatedAt: Date.now(),
|
||||
error: err instanceof Error ? err.message : 'Unknown error',
|
||||
status: 'error'
|
||||
})
|
||||
)
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
this.updateState({
|
||||
...this.state,
|
||||
grok: this.applyStalePolicy(grok, previousState.grok)
|
||||
})
|
||||
|
||||
this.lastFetchAt = Date.now()
|
||||
}
|
||||
|
||||
private applyStalePolicy(
|
||||
fresh: ProviderRateLimits,
|
||||
previous: ProviderRateLimits | null
|
||||
|
|
|
|||
|
|
@ -6859,11 +6859,11 @@ export class OrcaRuntimeService {
|
|||
// `rateLimits:update` IPC channel desktop already uses.
|
||||
onAccountsChanged(listener: (snapshot: AccountsSnapshot) => void): () => void {
|
||||
const services = this.requireAccountServices()
|
||||
return services.rateLimits.onStateChange(() => {
|
||||
return services.rateLimits.onStateChange((rateLimits) => {
|
||||
listener({
|
||||
claude: services.claudeAccounts.listAccounts(),
|
||||
codex: services.codexAccounts.listAccounts(),
|
||||
rateLimits: services.rateLimits.getState()
|
||||
rateLimits
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,10 +182,11 @@ describe('client UI RPC methods', () => {
|
|||
...getDefaultUIState(),
|
||||
worktreeCardProperties: ['status', 'branch', 'automation', 'inline-agents'],
|
||||
_worktreeCardModeDefaulted: true,
|
||||
statusBarItems: ['codex', 'kimi', 'minimax', 'ports'],
|
||||
statusBarItems: ['codex', 'kimi', 'minimax', 'grok', 'ports'],
|
||||
_portsStatusBarDefaultAdded: true,
|
||||
_kimiStatusBarDefaultAdded: true,
|
||||
_minimaxStatusBarDefaultAdded: true,
|
||||
_grokStatusBarDefaultAdded: true,
|
||||
taskResumeState: {
|
||||
githubMode: 'items',
|
||||
githubItemsQuery: 'is:open',
|
||||
|
|
@ -221,10 +222,11 @@ describe('client UI RPC methods', () => {
|
|||
const payload = {
|
||||
worktreeCardProperties: ['status', 'branch', 'automation', 'inline-agents'],
|
||||
_worktreeCardModeDefaulted: true,
|
||||
statusBarItems: ['codex', 'kimi', 'minimax', 'ports'],
|
||||
statusBarItems: ['codex', 'kimi', 'minimax', 'grok', 'ports'],
|
||||
_portsStatusBarDefaultAdded: true,
|
||||
_kimiStatusBarDefaultAdded: true,
|
||||
_minimaxStatusBarDefaultAdded: true,
|
||||
_grokStatusBarDefaultAdded: true,
|
||||
taskResumeState: {
|
||||
githubMode: 'items',
|
||||
githubItemsQuery: 'is:open',
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const StatusBarItem = z.enum([
|
|||
'opencode-go',
|
||||
'kimi',
|
||||
'minimax',
|
||||
'grok',
|
||||
'ssh',
|
||||
'resource-usage',
|
||||
'ports'
|
||||
|
|
@ -203,6 +204,7 @@ const UiUpdate = z
|
|||
_portsStatusBarDefaultAdded: z.boolean().optional(),
|
||||
_kimiStatusBarDefaultAdded: z.boolean().optional(),
|
||||
_minimaxStatusBarDefaultAdded: z.boolean().optional(),
|
||||
_grokStatusBarDefaultAdded: z.boolean().optional(),
|
||||
statusBarVisible: z.boolean().optional(),
|
||||
dismissedUpdateVersion: NullableString.optional(),
|
||||
lastUpdateCheckAt: z.number().finite().nullable().optional(),
|
||||
|
|
|
|||
|
|
@ -352,6 +352,7 @@ import type {
|
|||
} from '../shared/claude-usage-types'
|
||||
import type {
|
||||
CodexRateLimitResetResult,
|
||||
GrokAccountStatus,
|
||||
RateLimitRuntimeTarget,
|
||||
RateLimitState
|
||||
} from '../shared/rate-limit-types'
|
||||
|
|
@ -2876,6 +2877,7 @@ export type PreloadApi = {
|
|||
fetchInactiveClaudeAccounts: () => Promise<void>
|
||||
fetchInactiveCodexAccounts: () => Promise<void>
|
||||
refreshMiniMax: () => Promise<RateLimitState>
|
||||
refreshGrok: () => Promise<RateLimitState>
|
||||
onUpdate: (callback: (state: RateLimitState) => void) => () => void
|
||||
}
|
||||
minimaxCredentials: {
|
||||
|
|
@ -2883,6 +2885,9 @@ export type PreloadApi = {
|
|||
saveCookie: (cookie: string) => Promise<{ configured: boolean }>
|
||||
clearCookie: () => Promise<{ configured: boolean }>
|
||||
}
|
||||
grokAccounts: {
|
||||
getStatus: () => Promise<GrokAccountStatus>
|
||||
}
|
||||
ssh: {
|
||||
listTargets: () => Promise<SshTarget[]>
|
||||
// Removed-target id → last known label, for showing a friendly host name on
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ import type {
|
|||
} from '../shared/mobile-markdown-document'
|
||||
import type {
|
||||
CodexRateLimitResetResult,
|
||||
GrokAccountStatus,
|
||||
RateLimitRuntimeTarget,
|
||||
RateLimitState
|
||||
} from '../shared/rate-limit-types'
|
||||
|
|
@ -3851,6 +3852,7 @@ const api = {
|
|||
fetchInactiveCodexAccounts: (): Promise<void> =>
|
||||
ipcRenderer.invoke('rateLimits:fetchInactiveCodexAccounts'),
|
||||
refreshMiniMax: (): Promise<RateLimitState> => ipcRenderer.invoke('rateLimits:refreshMiniMax'),
|
||||
refreshGrok: (): Promise<RateLimitState> => ipcRenderer.invoke('rateLimits:refreshGrok'),
|
||||
onUpdate: (callback: (state: RateLimitState) => void): (() => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, state: RateLimitState) => callback(state)
|
||||
ipcRenderer.on('rateLimits:update', listener)
|
||||
|
|
@ -3867,6 +3869,10 @@ const api = {
|
|||
ipcRenderer.invoke('minimaxCredentials:clearCookie')
|
||||
},
|
||||
|
||||
grokAccounts: {
|
||||
getStatus: (): Promise<GrokAccountStatus> => ipcRenderer.invoke('grokAccounts:getStatus')
|
||||
},
|
||||
|
||||
ssh: {
|
||||
listTargets: (): Promise<SshTarget[]> => ipcRenderer.invoke('ssh:listTargets'),
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,12 @@ import {
|
|||
getAccountsCodexSearchEntries,
|
||||
getAccountsGeminiSearchEntries,
|
||||
getAccountsLocationSearchEntries,
|
||||
getAccountsGrokSearchEntries,
|
||||
getAccountsMiniMaxSearchEntries,
|
||||
getAccountsOpencodeSearchEntries,
|
||||
getAccountsPaneSearchEntries
|
||||
} from './accounts-search'
|
||||
import { GrokAccountsSection } from './GrokAccountsSection'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { SettingsRow, SettingsSegmentedControl } from './SettingsFormControls'
|
||||
import { matchesSettingsSearch } from './settings-search'
|
||||
|
|
@ -1796,6 +1798,9 @@ export function AccountsPane({
|
|||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, getAccountsGrokSearchEntries()) ? (
|
||||
<GrokAccountsSection key="grok" />
|
||||
) : null
|
||||
].filter(Boolean)
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ function recordStatusBarToggleInteraction(
|
|||
id === 'gemini' ||
|
||||
id === 'opencode-go' ||
|
||||
id === 'kimi' ||
|
||||
id === 'minimax'
|
||||
id === 'minimax' ||
|
||||
id === 'grok'
|
||||
) {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ExternalLink, Loader2, RefreshCw, ShieldCheck } from 'lucide-react'
|
||||
import { AgentIcon } from '@/lib/agent-catalog'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAppStore } from '../../store'
|
||||
import { Badge } from '../ui/badge'
|
||||
import { Button } from '../ui/button'
|
||||
import type { GrokAccountStatus } from '../../../../shared/rate-limit-types'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
const GROK_CLI_DOCS_URL = 'https://docs.x.ai/build/overview'
|
||||
|
||||
export function GrokAccountsSection(): React.JSX.Element {
|
||||
const refreshGrokRateLimits = useAppStore((s) => s.refreshGrokRateLimits)
|
||||
const grokUsage = useAppStore((s) => s.rateLimits.grok)
|
||||
const [status, setStatus] = useState<GrokAccountStatus | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const loadStatus = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const next = await window.api.grokAccounts.getStatus()
|
||||
setStatus(next)
|
||||
} catch (error) {
|
||||
console.error('Failed to load Grok account status:', error)
|
||||
setStatus({
|
||||
signedIn: false,
|
||||
email: null,
|
||||
teamId: null,
|
||||
tokenFresh: false,
|
||||
error: error instanceof Error ? error.message : 'Unable to read Grok sign-in'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Why: after a background usage fetch, sign-in state may change — reload status then.
|
||||
useEffect(() => {
|
||||
void loadStatus()
|
||||
}, [loadStatus, grokUsage?.updatedAt])
|
||||
|
||||
const handleRefreshUsage = async (): Promise<void> => {
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await refreshGrokRateLimits()
|
||||
await loadStatus()
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const signedIn = status?.signedIn === true
|
||||
const tokenFresh = status?.tokenFresh === true
|
||||
|
||||
return (
|
||||
<section id="accounts-grok" className="space-y-4 scroll-mt-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
||||
<AgentIcon agent="grok" size={16} />
|
||||
{translate('auto.components.settings.GrokAccountsSection.a1b2c3d4e5', 'Grok (xAI)')}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.GrokAccountsSection.f6e5d4c3b2',
|
||||
'Shows weekly credit usage from your Grok CLI sign-in (session file ~/.grok/auth.json).'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={GROK_CLI_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{translate('auto.components.settings.GrokAccountsSection.0d8e77bc40', 'Grok CLI docs')}
|
||||
<ExternalLink className="size-3" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-3 rounded-lg border bg-muted/20 p-3',
|
||||
signedIn && tokenFresh ? 'border-border/60' : 'border-border/40'
|
||||
)}
|
||||
>
|
||||
<ShieldCheck
|
||||
className={cn(
|
||||
'mt-0.5 size-4 shrink-0',
|
||||
signedIn && tokenFresh ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
{loading ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate('auto.components.settings.GrokAccountsSection.ad47a33f72', 'Loading…')}
|
||||
</p>
|
||||
) : signedIn ? (
|
||||
<>
|
||||
<p className="truncate text-xs font-medium">
|
||||
{status?.email ??
|
||||
translate('auto.components.settings.GrokAccountsSection.b2c3d4e5f6', 'Signed in')}
|
||||
</p>
|
||||
<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.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.GrokAccountsSection.d4e5f6a7b8',
|
||||
'Session expired — run grok login in a terminal to refresh.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-xs font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.GrokAccountsSection.e5f6a7b8c9',
|
||||
'Not signed in to Grok CLI'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.GrokAccountsSection.f6a7b8c9d0',
|
||||
'In a terminal, run grok login, then click Refresh usage here.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{status?.error ? <p className="text-xs text-destructive">{status.error}</p> : null}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
disabled={refreshing}
|
||||
onClick={() => void handleRefreshUsage()}
|
||||
className="shrink-0 gap-1"
|
||||
>
|
||||
{refreshing ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="size-3" />
|
||||
)}
|
||||
{translate('auto.components.settings.GrokAccountsSection.3325d996cb', 'Refresh usage')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{grokUsage?.weekly ? (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.GrokAccountsSection.a8f3e2c1b4',
|
||||
'Weekly credits'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.GrokAccountsSection.b7e2d9f0a3',
|
||||
'Same weekly credit % as the grok /usage screen in the terminal.'
|
||||
)}
|
||||
keywords={['grok', 'xai', 'usage', 'credits', 'oauth']}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{Math.round(grokUsage.weekly.usedPercent)}%
|
||||
</Badge>
|
||||
{grokUsage.weekly.resetDescription ? (
|
||||
<span className="text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.GrokAccountsSection.c6d1a8f4e2',
|
||||
'Resets {{when}}',
|
||||
{ when: grokUsage.weekly.resetDescription }
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{grokUsage.usageMetadata?.authProvenance ? (
|
||||
<span className="truncate text-muted-foreground">
|
||||
{grokUsage.usageMetadata.authProvenance}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -1490,7 +1490,7 @@ function Settings(): React.JSX.Element {
|
|||
title={translate('auto.components.settings.Settings.954a8f5aef', 'Stats & Usage')}
|
||||
description={translate(
|
||||
'auto.components.settings.Settings.8acf3f22e0',
|
||||
'Orca stats plus Claude, Codex, and OpenCode usage analytics.'
|
||||
'Orca stats plus Claude, Codex, OpenCode token analytics and Grok subscription usage.'
|
||||
)}
|
||||
searchEntries={getSectionSearchEntries('stats')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -191,11 +191,33 @@ export const getAccountsMiniMaxSearchEntries = createLocalizedCatalog(() => [
|
|||
}
|
||||
])
|
||||
|
||||
export const getAccountsGrokSearchEntries = createLocalizedCatalog(() => [
|
||||
{
|
||||
title: translate('auto.components.settings.accounts.search.f4a8c2e1b7', 'Grok (xAI) Usage'),
|
||||
description: translate(
|
||||
'auto.components.settings.accounts.search.e3b7d1f9a2',
|
||||
'OAuth sign-in via Grok CLI (grok login) for weekly credit usage.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.accounts.search.d2c6a0e8f1', 'grok'),
|
||||
...translateSearchKeyword('auto.components.settings.accounts.search.c1b5f9d7e0', 'xai'),
|
||||
...translateSearchKeyword('auto.components.settings.accounts.search.b0a4e8c6d9', 'oauth'),
|
||||
...translateSearchKeyword('auto.components.settings.accounts.search.a9f3d7b5c8', 'login'),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.accounts.search.e949b08ffb',
|
||||
'rate limit'
|
||||
),
|
||||
...translateSearchKeyword('auto.components.settings.accounts.search.86edc96bc9', 'status bar')
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
export const getAccountsPaneSearchEntries = createLocalizedCatalog((): SettingsSearchEntry[] => [
|
||||
...getAccountsLocationSearchEntries(),
|
||||
...getAccountsClaudeSearchEntries(),
|
||||
...getAccountsCodexSearchEntries(),
|
||||
...getAccountsGeminiSearchEntries(),
|
||||
...getAccountsOpencodeSearchEntries(),
|
||||
...getAccountsMiniMaxSearchEntries()
|
||||
...getAccountsMiniMaxSearchEntries(),
|
||||
...getAccountsGrokSearchEntries()
|
||||
])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import type { StatusBarItem } from '../../../../shared/types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { translateSearchKeyword } from './settings-search-keywords'
|
||||
|
||||
export function getGrokStatusBarToggleSearchEntry(): {
|
||||
id: StatusBarItem
|
||||
title: string
|
||||
description: string
|
||||
keywords: string[]
|
||||
toggleDescription: string
|
||||
} {
|
||||
return {
|
||||
id: 'grok',
|
||||
title: translate('auto.components.settings.appearance.search.f8e2a1c4b6', 'Grok Usage'),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.e7d1b0f3a5',
|
||||
'Show Grok weekly credit usage from Grok CLI OAuth.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.896eb53fd4',
|
||||
'status bar'
|
||||
),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.d6c0a9e2f4', 'grok'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.c5b9f8d1e3', 'xai'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.00a028f25f', 'usage'),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.de586def95',
|
||||
'subscription'
|
||||
)
|
||||
],
|
||||
toggleDescription: translate(
|
||||
'settings.appearance.statusBar.grokToggleDescription',
|
||||
'Show Grok subscription credit usage when signed in via Grok CLI.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import type { StatusBarItem } from '../../../../shared/types'
|
|||
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { translateSearchKeyword } from './settings-search-keywords'
|
||||
import { getGrokStatusBarToggleSearchEntry } from './appearance-status-bar-grok-toggle-search'
|
||||
|
||||
export const getStatusBarToggles = createLocalizedCatalog(
|
||||
(): readonly {
|
||||
|
|
@ -196,6 +197,7 @@ export const getStatusBarToggles = createLocalizedCatalog(
|
|||
'Show MiniMax subscription usage for the active workspace.'
|
||||
)
|
||||
},
|
||||
getGrokStatusBarToggleSearchEntry(),
|
||||
{
|
||||
id: 'ssh',
|
||||
title: translate('auto.components.settings.appearance.search.57fb424c56', 'Remote Hosts'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
import React from 'react'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '../../store'
|
||||
|
||||
const storeMocks = vi.hoisted(() => ({
|
||||
refreshGrokRateLimits: vi.fn(),
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn(),
|
||||
recordFeatureInteraction: vi.fn()
|
||||
}))
|
||||
|
||||
const mockStoreState = {
|
||||
rateLimits: {
|
||||
claude: null,
|
||||
codex: null,
|
||||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
minimax: null,
|
||||
grok: {
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly: {
|
||||
usedPercent: 42,
|
||||
windowMinutes: 10_080,
|
||||
resetsAt: null,
|
||||
resetDescription: 'Tue'
|
||||
},
|
||||
updatedAt: 1,
|
||||
error: null,
|
||||
status: 'ok'
|
||||
},
|
||||
minimaxCookieConfigured: false,
|
||||
grokAuthConfigured: true,
|
||||
claudeTarget: { runtime: 'host', wslDistro: null },
|
||||
codexTarget: { runtime: 'host', wslDistro: null },
|
||||
inactiveClaudeAccounts: [],
|
||||
inactiveCodexAccounts: []
|
||||
},
|
||||
refreshGrokRateLimits: storeMocks.refreshGrokRateLimits,
|
||||
openSettingsPage: storeMocks.openSettingsPage,
|
||||
openSettingsTarget: storeMocks.openSettingsTarget,
|
||||
recordFeatureInteraction: storeMocks.recordFeatureInteraction
|
||||
} satisfies Partial<AppState>
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useAppStore: Object.assign(
|
||||
(selector: (state: Partial<AppState>) => unknown) => selector(mockStoreState),
|
||||
{
|
||||
getState: () => mockStoreState
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('../ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
translate: (_key: string, fallback: string, values?: Record<string, string>) =>
|
||||
values
|
||||
? Object.entries(values).reduce(
|
||||
(text, [token, value]) => text.replace(`{{${token}}}`, value),
|
||||
fallback
|
||||
)
|
||||
: fallback
|
||||
}))
|
||||
|
||||
import { GrokUsagePane } from './GrokUsagePane'
|
||||
|
||||
describe('GrokUsagePane', () => {
|
||||
beforeEach(() => {
|
||||
storeMocks.refreshGrokRateLimits.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('does not refresh all providers just from opening the Grok tab', () => {
|
||||
render(<GrokUsagePane />)
|
||||
|
||||
expect(screen.getByTestId('grok-usage-pane')).toBeInTheDocument()
|
||||
expect(storeMocks.refreshGrokRateLimits).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refreshes usage only from the explicit refresh button', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<GrokUsagePane />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Refresh Grok usage' }))
|
||||
|
||||
expect(storeMocks.refreshGrokRateLimits).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { CalendarClock, ExternalLink, RefreshCw, Sparkles } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '../../store'
|
||||
import { Button } from '../ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||
import { StatCard } from './StatCard'
|
||||
import { formatUpdatedAt } from './usage-formatters'
|
||||
|
||||
export function GrokUsagePane(): React.JSX.Element {
|
||||
const grok = useAppStore((s) => s.rateLimits.grok)
|
||||
const grokAuthConfigured = useAppStore((s) => s.rateLimits.grokAuthConfigured)
|
||||
const refreshGrokRateLimits = useAppStore((s) => s.refreshGrokRateLimits)
|
||||
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
|
||||
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
|
||||
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
|
||||
|
||||
const openGrokAccounts = (): void => {
|
||||
openSettingsTarget({ pane: 'accounts', repoId: null, sectionId: 'accounts-grok' })
|
||||
openSettingsPage()
|
||||
}
|
||||
|
||||
const paneTitle = translate('auto.components.stats.GrokUsagePane.g8h9i0j1k2', 'Grok usage')
|
||||
|
||||
if (!grokAuthConfigured) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border border-border/60 bg-card/40 p-4"
|
||||
data-testid="grok-usage-pane"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">{paneTitle}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.stats.GrokUsagePane.b2d3e4f5c6',
|
||||
'Weekly subscription credits from Grok CLI OAuth (~/.grok/auth.json). Same source as the status bar.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
openGrokAccounts()
|
||||
}}
|
||||
>
|
||||
{translate('auto.components.stats.GrokUsagePane.c3e4f5a6b7', 'Set up in Accounts')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const weeklyPercent =
|
||||
grok?.weekly && typeof grok.weekly.usedPercent === 'number'
|
||||
? Math.round(grok.weekly.usedPercent)
|
||||
: null
|
||||
const isFetching = grok?.status === 'fetching'
|
||||
|
||||
return (
|
||||
<div
|
||||
className="space-y-4 rounded-lg border border-border/60 bg-card/30 p-4"
|
||||
data-testid="grok-usage-pane"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-semibold text-foreground">{paneTitle}</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{formatUpdatedAt(grok?.updatedAt ?? null)}
|
||||
{grok?.error
|
||||
? translate('auto.components.stats.GrokUsagePane.h9i0j1k2l3', ' • {{value0}}', {
|
||||
value0: grok.error
|
||||
})
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2 self-start">
|
||||
<TooltipProvider delayDuration={250}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => void refreshGrokRateLimits()}
|
||||
disabled={isFetching}
|
||||
aria-label={translate(
|
||||
'auto.components.stats.GrokUsagePane.i0j1k2l3m4',
|
||||
'Refresh Grok usage'
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={`size-3.5 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{translate('auto.components.stats.GrokUsagePane.d4f5a6b7c8', 'Refresh')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<StatCard
|
||||
label={translate('auto.components.stats.GrokUsagePane.e5a6b7c8d9', 'Weekly credits used')}
|
||||
value={weeklyPercent !== null ? `${weeklyPercent}%` : '—'}
|
||||
icon={<Sparkles className="size-4" />}
|
||||
/>
|
||||
<StatCard
|
||||
label={translate(
|
||||
'auto.components.stats.GrokUsagePane.f6b7c8d9e0',
|
||||
'Billing period reset'
|
||||
)}
|
||||
value={grok?.weekly?.resetDescription ?? '—'}
|
||||
icon={<CalendarClock className="size-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{grok?.usageMetadata?.authProvenance ? (
|
||||
<p className="px-1 text-xs text-muted-foreground">{grok.usageMetadata.authProvenance}</p>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 px-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto gap-1 px-0 text-xs"
|
||||
onClick={openGrokAccounts}
|
||||
>
|
||||
{translate('auto.components.stats.GrokUsagePane.a7b8c9d0e1', 'Grok account settings')}
|
||||
<ExternalLink className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { useAppStore } from '../../store'
|
|||
import { StatCard } from './StatCard'
|
||||
import { ClaudeUsagePane } from './ClaudeUsagePane'
|
||||
import { CodexUsagePane } from './CodexUsagePane'
|
||||
import { GrokUsagePane } from './GrokUsagePane'
|
||||
import { OpenCodeUsagePane } from './OpenCodeUsagePane'
|
||||
import { UsageOverviewPane } from './UsageOverviewPane'
|
||||
import { Button } from '../ui/button'
|
||||
|
|
@ -45,7 +46,7 @@ function formatTrackingSince(timestamp: number | null): string {
|
|||
return `Tracking since ${date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}`
|
||||
}
|
||||
|
||||
type UsageTab = 'overview' | 'claude' | 'codex' | 'opencode'
|
||||
type UsageTab = 'overview' | 'claude' | 'codex' | 'opencode' | 'grok'
|
||||
|
||||
const USAGE_ANALYTICS_OPTIONS = [
|
||||
{
|
||||
|
|
@ -71,6 +72,12 @@ const USAGE_ANALYTICS_OPTIONS = [
|
|||
get label() {
|
||||
return translate('auto.components.stats.StatsPane.1e696db2f6', 'OpenCode')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'grok',
|
||||
get label() {
|
||||
return translate('auto.components.stats.StatsPane.grokUsageTab', 'Grok')
|
||||
}
|
||||
}
|
||||
] as const satisfies readonly { id: UsageTab; label: string }[]
|
||||
|
||||
|
|
@ -193,8 +200,10 @@ export function StatsPane(): React.JSX.Element {
|
|||
<ClaudeUsagePane />
|
||||
) : activeUsageTab === 'codex' ? (
|
||||
<CodexUsagePane />
|
||||
) : (
|
||||
) : activeUsageTab === 'opencode' ? (
|
||||
<OpenCodeUsagePane />
|
||||
) : (
|
||||
<GrokUsagePane />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const getStatsPaneSearchEntries = createLocalizedCatalog(() => [
|
|||
title: translate('auto.components.stats.stats.search.cb2430ae6a', 'Stats & Usage'),
|
||||
description: translate(
|
||||
'auto.components.stats.stats.search.26bb901fcd',
|
||||
'Orca stats plus combined Claude, Codex, and OpenCode usage analytics, tokens, cache, models, and sessions.'
|
||||
'Orca stats plus Claude, Codex, OpenCode token analytics and Grok subscription usage.'
|
||||
),
|
||||
keywords: [
|
||||
translate('auto.components.stats.stats.search.372debfac0', 'stats'),
|
||||
|
|
@ -20,7 +20,12 @@ export const getStatsPaneSearchEntries = createLocalizedCatalog(() => [
|
|||
translate('auto.components.stats.stats.search.b77826fca3', 'codex'),
|
||||
translate('auto.components.stats.stats.search.6953af58e6', 'opencode'),
|
||||
translate('auto.components.stats.stats.search.eaf251e183', 'tokens'),
|
||||
translate('auto.components.stats.stats.search.cb6a9f0334', 'cache')
|
||||
translate('auto.components.stats.stats.search.cb6a9f0334', 'cache'),
|
||||
translate('auto.components.stats.stats.search.f8a1b2c3d4', 'grok'),
|
||||
translate('auto.components.stats.stats.search.e7f0a1b2c3', 'subscription'),
|
||||
translate('auto.components.stats.stats.search.d6e9f0a1b2', 'credits'),
|
||||
translate('auto.components.stats.stats.search.a3b6c7d8e9', 'grok usage'),
|
||||
translate('auto.components.stats.stats.search.9f2a3b4c5d', 'xai')
|
||||
]
|
||||
}
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1762,7 +1762,9 @@ export function ProviderDetailsMenu({
|
|||
? 'K'
|
||||
: provider.provider === 'minimax'
|
||||
? 'M'
|
||||
: 'X'}
|
||||
: provider.provider === 'grok'
|
||||
? 'R'
|
||||
: 'X'}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -1908,7 +1910,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
return null
|
||||
}
|
||||
|
||||
const { claude, codex, gemini, opencodeGo, kimi, minimax } = rateLimits
|
||||
const { claude, codex, gemini, opencodeGo, kimi, minimax, grok } = rateLimits
|
||||
|
||||
// Why: a provider earns a bar from either a usable live snapshot or durable
|
||||
// setup in Settings. The durable path keeps account switchers visible while
|
||||
|
|
@ -1917,12 +1919,17 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
// bars when the agent isn't installed on PATH.
|
||||
// Why: thread the cookie durability flag from RateLimitState so the
|
||||
// MiniMax bar stays visible after a reload and between snapshot refreshes.
|
||||
const usageSettings = { ...settings, minimaxCookieConfigured: rateLimits.minimaxCookieConfigured }
|
||||
const usageSettings = {
|
||||
...settings,
|
||||
minimaxCookieConfigured: rateLimits.minimaxCookieConfigured,
|
||||
grokAuthConfigured: rateLimits.grokAuthConfigured
|
||||
}
|
||||
const visibleClaude = getVisibleUsageProvider('claude', claude, usageSettings)
|
||||
const visibleCodex = getVisibleUsageProvider('codex', codex, usageSettings)
|
||||
const visibleGemini = getVisibleUsageProvider('gemini', gemini, usageSettings)
|
||||
const visibleKimi = getVisibleUsageProvider('kimi', kimi, usageSettings)
|
||||
const visibleMiniMax = getVisibleUsageProvider('minimax', minimax, usageSettings)
|
||||
const visibleGrok = getVisibleUsageProvider('grok', grok, usageSettings)
|
||||
const showClaude =
|
||||
visibleClaude !== null &&
|
||||
statusBarItems.includes('claude') &&
|
||||
|
|
@ -1942,6 +1949,10 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
// Why: MiniMax is a cookie-auth provider, not a CLI on PATH, so detection-gating
|
||||
// doesn't apply (same rationale as OpenCode Go below).
|
||||
const showMiniMax = visibleMiniMax !== null && statusBarItems.includes('minimax')
|
||||
const showGrok =
|
||||
visibleGrok !== null &&
|
||||
statusBarItems.includes('grok') &&
|
||||
isStatusBarItemAvailable('grok', detectedAgentIds)
|
||||
// Why: OpenCode Go is a web/cookie-auth provider, not a CLI on PATH, so
|
||||
// detection-gating doesn't apply.
|
||||
const visibleOpencodeGo = getVisibleUsageProvider('opencode-go', opencodeGo, usageSettings)
|
||||
|
|
@ -1958,13 +1969,14 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
showOpencodeGo ||
|
||||
showKimi ||
|
||||
showMiniMax ||
|
||||
showGrok ||
|
||||
showResourceUsage
|
||||
// Why: a brand-new user with no provider configured would otherwise see an
|
||||
// empty left side of the status bar and wonder what's missing. Settings are
|
||||
// included because managed accounts are durable even when live usage
|
||||
// snapshots are still hydrating or unavailable after an update.
|
||||
const isEmptyUsageState = isUsageEmptyState(
|
||||
{ claude, codex, gemini, opencodeGo, kimi, minimax },
|
||||
{ claude, codex, gemini, opencodeGo, kimi, minimax, grok },
|
||||
usageSettings
|
||||
)
|
||||
// Why: the teaching CTA is a one-time nudge — once the user hides it, keep it
|
||||
|
|
@ -1976,7 +1988,8 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
gemini?.status === 'fetching' ||
|
||||
opencodeGo?.status === 'fetching' ||
|
||||
kimi?.status === 'fetching' ||
|
||||
minimax?.status === 'fetching'
|
||||
minimax?.status === 'fetching' ||
|
||||
grok?.status === 'fetching'
|
||||
|
||||
const compact = containerWidth < 900
|
||||
const iconOnly = containerWidth < 500
|
||||
|
|
@ -2065,6 +2078,17 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
)}
|
||||
/>
|
||||
)}
|
||||
{showGrok && (
|
||||
<ProviderDetailsMenu
|
||||
provider={visibleGrok}
|
||||
compact={compact}
|
||||
iconOnly={iconOnly}
|
||||
ariaLabel={translate(
|
||||
'auto.components.status.bar.StatusBar.grokUsageAria',
|
||||
'Open Grok usage details'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{anyVisible && !isEmptyUsageState && (
|
||||
|
|
@ -2218,6 +2242,18 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
<MiniMaxIcon size={14} />
|
||||
{translate('auto.components.status.bar.StatusBar.3bbf140864', 'MiniMax Usage')}
|
||||
</DropdownMenuCheckboxItem>
|
||||
{isStatusBarItemAvailable('grok', detectedAgentIds) && (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={statusBarItems.includes('grok')}
|
||||
onCheckedChange={() => {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
toggleStatusBarItem('grok')
|
||||
}}
|
||||
>
|
||||
<AgentIcon agent="grok" size={14} />
|
||||
{translate('auto.components.status.bar.StatusBar.grokUsageMenu', 'Grok Usage')}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={statusBarItems.includes('ssh')}
|
||||
onCheckedChange={() => {
|
||||
|
|
|
|||
|
|
@ -18,17 +18,20 @@ describe('isStatusBarItemAvailable', () => {
|
|||
expect(isStatusBarItemAvailable('claude', null)).toBe(true)
|
||||
expect(isStatusBarItemAvailable('codex', null)).toBe(true)
|
||||
expect(isStatusBarItemAvailable('gemini', null)).toBe(true)
|
||||
expect(isStatusBarItemAvailable('grok', null)).toBe(true)
|
||||
})
|
||||
|
||||
it('hides CLI items not detected on PATH', () => {
|
||||
expect(isStatusBarItemAvailable('claude', [])).toBe(false)
|
||||
expect(isStatusBarItemAvailable('codex', ['claude'])).toBe(false)
|
||||
expect(isStatusBarItemAvailable('gemini', ['claude', 'codex'])).toBe(false)
|
||||
expect(isStatusBarItemAvailable('grok', ['claude', 'kimi'])).toBe(false)
|
||||
})
|
||||
|
||||
it('shows CLI items detected on PATH', () => {
|
||||
expect(isStatusBarItemAvailable('claude', ['claude'])).toBe(true)
|
||||
expect(isStatusBarItemAvailable('codex', ['codex', 'claude'])).toBe(true)
|
||||
expect(isStatusBarItemAvailable('gemini', ['gemini'])).toBe(true)
|
||||
expect(isStatusBarItemAvailable('grok', ['grok'])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ import type { StatusBarItem, TuiAgent } from '../../../../shared/types'
|
|||
// PATH detection reports the agent as missing. Pre-detection (null) keeps
|
||||
// the legacy behavior so the bar/toggle don't flicker on cold start, and
|
||||
// re-show automatically once the agent appears on PATH.
|
||||
const CLI_GATED_ITEMS: ReadonlySet<StatusBarItem> = new Set(['claude', 'codex', 'gemini', 'kimi'])
|
||||
const CLI_GATED_ITEMS: ReadonlySet<StatusBarItem> = new Set([
|
||||
'claude',
|
||||
'codex',
|
||||
'gemini',
|
||||
'kimi',
|
||||
'grok'
|
||||
])
|
||||
|
||||
export function isStatusBarItemAvailable(
|
||||
id: StatusBarItem,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ function usageSettings(overrides: Partial<UsageProviderSettings> = {}): UsagePro
|
|||
opencodeSessionCookie: '',
|
||||
geminiCliOAuthEnabled: false,
|
||||
minimaxCookieConfigured: false,
|
||||
grokAuthConfigured: false,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
|
@ -119,6 +120,7 @@ describe('hasUsageProviderSettings', () => {
|
|||
hasUsageProviderSettings(usageSettings({ opencodeSessionCookie: ' session=abc ' }))
|
||||
).toBe(true)
|
||||
expect(hasUsageProviderSettings(usageSettings({ minimaxCookieConfigured: true }))).toBe(true)
|
||||
expect(hasUsageProviderSettings(usageSettings({ grokAuthConfigured: true }))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not treat empty or unloaded settings as configured', () => {
|
||||
|
|
@ -160,6 +162,14 @@ describe('hasUsageProviderSettingsForProvider', () => {
|
|||
expect(hasUsageProviderSettingsForProvider('minimax', usageSettings())).toBe(false)
|
||||
expect(hasUsageProviderSettingsForProvider('minimax', null)).toBe(false)
|
||||
})
|
||||
|
||||
it('treats grokAuthConfigured as the durable signal for Grok', () => {
|
||||
expect(
|
||||
hasUsageProviderSettingsForProvider('grok', usageSettings({ grokAuthConfigured: true }))
|
||||
).toBe(true)
|
||||
expect(hasUsageProviderSettingsForProvider('grok', usageSettings())).toBe(false)
|
||||
expect(hasUsageProviderSettingsForProvider('grok', null)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getVisibleUsageProvider', () => {
|
||||
|
|
@ -235,6 +245,20 @@ describe('getVisibleUsageProvider', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('keeps Grok visible while the snapshot is pending when CLI auth is configured', () => {
|
||||
const visible = getVisibleUsageProvider(
|
||||
'grok',
|
||||
null,
|
||||
usageSettings({ grokAuthConfigured: true })
|
||||
)
|
||||
expect(visible).toMatchObject({
|
||||
provider: 'grok',
|
||||
status: 'fetching',
|
||||
session: null,
|
||||
weekly: null
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps MiniMax visible when the fetch returns unavailable for a configured cookie', () => {
|
||||
const unavailable = provider('unavailable', {
|
||||
provider: 'minimax',
|
||||
|
|
@ -271,7 +295,8 @@ describe('isUsageEmptyState', () => {
|
|||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
minimax: null
|
||||
minimax: null,
|
||||
grok: null
|
||||
},
|
||||
usageSettings()
|
||||
)
|
||||
|
|
@ -287,7 +312,8 @@ describe('isUsageEmptyState', () => {
|
|||
gemini: provider('unavailable'),
|
||||
opencodeGo: provider('unavailable', { provider: 'opencode-go' }),
|
||||
kimi: provider('unavailable', { provider: 'kimi' }),
|
||||
minimax: provider('unavailable', { provider: 'minimax' })
|
||||
minimax: provider('unavailable', { provider: 'minimax' }),
|
||||
grok: provider('unavailable', { provider: 'grok' })
|
||||
},
|
||||
usageSettings()
|
||||
)
|
||||
|
|
@ -303,7 +329,8 @@ describe('isUsageEmptyState', () => {
|
|||
gemini: provider('unavailable'),
|
||||
opencodeGo: provider('unavailable', { provider: 'opencode-go' }),
|
||||
kimi: provider('unavailable', { provider: 'kimi' }),
|
||||
minimax: provider('unavailable', { provider: 'minimax' })
|
||||
minimax: provider('unavailable', { provider: 'minimax' }),
|
||||
grok: provider('unavailable', { provider: 'grok' })
|
||||
},
|
||||
usageSettings({
|
||||
codexManagedAccounts: [
|
||||
|
|
@ -330,7 +357,8 @@ describe('isUsageEmptyState', () => {
|
|||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
minimax: null
|
||||
minimax: null,
|
||||
grok: null
|
||||
},
|
||||
null
|
||||
)
|
||||
|
|
@ -346,7 +374,8 @@ describe('isUsageEmptyState', () => {
|
|||
gemini: provider('unavailable'),
|
||||
opencodeGo: provider('unavailable', { provider: 'opencode-go' }),
|
||||
kimi: provider('unavailable', { provider: 'kimi' }),
|
||||
minimax: provider('unavailable', { provider: 'minimax' })
|
||||
minimax: provider('unavailable', { provider: 'minimax' }),
|
||||
grok: provider('unavailable', { provider: 'grok' })
|
||||
},
|
||||
usageSettings()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ export type UsageProviderSettings = Pick<
|
|||
| 'opencodeSessionCookie'
|
||||
| 'geminiCliOAuthEnabled'
|
||||
> & {
|
||||
// Why: the MiniMax cookie lives in the file system, not GlobalSettings, so
|
||||
// we can't derive durability from settings alone. The renderer threads the
|
||||
// flag from RateLimitState (pushed by the main process) so the bar stays
|
||||
// visible across reloads and between snapshot refreshes.
|
||||
// Why: MiniMax/Grok sign-in live on disk, not in settings; main sets these each poll.
|
||||
minimaxCookieConfigured: boolean
|
||||
grokAuthConfigured: boolean
|
||||
}
|
||||
|
||||
type UsageProviderSnapshots = {
|
||||
|
|
@ -22,6 +20,7 @@ type UsageProviderSnapshots = {
|
|||
opencodeGo: ProviderRateLimits | null
|
||||
kimi: ProviderRateLimits | null
|
||||
minimax: ProviderRateLimits | null
|
||||
grok: ProviderRateLimits | null
|
||||
}
|
||||
|
||||
type UsageProviderId = ProviderRateLimits['provider']
|
||||
|
|
@ -66,7 +65,8 @@ export function hasUsageProviderSettings(
|
|||
(settings?.claudeManagedAccounts?.length ?? 0) > 0 ||
|
||||
settings?.geminiCliOAuthEnabled === true ||
|
||||
Boolean(settings?.opencodeSessionCookie?.trim()) ||
|
||||
settings?.minimaxCookieConfigured === true
|
||||
settings?.minimaxCookieConfigured === true ||
|
||||
settings?.grokAuthConfigured === true
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +92,9 @@ export function hasUsageProviderSettingsForProvider(
|
|||
if (providerId === 'minimax') {
|
||||
return settings.minimaxCookieConfigured === true
|
||||
}
|
||||
if (providerId === 'grok') {
|
||||
return settings.grokAuthConfigured === true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +143,8 @@ export function isUsageEmptyState(
|
|||
isProviderSnapshotPending(providers.gemini) ||
|
||||
isProviderSnapshotPending(providers.opencodeGo) ||
|
||||
isProviderSnapshotPending(providers.kimi) ||
|
||||
isProviderSnapshotPending(providers.minimax)
|
||||
isProviderSnapshotPending(providers.minimax) ||
|
||||
isProviderSnapshotPending(providers.grok)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -151,6 +155,7 @@ export function isUsageEmptyState(
|
|||
!isProviderConfigured(providers.gemini) &&
|
||||
!isProviderConfigured(providers.opencodeGo) &&
|
||||
!isProviderConfigured(providers.kimi) &&
|
||||
!isProviderConfigured(providers.minimax)
|
||||
!isProviderConfigured(providers.minimax) &&
|
||||
!isProviderConfigured(providers.grok)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ export function ProviderIcon({ provider }: { provider: string }): React.JSX.Elem
|
|||
if (provider === 'minimax') {
|
||||
return <MiniMaxIcon size={13} />
|
||||
}
|
||||
if (provider === 'grok') {
|
||||
return <AgentIcon agent="grok" size={13} />
|
||||
}
|
||||
return <ClaudeIcon size={13} />
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ export function getProviderDisplayName(provider: ProviderRateLimits['provider'])
|
|||
if (provider === 'minimax') {
|
||||
return 'MiniMax'
|
||||
}
|
||||
if (provider === 'grok') {
|
||||
return 'Grok'
|
||||
}
|
||||
return provider
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export function buildSettingsNavigationMetadata({
|
|||
),
|
||||
description: translate(
|
||||
'auto.hooks.useSettingsNavigationMetadata.b1c2f8b0ac',
|
||||
'Optional account switching for Claude, Codex, Gemini, and OpenCode Go.'
|
||||
'Optional account switching and usage setup for Claude, Codex, Gemini, OpenCode Go, MiniMax, and Grok.'
|
||||
),
|
||||
icon: UserCog,
|
||||
searchEntries: getAccountsPaneSearchEntries(),
|
||||
|
|
@ -424,7 +424,7 @@ export function buildSettingsNavigationMetadata({
|
|||
title: translate('auto.hooks.useSettingsNavigationMetadata.d72a58b5b9', 'Stats & Usage'),
|
||||
description: translate(
|
||||
'auto.hooks.useSettingsNavigationMetadata.b351014180',
|
||||
'Orca stats plus Claude, Codex, and OpenCode usage analytics.'
|
||||
'Orca stats plus Claude, Codex, OpenCode token analytics and Grok subscription usage.'
|
||||
),
|
||||
icon: BarChart3,
|
||||
searchEntries: getStatsPaneSearchEntries(),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
"minimaxToggleDescription": "Show MiniMax subscription usage for the active workspace.",
|
||||
"sshToggleDescription": "Show configured SSH and remote Orca hosts when any are available.",
|
||||
"resourceUsageToggleDescription": "Show the Resource Manager. Click it for CPU, memory, sessions, daemon controls, and workspace disk scans.",
|
||||
"portsToggleDescription": "Show live workspace ports. Click it for workspace-scoped ports and external listeners."
|
||||
"portsToggleDescription": "Show live workspace ports. Click it for workspace-scoped ports and external listeners.",
|
||||
"grokToggleDescription": "Show Grok subscription credit usage when signed in via Grok CLI."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -635,7 +636,7 @@
|
|||
"94a5afe910": "SSH Hosts",
|
||||
"40d80bad8a": "Beta",
|
||||
"de0c2907a1": "Remote Orca Servers",
|
||||
"b351014180": "Orca stats plus Claude, Codex, and OpenCode usage analytics.",
|
||||
"b351014180": "Orca stats plus Claude, Codex, OpenCode token analytics and Grok subscription usage.",
|
||||
"d72a58b5b9": "Stats & Usage",
|
||||
"dcd0d9b74f": "Keyboard shortcuts for common actions.",
|
||||
"94295ebfb3": "Shortcuts",
|
||||
|
|
@ -676,7 +677,7 @@
|
|||
"cd50cec5d7": "Coordinate multiple coding agents through Orca.",
|
||||
"58a868e8e4": "Orchestration",
|
||||
"7c79d3b7bf": "Optional",
|
||||
"b1c2f8b0ac": "Optional account switching for Claude, Codex, Gemini, and OpenCode Go.",
|
||||
"b1c2f8b0ac": "Optional account switching and usage setup for Claude, Codex, Gemini, OpenCode Go, MiniMax, and Grok.",
|
||||
"f70ac54d38": "AI Provider Accounts",
|
||||
"4121f7a0a2": "Manage AI agents, set a default, and customize commands.",
|
||||
"b49abbd2f7": "Agents",
|
||||
|
|
@ -3032,7 +3033,9 @@
|
|||
"c0e972d726": "Cancel",
|
||||
"06741a2f3d": "Open MiniMax usage details",
|
||||
"3bbf140864": "MiniMax Usage",
|
||||
"remoteServerLabel": "Remote server"
|
||||
"remoteServerLabel": "Remote server",
|
||||
"grokUsageAria": "Open Grok usage details",
|
||||
"grokUsageMenu": "Grok Usage"
|
||||
},
|
||||
"StatusBarUsageEmptyCta": {
|
||||
"828c764a79": "Connect an account",
|
||||
|
|
@ -3380,7 +3383,8 @@
|
|||
"b2cf4310ce": "Overview",
|
||||
"908c470587": "codex",
|
||||
"eb6a066185": "claude",
|
||||
"eee19cfade": "overview"
|
||||
"eee19cfade": "overview",
|
||||
"grokUsageTab": "Grok"
|
||||
},
|
||||
"UsageOverviewPane": {
|
||||
"22ed1b7669": "sessions",
|
||||
|
|
@ -3431,8 +3435,13 @@
|
|||
"0bba8ca244": "statistics",
|
||||
"0e2a0b6431": "usage",
|
||||
"372debfac0": "stats",
|
||||
"26bb901fcd": "Orca stats plus combined Claude, Codex, and OpenCode usage analytics, tokens, cache, models, and sessions.",
|
||||
"cb2430ae6a": "Stats & Usage"
|
||||
"26bb901fcd": "Orca stats plus Claude, Codex, OpenCode token analytics and Grok subscription usage.",
|
||||
"cb2430ae6a": "Stats & Usage",
|
||||
"f8a1b2c3d4": "grok",
|
||||
"e7f0a1b2c3": "subscription",
|
||||
"d6e9f0a1b2": "credits",
|
||||
"a3b6c7d8e9": "grok usage",
|
||||
"9f2a3b4c5d": "xai"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
|
@ -3482,6 +3491,17 @@
|
|||
"faf3444859": "Input",
|
||||
"a8b7487ff7": "Output",
|
||||
"cfe2282ffa": "Unknown"
|
||||
},
|
||||
"GrokUsagePane": {
|
||||
"g8h9i0j1k2": "Grok usage",
|
||||
"b2d3e4f5c6": "Weekly subscription credits from Grok CLI OAuth (~/.grok/auth.json). Same source as the status bar.",
|
||||
"c3e4f5a6b7": "Set up in Accounts",
|
||||
"h9i0j1k2l3": " • {{value0}}",
|
||||
"i0j1k2l3m4": "Refresh Grok usage",
|
||||
"d4f5a6b7c8": "Refresh",
|
||||
"e5a6b7c8d9": "Weekly credits used",
|
||||
"f6b7c8d9e0": "Billing period reset",
|
||||
"a7b8c9d0e1": "Grok account settings"
|
||||
}
|
||||
},
|
||||
"sparse": {
|
||||
|
|
@ -6312,7 +6332,7 @@
|
|||
"b5ee17826b": "Pair remote Orca runtimes for persistent sessions, richer remote state, and web or mobile handoff.",
|
||||
"7686cb5c36": "Connect this browser to a saved Orca server.",
|
||||
"bd0181eeca": "Remote Orca Servers",
|
||||
"8acf3f22e0": "Orca stats plus Claude, Codex, and OpenCode usage analytics.",
|
||||
"8acf3f22e0": "Orca stats plus Claude, Codex, OpenCode token analytics and Grok subscription usage.",
|
||||
"954a8f5aef": "Stats & Usage",
|
||||
"a737a4bb22": "Keyboard shortcuts for common actions.",
|
||||
"23bf7a1ad4": "Shortcuts",
|
||||
|
|
@ -6982,7 +7002,13 @@
|
|||
"b84a5b0c8a": "Choose whether provider accounts are inspected and added on this device or in WSL.",
|
||||
"d09fb5ca92": "Account Location",
|
||||
"733f9e2a93": "MiniMax Usage",
|
||||
"f8374c3151": "Paste your platform.minimax.io session cookie for local rate-limit fetching."
|
||||
"f8374c3151": "Paste your platform.minimax.io session cookie for local rate-limit fetching.",
|
||||
"f4a8c2e1b7": "Grok (xAI) Usage",
|
||||
"e3b7d1f9a2": "OAuth sign-in via Grok CLI (grok login) for weekly credit usage.",
|
||||
"d2c6a0e8f1": "grok",
|
||||
"c1b5f9d7e0": "xai",
|
||||
"b0a4e8c6d9": "oauth",
|
||||
"a9f3d7b5c8": "login"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
|
|
@ -7196,7 +7222,11 @@
|
|||
"cardLayout": "card layout",
|
||||
"workspaceOptions": "workspace options",
|
||||
"detailed": "detailed"
|
||||
}
|
||||
},
|
||||
"f8e2a1c4b6": "Grok Usage",
|
||||
"e7d1b0f3a5": "Show Grok weekly credit usage from Grok CLI OAuth.",
|
||||
"d6c0a9e2f4": "grok",
|
||||
"c5b9f8d1e3": "xai"
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
|
|
@ -8936,6 +8966,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrokAccountsSection": {
|
||||
"a1b2c3d4e5": "Grok (xAI)",
|
||||
"f6e5d4c3b2": "Shows weekly credit usage from your Grok CLI sign-in (session file ~/.grok/auth.json).",
|
||||
"0d8e77bc40": "Grok CLI docs",
|
||||
"ad47a33f72": "Loading…",
|
||||
"b2c3d4e5f6": "Signed in",
|
||||
"c3d4e5f6a7": "Signed in. Orca only reads that file on disk — run grok login again if usage fails.",
|
||||
"d4e5f6a7b8": "Session expired — run grok login in a terminal to refresh.",
|
||||
"e5f6a7b8c9": "Not signed in to Grok CLI",
|
||||
"f6a7b8c9d0": "In a terminal, run grok login, then click Refresh usage here.",
|
||||
"3325d996cb": "Refresh usage",
|
||||
"a8f3e2c1b4": "Weekly credits",
|
||||
"b7e2d9f0a3": "Same weekly credit % as the grok /usage screen in the terminal.",
|
||||
"c6d1a8f4e2": "Resets {{when}}"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
"minimaxToggleDescription": "Muestra el uso de la suscripción de MiniMax para el espacio de trabajo activo.",
|
||||
"sshToggleDescription": "Muestra hosts SSH configurados y hosts remotos de Orca cuando haya alguno disponible.",
|
||||
"resourceUsageToggleDescription": "Muestra el Administrador de recursos. Haz clic para ver CPU, memoria, sesiones, controles del servicio en segundo plano y análisis de disco de los espacios de trabajo.",
|
||||
"portsToggleDescription": "Muestra los puertos activos de los espacios de trabajo. Haz clic para ver los puertos de cada espacio de trabajo y los puertos externos."
|
||||
"portsToggleDescription": "Muestra los puertos activos de los espacios de trabajo. Haz clic para ver los puertos de cada espacio de trabajo y los puertos externos.",
|
||||
"grokToggleDescription": "Muestra el uso de créditos de suscripción de Grok cuando has iniciado sesión con Grok CLI."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -635,7 +636,7 @@
|
|||
"94a5afe910": "Hosts SSH",
|
||||
"40d80bad8a": "Beta",
|
||||
"de0c2907a1": "Servidores remotos de Orca",
|
||||
"b351014180": "Estadísticas de Orca más análisis de uso de Claude, Codex y OpenCode.",
|
||||
"b351014180": "Estadísticas de Orca más análisis de tokens de Claude, Codex y OpenCode y uso de suscripción de Grok.",
|
||||
"d72a58b5b9": "Estadísticas y uso",
|
||||
"dcd0d9b74f": "Atajos de teclado para acciones comunes.",
|
||||
"94295ebfb3": "Atajos",
|
||||
|
|
@ -676,7 +677,7 @@
|
|||
"cd50cec5d7": "Coordina múltiples coding agents a través de Orca.",
|
||||
"58a868e8e4": "Orquestación",
|
||||
"7c79d3b7bf": "Opcional",
|
||||
"b1c2f8b0ac": "Cambio de cuenta opcional para Claude, Codex, Gemini y OpenCode Go.",
|
||||
"b1c2f8b0ac": "Configuración opcional de cuentas y uso para Claude, Codex, Gemini, OpenCode Go, MiniMax y Grok.",
|
||||
"f70ac54d38": "Cuentas de proveedores de IA",
|
||||
"4121f7a0a2": "Administra agentes de IA, define uno predeterminado y personaliza comandos.",
|
||||
"b49abbd2f7": "Agentes",
|
||||
|
|
@ -3032,7 +3033,9 @@
|
|||
"c0e972d726": "Cancelar",
|
||||
"06741a2f3d": "Abrir detalles de uso de MiniMax",
|
||||
"3bbf140864": "Uso de MiniMax",
|
||||
"remoteServerLabel": "Servidor remoto"
|
||||
"remoteServerLabel": "Servidor remoto",
|
||||
"grokUsageAria": "Abrir detalles de uso de Grok",
|
||||
"grokUsageMenu": "Uso de Grok"
|
||||
},
|
||||
"StatusBarUsageEmptyCta": {
|
||||
"828c764a79": "Conectar una cuenta",
|
||||
|
|
@ -3380,7 +3383,8 @@
|
|||
"b2cf4310ce": "Resumen",
|
||||
"908c470587": "codex",
|
||||
"eb6a066185": "claude",
|
||||
"eee19cfade": "resumen"
|
||||
"eee19cfade": "resumen",
|
||||
"grokUsageTab": "Grok"
|
||||
},
|
||||
"UsageOverviewPane": {
|
||||
"22ed1b7669": "sesiones",
|
||||
|
|
@ -3431,8 +3435,13 @@
|
|||
"0bba8ca244": "estadísticas",
|
||||
"0e2a0b6431": "uso",
|
||||
"372debfac0": "estadísticas",
|
||||
"26bb901fcd": "Estadísticas de Orca más analíticas de uso combinadas de Claude, Codex y OpenCode: tokens, caché, modelos y sesiones.",
|
||||
"cb2430ae6a": "Estadísticas y uso"
|
||||
"26bb901fcd": "Estadísticas de Orca más análisis de tokens de Claude, Codex y OpenCode y uso de suscripción de Grok.",
|
||||
"cb2430ae6a": "Estadísticas y uso",
|
||||
"f8a1b2c3d4": "grok",
|
||||
"e7f0a1b2c3": "suscripción",
|
||||
"d6e9f0a1b2": "créditos",
|
||||
"a3b6c7d8e9": "uso de grok",
|
||||
"9f2a3b4c5d": "xai"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
|
@ -3482,6 +3491,17 @@
|
|||
"faf3444859": "Entrada",
|
||||
"a8b7487ff7": "Salida",
|
||||
"cfe2282ffa": "Desconocido"
|
||||
},
|
||||
"GrokUsagePane": {
|
||||
"g8h9i0j1k2": "Uso de Grok",
|
||||
"b2d3e4f5c6": "Créditos semanales de suscripción desde OAuth de Grok CLI (~/.grok/auth.json). Es la misma fuente que la barra de estado.",
|
||||
"c3e4f5a6b7": "Configurar en Cuentas",
|
||||
"h9i0j1k2l3": " • {{value0}}",
|
||||
"i0j1k2l3m4": "Actualizar uso de Grok",
|
||||
"d4f5a6b7c8": "Actualizar",
|
||||
"e5a6b7c8d9": "Créditos semanales usados",
|
||||
"f6b7c8d9e0": "Restablecimiento del periodo de facturación",
|
||||
"a7b8c9d0e1": "Configuración de la cuenta de Grok"
|
||||
}
|
||||
},
|
||||
"sparse": {
|
||||
|
|
@ -6275,7 +6295,7 @@
|
|||
"b5ee17826b": "Empareja runtimes remotos de Orca para sesiones persistentes, estado remoto más completo y traspaso web o móvil.",
|
||||
"7686cb5c36": "Conecta este navegador a un servidor Orca guardado.",
|
||||
"bd0181eeca": "Servidores remotos de Orca",
|
||||
"8acf3f22e0": "Estadísticas de Orca más análisis de uso de Claude, Codex y OpenCode.",
|
||||
"8acf3f22e0": "Estadísticas de Orca más análisis de tokens de Claude, Codex y OpenCode y uso de suscripción de Grok.",
|
||||
"954a8f5aef": "Estadísticas y uso",
|
||||
"a737a4bb22": "Atajos de teclado para acciones comunes.",
|
||||
"23bf7a1ad4": "Atajos",
|
||||
|
|
@ -6945,7 +6965,13 @@
|
|||
"b84a5b0c8a": "Elija si las cuentas de proveedor se inspeccionan y agregan en este dispositivo o en WSL.",
|
||||
"d09fb5ca92": "Ubicación de cuentas",
|
||||
"733f9e2a93": "Uso de MiniMax",
|
||||
"f8374c3151": "Pega tu cookie de sesión de platform.minimax.io para obtener límites de uso locales."
|
||||
"f8374c3151": "Pega tu cookie de sesión de platform.minimax.io para obtener límites de uso locales.",
|
||||
"f4a8c2e1b7": "Uso de Grok (xAI)",
|
||||
"e3b7d1f9a2": "Inicio de sesión OAuth mediante Grok CLI (grok login) para el uso de créditos semanales.",
|
||||
"d2c6a0e8f1": "grok",
|
||||
"c1b5f9d7e0": "xai",
|
||||
"b0a4e8c6d9": "OAuth",
|
||||
"a9f3d7b5c8": "inicio de sesión"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
|
|
@ -7159,7 +7185,11 @@
|
|||
"detailed": "detallado"
|
||||
},
|
||||
"9a115966d3": "Minimizar a la bandeja al cerrar",
|
||||
"4d5b9427b5": "Cuando está activado, cerrar la ventana mantiene Orca en ejecución en la bandeja del sistema en lugar de salir."
|
||||
"4d5b9427b5": "Cuando está activado, cerrar la ventana mantiene Orca en ejecución en la bandeja del sistema en lugar de salir.",
|
||||
"f8e2a1c4b6": "Uso de Grok",
|
||||
"e7d1b0f3a5": "Muestra el uso semanal de créditos de Grok desde OAuth de Grok CLI.",
|
||||
"d6c0a9e2f4": "grok",
|
||||
"c5b9f8d1e3": "xai"
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
|
|
@ -8936,6 +8966,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrokAccountsSection": {
|
||||
"a1b2c3d4e5": "Grok (xAI)",
|
||||
"f6e5d4c3b2": "Muestra el uso semanal de créditos desde tu inicio de sesión en Grok CLI (archivo de sesión ~/.grok/auth.json).",
|
||||
"0d8e77bc40": "Documentación de Grok CLI",
|
||||
"ad47a33f72": "Cargando…",
|
||||
"b2c3d4e5f6": "Sesión iniciada",
|
||||
"c3d4e5f6a7": "Sesión iniciada. Orca solo lee ese archivo en disco; ejecuta grok login de nuevo si falla el uso.",
|
||||
"d4e5f6a7b8": "Sesión vencida; ejecuta grok login en una terminal para renovarla.",
|
||||
"e5f6a7b8c9": "No has iniciado sesión en Grok CLI",
|
||||
"f6a7b8c9d0": "En una terminal, ejecuta grok login y luego haz clic en Actualizar uso aquí.",
|
||||
"3325d996cb": "Actualizar uso",
|
||||
"a8f3e2c1b4": "Créditos semanales",
|
||||
"b7e2d9f0a3": "El mismo porcentaje de créditos semanales que la pantalla grok /usage en la terminal.",
|
||||
"c6d1a8f4e2": "Se restablece {{when}}"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
"minimaxToggleDescription": "アクティブなワークスペースの MiniMax サブスクリプション使用状況を表示します。",
|
||||
"sshToggleDescription": "設定済み SSH ホストとリモート Orca ホストがある場合に表示します。",
|
||||
"resourceUsageToggleDescription": "リソースマネージャーを表示します。これをクリックすると、CPU、メモリ、セッション、デーモン コントロール、およびワークスペース ディスク スキャンが行われます。",
|
||||
"portsToggleDescription": "ライブワークスペースポートを表示します。ワークスペーススコープのポートと外部リスナーの場合はこれをクリックします。"
|
||||
"portsToggleDescription": "ライブワークスペースポートを表示します。ワークスペーススコープのポートと外部リスナーの場合はこれをクリックします。",
|
||||
"grokToggleDescription": "Grok CLI でサインインしている場合に Grok サブスクリプションのクレジット使用量を表示します。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -635,7 +636,7 @@
|
|||
"94a5afe910": "SSH ホスト",
|
||||
"40d80bad8a": "ベータ",
|
||||
"de0c2907a1": "リモート Orca サーバー",
|
||||
"b351014180": "Orca の統計と Claude、Codex、OpenCode の使用状況分析。",
|
||||
"b351014180": "Orca の統計に加え、Claude、Codex、OpenCode のトークン分析と Grok サブスクリプション使用状況。",
|
||||
"d72a58b5b9": "統計と使用状況",
|
||||
"dcd0d9b74f": "よく使う操作のキーボード ショートカット。",
|
||||
"94295ebfb3": "ショートカット",
|
||||
|
|
@ -676,7 +677,7 @@
|
|||
"cd50cec5d7": "Orca を通じて複数のコーディング agents を調整します。",
|
||||
"58a868e8e4": "オーケストレーション",
|
||||
"7c79d3b7bf": "任意",
|
||||
"b1c2f8b0ac": "Claude、Codex、Gemini、OpenCode Go のオプションのアカウント切り替え。",
|
||||
"b1c2f8b0ac": "Claude、Codex、Gemini、OpenCode Go、MiniMax、Grok の任意のアカウント切り替えと使用量設定。",
|
||||
"f70ac54d38": "AI プロバイダー アカウント",
|
||||
"4121f7a0a2": "AI agents を管理し、デフォルトを設定し、コマンドをカスタマイズします。",
|
||||
"b49abbd2f7": "エージェント",
|
||||
|
|
@ -3032,7 +3033,9 @@
|
|||
"c0e972d726": "キャンセル",
|
||||
"06741a2f3d": "MiniMax の使用量詳細を開く",
|
||||
"3bbf140864": "MiniMax 使用量",
|
||||
"remoteServerLabel": "リモートサーバー"
|
||||
"remoteServerLabel": "リモートサーバー",
|
||||
"grokUsageAria": "Grok の使用状況詳細を開く",
|
||||
"grokUsageMenu": "Grok の使用状況"
|
||||
},
|
||||
"StatusBarUsageEmptyCta": {
|
||||
"828c764a79": "アカウントを接続する",
|
||||
|
|
@ -3380,7 +3383,8 @@
|
|||
"b2cf4310ce": "概要",
|
||||
"908c470587": "codex",
|
||||
"eb6a066185": "claude",
|
||||
"eee19cfade": "概要"
|
||||
"eee19cfade": "概要",
|
||||
"grokUsageTab": "Grok"
|
||||
},
|
||||
"UsageOverviewPane": {
|
||||
"22ed1b7669": "セッション",
|
||||
|
|
@ -3431,8 +3435,13 @@
|
|||
"0bba8ca244": "統計",
|
||||
"0e2a0b6431": "使用法",
|
||||
"372debfac0": "統計",
|
||||
"26bb901fcd": "Orca の統計に加え、Claude、Codex、OpenCode の使用状況分析、トークン、キャッシュ、モデル、セッションを組み合わせたもの。",
|
||||
"cb2430ae6a": "統計と使用状況"
|
||||
"26bb901fcd": "Orca の統計に加え、Claude、Codex、OpenCode のトークン分析と Grok サブスクリプション使用状況。",
|
||||
"cb2430ae6a": "統計と使用状況",
|
||||
"f8a1b2c3d4": "grok",
|
||||
"e7f0a1b2c3": "サブスクリプション",
|
||||
"d6e9f0a1b2": "クレジット",
|
||||
"a3b6c7d8e9": "grok の使用状況",
|
||||
"9f2a3b4c5d": "xai"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
|
@ -3482,6 +3491,17 @@
|
|||
"faf3444859": "入力",
|
||||
"a8b7487ff7": "出力",
|
||||
"cfe2282ffa": "不明"
|
||||
},
|
||||
"GrokUsagePane": {
|
||||
"g8h9i0j1k2": "Grok の使用状況",
|
||||
"b2d3e4f5c6": "Grok CLI OAuth (~/.grok/auth.json) から取得した週次サブスクリプションクレジット。ステータスバーと同じ情報源です。",
|
||||
"c3e4f5a6b7": "アカウントで設定",
|
||||
"h9i0j1k2l3": " • {{value0}}",
|
||||
"i0j1k2l3m4": "Grok 使用状況を更新",
|
||||
"d4f5a6b7c8": "更新",
|
||||
"e5a6b7c8d9": "使用済み週次クレジット",
|
||||
"f6b7c8d9e0": "請求期間のリセット",
|
||||
"a7b8c9d0e1": "Grok アカウント設定"
|
||||
}
|
||||
},
|
||||
"sparse": {
|
||||
|
|
@ -6297,7 +6317,7 @@
|
|||
"b5ee17826b": "Pair remote Orca runtimes for persistent sessions, richer remote state, and web or mobile handoff.",
|
||||
"7686cb5c36": "このブラウザを保存された Orca サーバーに接続します。",
|
||||
"bd0181eeca": "リモート Orca サーバー",
|
||||
"8acf3f22e0": "Orca の統計と Claude、Codex、OpenCode の使用状況分析。",
|
||||
"8acf3f22e0": "Orca の統計に加え、Claude、Codex、OpenCode のトークン分析と Grok サブスクリプション使用状況。",
|
||||
"954a8f5aef": "統計と使用状況",
|
||||
"a737a4bb22": "よく使う操作のキーボード ショートカット。",
|
||||
"23bf7a1ad4": "ショートカット",
|
||||
|
|
@ -6967,7 +6987,13 @@
|
|||
"b84a5b0c8a": "プロバイダー アカウントをこのデバイスまたは WSL で検査および追加するかどうかを選択します。",
|
||||
"d09fb5ca92": "アカウントの場所",
|
||||
"733f9e2a93": "MiniMax 使用量",
|
||||
"f8374c3151": "ローカルのレート制限取得用に platform.minimax.io のセッション Cookie を貼り付けます。"
|
||||
"f8374c3151": "ローカルのレート制限取得用に platform.minimax.io のセッション Cookie を貼り付けます。",
|
||||
"f4a8c2e1b7": "Grok (xAI) の使用状況",
|
||||
"e3b7d1f9a2": "週次クレジット使用量のための Grok CLI (grok login) による OAuth サインイン。",
|
||||
"d2c6a0e8f1": "grok",
|
||||
"c1b5f9d7e0": "xai",
|
||||
"b0a4e8c6d9": "認証",
|
||||
"a9f3d7b5c8": "ログイン"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
|
|
@ -7181,7 +7207,11 @@
|
|||
"detailed": "詳細"
|
||||
},
|
||||
"9a115966d3": "閉じるときにトレイへ最小化",
|
||||
"4d5b9427b5": "有効にすると、ウィンドウを閉じてもOrcaは終了せず、システムトレイで実行を続けます。"
|
||||
"4d5b9427b5": "有効にすると、ウィンドウを閉じてもOrcaは終了せず、システムトレイで実行を続けます。",
|
||||
"f8e2a1c4b6": "Grok の使用状況",
|
||||
"e7d1b0f3a5": "Grok CLI OAuth から Grok の週次クレジット使用量を表示します。",
|
||||
"d6c0a9e2f4": "grok",
|
||||
"c5b9f8d1e3": "xai"
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
|
|
@ -8936,6 +8966,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrokAccountsSection": {
|
||||
"a1b2c3d4e5": "Grok (xAI)",
|
||||
"f6e5d4c3b2": "Grok CLI サインインから週次クレジット使用量を表示します (セッションファイル ~/.grok/auth.json)。",
|
||||
"0d8e77bc40": "Grok CLI ドキュメント",
|
||||
"ad47a33f72": "読み込み中…",
|
||||
"b2c3d4e5f6": "サインイン済み",
|
||||
"c3d4e5f6a7": "サインイン済みです。Orca はディスク上のそのファイルだけを読み取ります。使用状況の取得に失敗する場合は grok login を再実行してください。",
|
||||
"d4e5f6a7b8": "セッションの有効期限が切れました。ターミナルで grok login を実行して更新してください。",
|
||||
"e5f6a7b8c9": "Grok CLI にサインインしていません",
|
||||
"f6a7b8c9d0": "ターミナルで grok login を実行してから、ここで「使用状況を更新」をクリックしてください。",
|
||||
"3325d996cb": "使用状況を更新",
|
||||
"a8f3e2c1b4": "週次クレジット",
|
||||
"b7e2d9f0a3": "ターミナルの grok /usage 画面と同じ週次クレジット率です。",
|
||||
"c6d1a8f4e2": "{{when}} にリセット"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
"minimaxToggleDescription": "활성 워크스페이스의 MiniMax 구독 사용량을 표시합니다.",
|
||||
"sshToggleDescription": "사용 가능한 SSH 및 원격 Orca 호스트가 있으면 표시합니다.",
|
||||
"resourceUsageToggleDescription": "리소스 관리자를 표시합니다. 클릭하면 CPU, 메모리, 세션, 데몬 제어, 워크스페이스 디스크 스캔을 볼 수 있습니다.",
|
||||
"portsToggleDescription": "라이브 워크스페이스 포트를 표시합니다. 워크스페이스 범위 포트 및 외부 수신기를 보려면 클릭하세요."
|
||||
"portsToggleDescription": "라이브 워크스페이스 포트를 표시합니다. 워크스페이스 범위 포트 및 외부 수신기를 보려면 클릭하세요.",
|
||||
"grokToggleDescription": "Grok CLI로 로그인한 경우 Grok 구독 크레딧 사용량을 표시합니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -635,7 +636,7 @@
|
|||
"94a5afe910": "SSH 호스트",
|
||||
"40d80bad8a": "베타",
|
||||
"de0c2907a1": "원격 Orca 서버",
|
||||
"b351014180": "Orca 통계와 Claude, Codex 및 OpenCode 사용 분석.",
|
||||
"b351014180": "Orca 통계와 Claude, Codex, OpenCode 토큰 분석 및 Grok 구독 사용량.",
|
||||
"d72a58b5b9": "통계 및 사용량",
|
||||
"dcd0d9b74f": "일반 작업에 대한 키보드 단축키입니다.",
|
||||
"94295ebfb3": "단축키",
|
||||
|
|
@ -676,7 +677,7 @@
|
|||
"cd50cec5d7": "Orca를 통해 여러 코딩 agents를 조정합니다.",
|
||||
"58a868e8e4": "오케스트레이션",
|
||||
"7c79d3b7bf": "선택 사항",
|
||||
"b1c2f8b0ac": "Claude, Codex, Gemini 및 OpenCode Go에 대한 선택적 계정 전환.",
|
||||
"b1c2f8b0ac": "Claude, Codex, Gemini, OpenCode Go, MiniMax, Grok의 선택적 계정 전환 및 사용량 설정.",
|
||||
"f70ac54d38": "AI 제공업체 계정",
|
||||
"4121f7a0a2": "AI agents를 관리하고, 기본값을 설정하고, 명령을 사용자 정의하세요.",
|
||||
"b49abbd2f7": "에이전트",
|
||||
|
|
@ -3032,7 +3033,9 @@
|
|||
"c0e972d726": "취소",
|
||||
"06741a2f3d": "MiniMax 사용량 세부 정보 열기",
|
||||
"3bbf140864": "MiniMax 사용량",
|
||||
"remoteServerLabel": "원격 서버"
|
||||
"remoteServerLabel": "원격 서버",
|
||||
"grokUsageAria": "Grok 사용량 세부 정보 열기",
|
||||
"grokUsageMenu": "Grok 사용량"
|
||||
},
|
||||
"StatusBarUsageEmptyCta": {
|
||||
"828c764a79": "계정 연결",
|
||||
|
|
@ -3380,7 +3383,8 @@
|
|||
"b2cf4310ce": "개요",
|
||||
"908c470587": "codex",
|
||||
"eb6a066185": "claude",
|
||||
"eee19cfade": "개요"
|
||||
"eee19cfade": "개요",
|
||||
"grokUsageTab": "Grok"
|
||||
},
|
||||
"UsageOverviewPane": {
|
||||
"22ed1b7669": "세션",
|
||||
|
|
@ -3431,8 +3435,13 @@
|
|||
"0bba8ca244": "통계",
|
||||
"0e2a0b6431": "사용량",
|
||||
"372debfac0": "통계",
|
||||
"26bb901fcd": "Orca 통계, Claude/Codex/OpenCode 통합 사용량 분석, 토큰, 캐시, 모델, 세션",
|
||||
"cb2430ae6a": "통계 및 사용량"
|
||||
"26bb901fcd": "Orca 통계와 Claude, Codex, OpenCode 토큰 분석 및 Grok 구독 사용량.",
|
||||
"cb2430ae6a": "통계 및 사용량",
|
||||
"f8a1b2c3d4": "grok",
|
||||
"e7f0a1b2c3": "구독",
|
||||
"d6e9f0a1b2": "크레딧",
|
||||
"a3b6c7d8e9": "grok 사용량",
|
||||
"9f2a3b4c5d": "xai"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
|
@ -3482,6 +3491,17 @@
|
|||
"faf3444859": "입력",
|
||||
"a8b7487ff7": "출력",
|
||||
"cfe2282ffa": "알 수 없음"
|
||||
},
|
||||
"GrokUsagePane": {
|
||||
"g8h9i0j1k2": "Grok 사용량",
|
||||
"b2d3e4f5c6": "Grok CLI OAuth(~/.grok/auth.json)의 주간 구독 크레딧입니다. 상태 표시줄과 같은 출처입니다.",
|
||||
"c3e4f5a6b7": "계정에서 설정",
|
||||
"h9i0j1k2l3": " • {{value0}}",
|
||||
"i0j1k2l3m4": "Grok 사용량 새로 고침",
|
||||
"d4f5a6b7c8": "새로 고침",
|
||||
"e5a6b7c8d9": "사용한 주간 크레딧",
|
||||
"f6b7c8d9e0": "결제 기간 재설정",
|
||||
"a7b8c9d0e1": "Grok 계정 설정"
|
||||
}
|
||||
},
|
||||
"sparse": {
|
||||
|
|
@ -6260,7 +6280,7 @@
|
|||
"b5ee17826b": "지속 세션, 더 풍부한 원격 상태, 웹 또는 모바일 핸드오프를 위해 원격 Orca 런타임을 페어링합니다.",
|
||||
"7686cb5c36": "이 브라우저를 저장된 Orca 서버에 연결하세요.",
|
||||
"bd0181eeca": "원격 Orca 서버",
|
||||
"8acf3f22e0": "Orca 통계와 Claude, Codex 및 OpenCode 사용 분석.",
|
||||
"8acf3f22e0": "Orca 통계와 Claude, Codex, OpenCode 토큰 분석 및 Grok 구독 사용량.",
|
||||
"954a8f5aef": "통계 및 사용량",
|
||||
"a737a4bb22": "일반 작업에 대한 키보드 단축키입니다.",
|
||||
"23bf7a1ad4": "단축키",
|
||||
|
|
@ -6930,7 +6950,13 @@
|
|||
"b84a5b0c8a": "이 장치 또는 WSL에서 공급자 계정을 검사하고 추가할지 선택합니다.",
|
||||
"d09fb5ca92": "계정 위치",
|
||||
"733f9e2a93": "MiniMax 사용량",
|
||||
"f8374c3151": "로컬 rate-limit 가져오기에 사용할 platform.minimax.io 세션 Cookie를 붙여넣으세요."
|
||||
"f8374c3151": "로컬 rate-limit 가져오기에 사용할 platform.minimax.io 세션 Cookie를 붙여넣으세요.",
|
||||
"f4a8c2e1b7": "Grok(xAI) 사용량",
|
||||
"e3b7d1f9a2": "주간 크레딧 사용량을 위한 Grok CLI(grok login) OAuth 로그인.",
|
||||
"d2c6a0e8f1": "grok",
|
||||
"c1b5f9d7e0": "xai",
|
||||
"b0a4e8c6d9": "OAuth",
|
||||
"a9f3d7b5c8": "로그인"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
|
|
@ -7144,7 +7170,11 @@
|
|||
"cardLayout": "카드 레이아웃",
|
||||
"workspaceOptions": "워크스페이스 옵션",
|
||||
"detailed": "상세"
|
||||
}
|
||||
},
|
||||
"f8e2a1c4b6": "Grok 사용량",
|
||||
"e7d1b0f3a5": "Grok CLI OAuth에서 Grok 주간 크레딧 사용량을 표시합니다.",
|
||||
"d6c0a9e2f4": "grok",
|
||||
"c5b9f8d1e3": "xai"
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
|
|
@ -8936,6 +8966,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrokAccountsSection": {
|
||||
"a1b2c3d4e5": "Grok (xAI)",
|
||||
"f6e5d4c3b2": "Grok CLI 로그인에서 주간 크레딧 사용량을 표시합니다(세션 파일 ~/.grok/auth.json).",
|
||||
"0d8e77bc40": "Grok CLI 문서",
|
||||
"ad47a33f72": "로드 중…",
|
||||
"b2c3d4e5f6": "로그인됨",
|
||||
"c3d4e5f6a7": "로그인되었습니다. Orca는 디스크의 해당 파일만 읽습니다. 사용량 조회가 실패하면 grok login을 다시 실행하세요.",
|
||||
"d4e5f6a7b8": "세션이 만료되었습니다. 터미널에서 grok login을 실행해 갱신하세요.",
|
||||
"e5f6a7b8c9": "Grok CLI에 로그인되어 있지 않음",
|
||||
"f6a7b8c9d0": "터미널에서 grok login을 실행한 다음 여기에서 사용량 새로 고침을 클릭하세요.",
|
||||
"3325d996cb": "사용량 새로 고침",
|
||||
"a8f3e2c1b4": "주간 크레딧",
|
||||
"b7e2d9f0a3": "터미널의 grok /usage 화면과 같은 주간 크레딧 비율입니다.",
|
||||
"c6d1a8f4e2": "{{when}}에 재설정"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
"minimaxToggleDescription": "显示当前工作区的 MiniMax 订阅使用情况。",
|
||||
"sshToggleDescription": "当有可用的 SSH 和远程 Orca 主机时显示它们。",
|
||||
"resourceUsageToggleDescription": "显示资源管理器。点击可查看 CPU、内存、会话、守护进程控制和工作区磁盘扫描。",
|
||||
"portsToggleDescription": "显示实时工作区端口。单击它可获取工作区范围的端口和外部侦听器。"
|
||||
"portsToggleDescription": "显示实时工作区端口。单击它可获取工作区范围的端口和外部侦听器。",
|
||||
"grokToggleDescription": "通过 Grok CLI 登录后显示 Grok 订阅额度使用量。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -635,7 +636,7 @@
|
|||
"94a5afe910": "SSH 主机",
|
||||
"40d80bad8a": "测试版",
|
||||
"de0c2907a1": "远程 Orca 服务器",
|
||||
"b351014180": "Orca 统计数据以及 Claude、Codex 和 OpenCode 使用情况分析。",
|
||||
"b351014180": "Orca 统计,以及 Claude、Codex、OpenCode Token 分析和 Grok 订阅使用量。",
|
||||
"d72a58b5b9": "统计和使用情况",
|
||||
"dcd0d9b74f": "常见操作的键盘快捷键。",
|
||||
"94295ebfb3": "快捷键",
|
||||
|
|
@ -676,7 +677,7 @@
|
|||
"cd50cec5d7": "通过 Orca 协调多个编码智能体。",
|
||||
"58a868e8e4": "编排",
|
||||
"7c79d3b7bf": "可选",
|
||||
"b1c2f8b0ac": "Claude、Codex、Gemini 和 OpenCode Go 的可选账户切换。",
|
||||
"b1c2f8b0ac": "Claude、Codex、Gemini、OpenCode Go、MiniMax 和 Grok 的可选账户切换与使用量设置。",
|
||||
"f70ac54d38": "AI 提供商账户",
|
||||
"4121f7a0a2": "管理 AI 智能体、设置默认值并自定义命令。",
|
||||
"b49abbd2f7": "智能体",
|
||||
|
|
@ -3032,7 +3033,9 @@
|
|||
"c0e972d726": "取消",
|
||||
"06741a2f3d": "打开 MiniMax 使用量详情",
|
||||
"3bbf140864": "MiniMax 使用量",
|
||||
"remoteServerLabel": "远程服务器"
|
||||
"remoteServerLabel": "远程服务器",
|
||||
"grokUsageAria": "打开 Grok 使用详情",
|
||||
"grokUsageMenu": "Grok 使用量"
|
||||
},
|
||||
"StatusBarUsageEmptyCta": {
|
||||
"828c764a79": "连接账户",
|
||||
|
|
@ -3380,7 +3383,8 @@
|
|||
"b2cf4310ce": "概览",
|
||||
"908c470587": "codex",
|
||||
"eb6a066185": "claude",
|
||||
"eee19cfade": "概览"
|
||||
"eee19cfade": "概览",
|
||||
"grokUsageTab": "Grok"
|
||||
},
|
||||
"UsageOverviewPane": {
|
||||
"22ed1b7669": "会话",
|
||||
|
|
@ -3431,8 +3435,13 @@
|
|||
"0bba8ca244": "统计数据",
|
||||
"0e2a0b6431": "用法",
|
||||
"372debfac0": "统计数据",
|
||||
"26bb901fcd": "Orca 统计数据以及 Claude、Codex 和 OpenCode 的综合使用分析、Token、缓存、模型和会话。",
|
||||
"cb2430ae6a": "统计和使用情况"
|
||||
"26bb901fcd": "Orca 统计,以及 Claude、Codex、OpenCode Token 分析和 Grok 订阅使用量。",
|
||||
"cb2430ae6a": "统计和使用情况",
|
||||
"f8a1b2c3d4": "grok",
|
||||
"e7f0a1b2c3": "订阅",
|
||||
"d6e9f0a1b2": "额度",
|
||||
"a3b6c7d8e9": "grok 使用量",
|
||||
"9f2a3b4c5d": "xai"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
|
|
@ -3482,6 +3491,17 @@
|
|||
"faf3444859": "输入",
|
||||
"a8b7487ff7": "输出",
|
||||
"cfe2282ffa": "未知"
|
||||
},
|
||||
"GrokUsagePane": {
|
||||
"g8h9i0j1k2": "Grok 使用量",
|
||||
"b2d3e4f5c6": "来自 Grok CLI OAuth(~/.grok/auth.json)的每周订阅额度。与状态栏使用同一来源。",
|
||||
"c3e4f5a6b7": "在账户中设置",
|
||||
"h9i0j1k2l3": " • {{value0}}",
|
||||
"i0j1k2l3m4": "刷新 Grok 使用量",
|
||||
"d4f5a6b7c8": "刷新",
|
||||
"e5a6b7c8d9": "已用每周额度",
|
||||
"f6b7c8d9e0": "计费周期重置",
|
||||
"a7b8c9d0e1": "Grok 账户设置"
|
||||
}
|
||||
},
|
||||
"sparse": {
|
||||
|
|
@ -6260,7 +6280,7 @@
|
|||
"b5ee17826b": "配对远程 Orca 运行时,以获得持久会话、更丰富的远程状态,以及 Web 或移动端接续。",
|
||||
"7686cb5c36": "将此浏览器连接到已保存的 Orca 服务器。",
|
||||
"bd0181eeca": "远程 Orca 服务器",
|
||||
"8acf3f22e0": "Orca 统计数据以及 Claude、Codex 和 OpenCode 使用情况分析。",
|
||||
"8acf3f22e0": "Orca 统计,以及 Claude、Codex、OpenCode Token 分析和 Grok 订阅使用量。",
|
||||
"954a8f5aef": "统计和使用情况",
|
||||
"a737a4bb22": "常见操作的键盘快捷键。",
|
||||
"23bf7a1ad4": "快捷键",
|
||||
|
|
@ -6930,7 +6950,13 @@
|
|||
"b84a5b0c8a": "选择是否在此设备上或 WSL 中检查和添加提供商账户。",
|
||||
"d09fb5ca92": "账户位置",
|
||||
"733f9e2a93": "MiniMax 使用量",
|
||||
"f8374c3151": "粘贴 platform.minimax.io 会话 Cookie 以在本地获取速率限制。"
|
||||
"f8374c3151": "粘贴 platform.minimax.io 会话 Cookie 以在本地获取速率限制。",
|
||||
"f4a8c2e1b7": "Grok (xAI) 使用量",
|
||||
"e3b7d1f9a2": "通过 Grok CLI(grok login)OAuth 登录以查看每周额度使用量。",
|
||||
"d2c6a0e8f1": "grok",
|
||||
"c1b5f9d7e0": "xai",
|
||||
"b0a4e8c6d9": "OAuth",
|
||||
"a9f3d7b5c8": "登录"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
|
|
@ -7144,7 +7170,11 @@
|
|||
"detailed": "详细"
|
||||
},
|
||||
"9a115966d3": "关闭时最小化到托盘",
|
||||
"4d5b9427b5": "启用后,关闭窗口会让 Orca 继续在系统托盘中运行,而不是退出。"
|
||||
"4d5b9427b5": "启用后,关闭窗口会让 Orca 继续在系统托盘中运行,而不是退出。",
|
||||
"f8e2a1c4b6": "Grok 使用量",
|
||||
"e7d1b0f3a5": "显示来自 Grok CLI OAuth 的 Grok 每周额度使用量。",
|
||||
"d6c0a9e2f4": "grok",
|
||||
"c5b9f8d1e3": "xai"
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
|
|
@ -8936,6 +8966,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GrokAccountsSection": {
|
||||
"a1b2c3d4e5": "Grok (xAI)",
|
||||
"f6e5d4c3b2": "显示来自 Grok CLI 登录的每周额度使用量(会话文件 ~/.grok/auth.json)。",
|
||||
"0d8e77bc40": "Grok CLI 文档",
|
||||
"ad47a33f72": "正在加载…",
|
||||
"b2c3d4e5f6": "已登录",
|
||||
"c3d4e5f6a7": "已登录。Orca 只读取磁盘上的该文件;如果使用量获取失败,请重新运行 grok login。",
|
||||
"d4e5f6a7b8": "会话已过期,请在终端运行 grok login 以刷新。",
|
||||
"e5f6a7b8c9": "未登录 Grok CLI",
|
||||
"f6a7b8c9d0": "在终端运行 grok login,然后点击此处的刷新使用量。",
|
||||
"3325d996cb": "刷新使用量",
|
||||
"a8f3e2c1b4": "每周额度",
|
||||
"b7e2d9f0a3": "与终端中 grok /usage 屏幕显示的每周额度百分比相同。",
|
||||
"c6d1a8f4e2": "{{when}} 重置"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type RateLimitSlice = {
|
|||
rateLimits: RateLimitState
|
||||
fetchRateLimits: () => Promise<void>
|
||||
refreshRateLimits: () => Promise<void>
|
||||
refreshGrokRateLimits: () => Promise<void>
|
||||
refreshClaudeRateLimitsForTarget: (target: RateLimitRuntimeTarget) => Promise<void>
|
||||
refreshCodexRateLimitsForTarget: (target: RateLimitRuntimeTarget) => Promise<void>
|
||||
consumeCodexRateLimitResetCredit: () => Promise<void>
|
||||
|
|
@ -22,7 +23,9 @@ export const createRateLimitSlice: StateCreator<AppState, [], [], RateLimitSlice
|
|||
opencodeGo: null,
|
||||
kimi: null,
|
||||
minimax: null,
|
||||
grok: null,
|
||||
minimaxCookieConfigured: false,
|
||||
grokAuthConfigured: false,
|
||||
claudeTarget: { runtime: 'host', wslDistro: null },
|
||||
codexTarget: { runtime: 'host', wslDistro: null },
|
||||
inactiveClaudeAccounts: [],
|
||||
|
|
@ -47,6 +50,15 @@ export const createRateLimitSlice: StateCreator<AppState, [], [], RateLimitSlice
|
|||
}
|
||||
},
|
||||
|
||||
refreshGrokRateLimits: async () => {
|
||||
try {
|
||||
const state = await window.api.rateLimits.refreshGrok()
|
||||
set({ rateLimits: state })
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh Grok usage:', error)
|
||||
}
|
||||
},
|
||||
|
||||
refreshClaudeRateLimitsForTarget: async (target) => {
|
||||
const current = get().rateLimits
|
||||
const targetChanged =
|
||||
|
|
|
|||
|
|
@ -1215,13 +1215,15 @@ describe('createUISlice hydratePersistedUI', () => {
|
|||
'resource-usage',
|
||||
'ports',
|
||||
'kimi',
|
||||
'minimax'
|
||||
'minimax',
|
||||
'grok'
|
||||
])
|
||||
expect(setUI).toHaveBeenCalledWith({
|
||||
statusBarItems: ['claude', 'resource-usage', 'ports', 'kimi', 'minimax'],
|
||||
statusBarItems: ['claude', 'resource-usage', 'ports', 'kimi', 'minimax', 'grok'],
|
||||
_portsStatusBarDefaultAdded: true,
|
||||
_kimiStatusBarDefaultAdded: true,
|
||||
_minimaxStatusBarDefaultAdded: true
|
||||
_minimaxStatusBarDefaultAdded: true,
|
||||
_grokStatusBarDefaultAdded: true
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1235,7 +1237,8 @@ describe('createUISlice hydratePersistedUI', () => {
|
|||
statusBarItems: ['claude', 'resource-usage'],
|
||||
_portsStatusBarDefaultAdded: true,
|
||||
_kimiStatusBarDefaultAdded: true,
|
||||
_minimaxStatusBarDefaultAdded: true
|
||||
_minimaxStatusBarDefaultAdded: true,
|
||||
_grokStatusBarDefaultAdded: true
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@ function migrateStatusBarItems(items: readonly string[] | undefined): StatusBarI
|
|||
const DEFAULT_ON_PORTS_STATUS_BAR_ITEM: StatusBarItem = 'ports'
|
||||
const DEFAULT_ON_KIMI_STATUS_BAR_ITEM: StatusBarItem = 'kimi'
|
||||
const DEFAULT_ON_MINIMAX_STATUS_BAR_ITEM: StatusBarItem = 'minimax'
|
||||
const DEFAULT_ON_GROK_STATUS_BAR_ITEM: StatusBarItem = 'grok'
|
||||
|
||||
function normalizeHydratedVisibleWorkspaceHostIds(ui: PersistedUIState): VisibleWorkspaceHostIds {
|
||||
const visibleHostIds = normalizeVisibleExecutionHostIds(ui.visibleWorkspaceHostIds)
|
||||
|
|
@ -2257,18 +2258,24 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
|
|||
ui._minimaxStatusBarDefaultAdded || statusBarItems.includes('minimax')
|
||||
? statusBarItems
|
||||
: [...statusBarItems, DEFAULT_ON_MINIMAX_STATUS_BAR_ITEM]
|
||||
const statusBarItemsWithGrok =
|
||||
ui._grokStatusBarDefaultAdded || statusBarItemsWithMiniMax.includes('grok')
|
||||
? statusBarItemsWithMiniMax
|
||||
: [...statusBarItemsWithMiniMax, DEFAULT_ON_GROK_STATUS_BAR_ITEM]
|
||||
if (
|
||||
(!ui._portsStatusBarDefaultAdded ||
|
||||
!ui._kimiStatusBarDefaultAdded ||
|
||||
!ui._minimaxStatusBarDefaultAdded) &&
|
||||
!ui._minimaxStatusBarDefaultAdded ||
|
||||
!ui._grokStatusBarDefaultAdded) &&
|
||||
typeof window !== 'undefined'
|
||||
) {
|
||||
window.api.ui
|
||||
.set({
|
||||
statusBarItems: statusBarItemsWithMiniMax,
|
||||
statusBarItems: statusBarItemsWithGrok,
|
||||
_portsStatusBarDefaultAdded: true,
|
||||
_kimiStatusBarDefaultAdded: true,
|
||||
_minimaxStatusBarDefaultAdded: true
|
||||
_minimaxStatusBarDefaultAdded: true,
|
||||
_grokStatusBarDefaultAdded: true
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
|
@ -2333,7 +2340,7 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
|
|||
workspaceBoardOpacity: clampWorkspaceBoardOpacity(ui.workspaceBoardOpacity),
|
||||
workspaceBoardColumnWidth: clampWorkspaceBoardColumnWidth(ui.workspaceBoardColumnWidth),
|
||||
syncTaskStatusFromWorkspaceBoard: ui.syncTaskStatusFromWorkspaceBoard === true,
|
||||
statusBarItems: statusBarItemsWithMiniMax,
|
||||
statusBarItems: statusBarItemsWithGrok,
|
||||
statusBarVisible: ui.statusBarVisible ?? true,
|
||||
// Why: absent → true so existing users see the pet the first time
|
||||
// they enable the experimental flag. Only an explicit Hide pet
|
||||
|
|
|
|||
|
|
@ -704,6 +704,7 @@ function createWebPreloadApi(): Partial<PreloadApi> {
|
|||
notifications: createNotificationsApi(),
|
||||
rateLimits: createRateLimitsApi(),
|
||||
minimaxCredentials: createMiniMaxCredentialsApi(),
|
||||
grokAccounts: createGrokAccountsApi(),
|
||||
codexAccounts: createAccountsApi(),
|
||||
claudeAccounts: createAccountsApi(),
|
||||
cli: createCliApi(),
|
||||
|
|
@ -2583,7 +2584,9 @@ function createRateLimitsApi(): NonNullable<Partial<PreloadApi>['rateLimits']> {
|
|||
opencodeGo: null,
|
||||
kimi: null,
|
||||
minimax: null,
|
||||
grok: null,
|
||||
minimaxCookieConfigured: false,
|
||||
grokAuthConfigured: false,
|
||||
claudeTarget: { runtime: 'host', wslDistro: null },
|
||||
codexTarget: { runtime: 'host', wslDistro: null },
|
||||
inactiveClaudeAccounts: [],
|
||||
|
|
@ -2601,6 +2604,7 @@ function createRateLimitsApi(): NonNullable<Partial<PreloadApi>['rateLimits']> {
|
|||
fetchInactiveClaudeAccounts: () => Promise.resolve(),
|
||||
fetchInactiveCodexAccounts: () => Promise.resolve(),
|
||||
refreshMiniMax: () => Promise.resolve(empty),
|
||||
refreshGrok: () => Promise.resolve(empty),
|
||||
onUpdate: () => noopUnsubscribe
|
||||
}
|
||||
}
|
||||
|
|
@ -2615,6 +2619,19 @@ function createMiniMaxCredentialsApi(): NonNullable<Partial<PreloadApi>['minimax
|
|||
}
|
||||
}
|
||||
|
||||
function createGrokAccountsApi(): NonNullable<Partial<PreloadApi>['grokAccounts']> {
|
||||
const unsigned = {
|
||||
signedIn: false,
|
||||
email: null,
|
||||
teamId: null,
|
||||
tokenFresh: false,
|
||||
error: null
|
||||
}
|
||||
return {
|
||||
getStatus: () => Promise.resolve(unsigned)
|
||||
}
|
||||
}
|
||||
|
||||
function createAccountsApi(): never {
|
||||
const empty = {
|
||||
accounts: [],
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ describe('RateLimitState', () => {
|
|||
opencodeGo: null,
|
||||
kimi: null,
|
||||
minimax: null,
|
||||
grok: null,
|
||||
minimaxCookieConfigured: false,
|
||||
grokAuthConfigured: false,
|
||||
claudeTarget: { runtime: 'host', wslDistro: null },
|
||||
codexTarget: { runtime: 'host', wslDistro: null },
|
||||
inactiveClaudeAccounts: [],
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export type UsageRateLimitMetadata = {
|
|||
}
|
||||
|
||||
export type ProviderRateLimits = {
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax'
|
||||
provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok'
|
||||
/** 5-hour session window, null if not available. */
|
||||
session: RateLimitWindow | null
|
||||
/** 7-day weekly window, null if not available. */
|
||||
|
|
@ -95,6 +95,14 @@ export type InactiveAccountUsage = {
|
|||
isFetching: boolean
|
||||
}
|
||||
|
||||
export type GrokAccountStatus = {
|
||||
signedIn: boolean
|
||||
email: string | null
|
||||
teamId: string | null
|
||||
tokenFresh: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export type RateLimitState = {
|
||||
claude: ProviderRateLimits | null
|
||||
codex: ProviderRateLimits | null
|
||||
|
|
@ -102,6 +110,7 @@ export type RateLimitState = {
|
|||
opencodeGo: ProviderRateLimits | null
|
||||
kimi: ProviderRateLimits | null
|
||||
minimax: ProviderRateLimits | null
|
||||
grok: ProviderRateLimits | null
|
||||
/**
|
||||
* True when a MiniMax session cookie is persisted on disk. The cookie lives
|
||||
* outside GlobalSettings, so this flag is the durable signal that the
|
||||
|
|
@ -109,6 +118,8 @@ export type RateLimitState = {
|
|||
* between snapshot refreshes.
|
||||
*/
|
||||
minimaxCookieConfigured: boolean
|
||||
/** True when main finds a Grok CLI session file (~/.grok/auth.json or GROK_HOME). */
|
||||
grokAuthConfigured: boolean
|
||||
claudeTarget: RateLimitRuntimeTarget
|
||||
codexTarget: RateLimitRuntimeTarget
|
||||
inactiveClaudeAccounts: InactiveAccountUsage[]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const DEFAULT_STATUS_BAR_ITEMS: StatusBarItem[] = [
|
|||
'opencode-go',
|
||||
'kimi',
|
||||
'minimax',
|
||||
'grok',
|
||||
'ssh',
|
||||
'resource-usage',
|
||||
'ports'
|
||||
|
|
|
|||
|
|
@ -3154,6 +3154,7 @@ export type StatusBarItem =
|
|||
| 'opencode-go'
|
||||
| 'kimi'
|
||||
| 'minimax'
|
||||
| 'grok'
|
||||
| 'ssh'
|
||||
| 'resource-usage'
|
||||
| 'ports'
|
||||
|
|
@ -3269,6 +3270,8 @@ export type PersistedUIState = {
|
|||
_kimiStatusBarDefaultAdded?: boolean
|
||||
/** One-shot migration flag for adding the default-on MiniMax status item. */
|
||||
_minimaxStatusBarDefaultAdded?: boolean
|
||||
/** One-shot migration flag for adding the default-on Grok status item. */
|
||||
_grokStatusBarDefaultAdded?: boolean
|
||||
statusBarItems: StatusBarItem[]
|
||||
statusBarVisible: boolean
|
||||
dismissedUpdateVersion: string | null
|
||||
|
|
|
|||
Loading…
Reference in New Issue