mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 09:57:48 +00:00
feat(usage): add a Claude usage polling switch (#41921)
Add an on-by-default Claude usage polling switch with live-terminal-only behavior when disabled, cancellation and account attribution safeguards, and translations for all supported locales. Fixes #27151. Fixes #39502. Fixes #37037. Co-authored-by: Sibyl <scauyb@qq.com>
This commit is contained in:
@@ -14,7 +14,15 @@ Orca fetches usage for Claude Code, Codex, Gemini, OpenCode Go, Kimi Code, MiniM
|
||||
|
||||
Orca asks each provider for your usage over the network, using the login that agent already has or a key or session cookie you add in Orca Settings.
|
||||
|
||||
For Claude, Orca requests `https://api.anthropic.com/api/oauth/usage` with the token from your existing Claude Code login; it does not ask for extra credentials. Triggers include:
|
||||
### Claude usage polling
|
||||
|
||||
**Settings → Accounts → Claude → Claude usage polling** is on by default. Turn it off to stop Orca's requests to `https://api.anthropic.com/api/oauth/usage`, including manual refreshes, account switching, inactive-account previews, and refreshes from mobile or web clients. Turning it off cancels pending usage requests and removes their cached figures while retaining live terminal readings. A request already sent cannot be recalled.
|
||||
|
||||
With polling off, the meter uses Claude Code's [status-line data](https://code.claude.com/docs/en/statusline#rate-limit-usage): five-hour and weekly percentages and reset times. This requires a supported Claude Code version and Orca's status-line feed. Orca preserves a user-owned status line, so those sessions do not supply this feed. Without a reading, usage is unavailable until a terminal session reports it. Model-specific weekly windows, credits, and inactive-account readings require polling.
|
||||
|
||||
The persisted setting is `claudeOAuthUsageEnabled: false` to disable polling. It applies on the host running the agents, including a headless Orca server; configure each host separately. Hiding the Claude status-bar item controls visibility independently.
|
||||
|
||||
When polling is enabled, Orca uses the token from your existing Claude Code login. Triggers include:
|
||||
|
||||
- Once the Orca window is active after startup, then about every 15 minutes while it stays visible and focused.
|
||||
- Bringing the window back to the front when the last check is more than 5 minutes old (sooner after a failed check).
|
||||
|
||||
@@ -69,6 +69,18 @@ export function createProviderAccountServices(
|
||||
)
|
||||
store.onSettingsChanged((updates, settings) => {
|
||||
void agentView.reconcile(settings)
|
||||
if (updates.claudeOAuthUsageEnabled === false) {
|
||||
rateLimits.discardClaudeOAuthUsage()
|
||||
} else if (updates.claudeOAuthUsageEnabled === true) {
|
||||
void rateLimits
|
||||
.refreshClaudeForTarget(rateLimits.getState().claudeTarget)
|
||||
.catch((error: unknown) => {
|
||||
console.warn(
|
||||
'[rate-limits] Failed to refresh Claude usage after enabling polling:',
|
||||
error
|
||||
)
|
||||
})
|
||||
}
|
||||
// Auto runtime selection remains live as provider settings change.
|
||||
void syncAccountRuntimeTargets(updates, settings).catch((error) =>
|
||||
console.warn('[rate-limits] Failed to apply account runtime target:', error)
|
||||
@@ -137,6 +149,7 @@ export function createProviderAccountServices(
|
||||
apiKey: readZcodePlanApiKey() ?? ''
|
||||
}))
|
||||
rateLimits.setGeminiCliOAuthEnabledResolver(() => store.getSettings().geminiCliOAuthEnabled)
|
||||
rateLimits.setClaudeOAuthUsageEnabledResolver(() => store.getSettings().claudeOAuthUsageEnabled)
|
||||
// Reuse the meter switch so hidden Antigravity usage does not spawn agy.
|
||||
rateLimits.setAntigravityUsageEnabledResolver(() =>
|
||||
store.getUI().statusBarItems.includes('antigravity')
|
||||
|
||||
@@ -30,6 +30,14 @@ export function makeClaudeUsageResult(
|
||||
}
|
||||
}
|
||||
|
||||
// Not idle: the meter draws idle as pending, and nothing here will settle it.
|
||||
export function claudeOAuthUsageDisabledResult(): ProviderRateLimits {
|
||||
return makeClaudeUsageResult('unavailable', 'Usage appears when a Claude session runs.', {
|
||||
failureKind: 'usage-unavailable',
|
||||
attemptedSources: []
|
||||
})
|
||||
}
|
||||
|
||||
// Account rows explain host/profile failures; usage keeps its generic message.
|
||||
export function claudeUsageUnavailable(): ProviderRateLimits {
|
||||
return makeClaudeUsageResult('error', 'Claude usage is unavailable right now.', {
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
OpenCodeGoRateLimitConfig,
|
||||
MiniMaxRateLimitConfig,
|
||||
AntigravityUsageEnabledResolver,
|
||||
ClaudeOAuthUsageEnabledResolver,
|
||||
MuseUsageEnabledResolver,
|
||||
ZcodePlanRateLimitConfig,
|
||||
GeminiCliOAuthEnabledResolver,
|
||||
@@ -73,6 +74,27 @@ export class RateLimitService extends RateLimitServiceAccountRefresh {
|
||||
this.geminiCliOAuthEnabledResolver = resolver
|
||||
}
|
||||
|
||||
setClaudeOAuthUsageEnabledResolver(resolver: ClaudeOAuthUsageEnabledResolver): void {
|
||||
this.claudeOAuthUsageEnabledResolver = resolver
|
||||
}
|
||||
|
||||
// Retain only readings actually received from the terminal feed.
|
||||
discardClaudeOAuthUsage(): void {
|
||||
this.claudeUsageAbortController.abort()
|
||||
this.claudeUsageAbortController = new AbortController()
|
||||
this.claudeFetchGeneration += 1
|
||||
this.inactiveClaudeAccountsGeneration += 1
|
||||
this.inactiveClaudeCache.clear()
|
||||
this.inactiveClaudeFetching.clear()
|
||||
this.lastInactiveClaudeFetchAt = 0
|
||||
this.lastClaudeCheckCompletedAt = null
|
||||
this.activeFailureStreakByProvider.claude = 0
|
||||
this.updateState({ ...this.state, claude: this.claudeUsageWithoutOAuth() })
|
||||
if (!this.lastClaudeAuthSnapshot) {
|
||||
void this.fetchClaudeOnly({ force: true })
|
||||
}
|
||||
}
|
||||
|
||||
setAntigravityUsageEnabledResolver(resolver: AntigravityUsageEnabledResolver): void {
|
||||
this.antigravityUsageEnabledResolver = resolver
|
||||
}
|
||||
@@ -162,6 +184,10 @@ export class RateLimitService extends RateLimitServiceAccountRefresh {
|
||||
void this.fetchAll()
|
||||
} else {
|
||||
this.scheduleDeferredStartupRefresh()
|
||||
// Passive terminal usage needs account attribution even without a usage viewer.
|
||||
if (!this.isClaudeOAuthUsageEnabled()) {
|
||||
void this.fetchClaudeOnly({ force: true })
|
||||
}
|
||||
}
|
||||
this.startTimer()
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ export abstract class RateLimitServiceAccountRefresh extends RateLimitServiceIna
|
||||
provider: 'claude' | 'codex',
|
||||
options: { notWithinMs?: number } = {}
|
||||
): boolean {
|
||||
if (provider === 'claude' && !this.isClaudeOAuthUsageEnabled()) {
|
||||
return false
|
||||
}
|
||||
const limits = this.state[provider]
|
||||
const now = Date.now()
|
||||
if (this.isFetching || this.isRetryAfterActive(limits)) {
|
||||
@@ -230,6 +233,8 @@ export abstract class RateLimitServiceAccountRefresh extends RateLimitServiceIna
|
||||
this.activeFailureStreakByProvider.claude = 0
|
||||
// Outgoing statusline posts must not land on the incoming account during the switch.
|
||||
this.lastClaudeAuthSnapshot = null
|
||||
this.liveClaudeUsage = null
|
||||
this.usageReadingAccounts.claude = undefined
|
||||
this.lastInactiveClaudeFetchAt = 0
|
||||
this.updateState({
|
||||
...this.state,
|
||||
@@ -249,6 +254,8 @@ export abstract class RateLimitServiceAccountRefresh extends RateLimitServiceIna
|
||||
if (targetChanged) {
|
||||
// Outgoing target posts must not land on the incoming target during the switch.
|
||||
this.lastClaudeAuthSnapshot = null
|
||||
this.liveClaudeUsage = null
|
||||
this.usageReadingAccounts.claude = undefined
|
||||
}
|
||||
this.updateState({
|
||||
...this.state,
|
||||
|
||||
@@ -15,10 +15,24 @@ import {
|
||||
isStaleClaudeUsageWindow,
|
||||
mapClaudeUsageWindow
|
||||
} from '../providers/claude/claude-usage-window'
|
||||
import { claudeOAuthUsageDisabledResult } from '../providers/claude/claude-usage-result'
|
||||
import { readUserClaudeConfigDir } from '../../claude-accounts/profiles/claude-profile-paths'
|
||||
import { getClaudeProfileRouter } from '../../claude-accounts/profiles/routing/claude-profile-installed-router'
|
||||
|
||||
export abstract class RateLimitServiceFetchPolicy extends RateLimitServiceFetchTargets {
|
||||
protected isClaudeOAuthUsageEnabled(): boolean {
|
||||
return this.claudeOAuthUsageEnabledResolver?.() ?? true
|
||||
}
|
||||
|
||||
protected claudeUsageFetchSignal(signal: AbortSignal): AbortSignal {
|
||||
return AbortSignal.any([signal, this.claudeUsageAbortController.signal])
|
||||
}
|
||||
|
||||
protected claudeUsageWithoutOAuth(): ProviderRateLimits {
|
||||
const live = this.liveClaudeUsage ? withoutResetWindows(this.liveClaudeUsage, Date.now()) : null
|
||||
return live?.session || live?.weekly ? live : claudeOAuthUsageDisabledResult()
|
||||
}
|
||||
|
||||
protected getMiniMaxCredentialError(message: string): ProviderRateLimits {
|
||||
return {
|
||||
provider: 'minimax',
|
||||
@@ -44,6 +58,12 @@ export abstract class RateLimitServiceFetchPolicy extends RateLimitServiceFetchT
|
||||
}
|
||||
}
|
||||
|
||||
protected claudeStateAtCycleStart(limits: ProviderRateLimits | null): ProviderRateLimits {
|
||||
return this.isClaudeOAuthUsageEnabled()
|
||||
? this.withFetchingStatus(limits, 'claude')
|
||||
: this.claudeUsageWithoutOAuth()
|
||||
}
|
||||
|
||||
// Retry-After remains binding even when live posts restore ok display; early polls waste budget and prolong 429s.
|
||||
protected isRetryAfterActive(limits: ProviderRateLimits | null): boolean {
|
||||
return Boolean(limits?.usageMetadata?.retryAtMs && limits.usageMetadata.retryAtMs > Date.now())
|
||||
@@ -70,6 +90,9 @@ export abstract class RateLimitServiceFetchPolicy extends RateLimitServiceFetchT
|
||||
fresh: ProviderRateLimits,
|
||||
previous: ProviderRateLimits | null
|
||||
): ProviderRateLimits {
|
||||
if (!this.isClaudeOAuthUsageEnabled()) {
|
||||
return this.claudeUsageWithoutOAuth()
|
||||
}
|
||||
this.lastClaudeCheckCompletedAt = fresh.updatedAt
|
||||
// Failed OAuth must preserve newer live display while forwarding Retry-After to the poll gate.
|
||||
const current = this.state.claude
|
||||
@@ -94,15 +117,23 @@ export abstract class RateLimitServiceFetchPolicy extends RateLimitServiceFetchT
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.lastClaudeAuthSnapshot = {
|
||||
const snapshot = {
|
||||
// System Default uses the user config folder without injecting a managed one.
|
||||
configDir: normalizeClaudeConfigDir(
|
||||
authPreparation?.envPatch.CLAUDE_CONFIG_DIR ??
|
||||
getClaudeProfileRouter()?.userConfigDir() ??
|
||||
readUserClaudeConfigDir(process.env)
|
||||
),
|
||||
home: authPreparation?.configDir,
|
||||
provenance: authPreparation?.provenance ?? 'system'
|
||||
}
|
||||
if (
|
||||
snapshot.configDir !== this.lastClaudeAuthSnapshot?.configDir ||
|
||||
snapshot.provenance !== this.lastClaudeAuthSnapshot?.provenance
|
||||
) {
|
||||
this.liveClaudeUsage = null
|
||||
}
|
||||
this.lastClaudeAuthSnapshot = snapshot
|
||||
}
|
||||
|
||||
ingestLiveClaudeRateLimits(event: ClaudeStatusLineRateLimits): void {
|
||||
@@ -155,6 +186,26 @@ export abstract class RateLimitServiceFetchPolicy extends RateLimitServiceFetchT
|
||||
) {
|
||||
return
|
||||
}
|
||||
const priorLive = this.liveClaudeUsage ? withoutResetWindows(this.liveClaudeUsage, now) : null
|
||||
this.liveClaudeUsage = {
|
||||
provider: 'claude',
|
||||
session: freshSession ?? priorLive?.session ?? null,
|
||||
weekly: freshWeekly ?? priorLive?.weekly ?? null,
|
||||
updatedAt: now,
|
||||
error: null,
|
||||
status: 'ok',
|
||||
usageMetadata: {
|
||||
source: 'live-session',
|
||||
lastSuccessfulSource: 'live-session',
|
||||
authProvenance: snapshot.provenance
|
||||
}
|
||||
}
|
||||
this.rememberUsageReadingAccount(
|
||||
'claude',
|
||||
this.claudeFetchTarget,
|
||||
snapshot.home,
|
||||
snapshot.provenance
|
||||
)
|
||||
if (
|
||||
previous?.status === 'ok' &&
|
||||
previous.usageMetadata?.source === 'live-session' &&
|
||||
|
||||
@@ -153,7 +153,7 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ
|
||||
|
||||
this.updateState({
|
||||
...previousState,
|
||||
claude: this.withFetchingStatus(previousState.claude, 'claude'),
|
||||
claude: this.claudeStateAtCycleStart(previousState.claude),
|
||||
// A gated slot makes no attempt and cannot settle a fetching chip.
|
||||
codex: codexFetchGated
|
||||
? codexStateBeforeFetch
|
||||
@@ -220,7 +220,9 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ
|
||||
|
||||
// Live display does not waive automated Retry-After gates.
|
||||
const claudeFetchGated =
|
||||
!options?.force && this.shouldSkipAutomatedFetch('claude', previousState.claude)
|
||||
!this.isClaudeOAuthUsageEnabled() ||
|
||||
claudeGeneration !== this.claudeFetchGeneration ||
|
||||
(!options?.force && this.shouldSkipAutomatedFetch('claude', previousState.claude))
|
||||
|
||||
const [claudeResult, codexResult, geminiResult, opencodeGoResult, kimiResult, miniMaxResult] =
|
||||
await Promise.allSettled([
|
||||
@@ -231,7 +233,7 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ
|
||||
allowPtyFallback: this.shouldAllowClaudePtyFallback(claudeAuthPreparation),
|
||||
allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(),
|
||||
networkProxySettings: this.networkProxySettingsResolver?.(),
|
||||
signal
|
||||
signal: this.claudeUsageFetchSignal(signal)
|
||||
}),
|
||||
codexFetchGated
|
||||
? Promise.resolve(previousState.codex as ProviderRateLimits)
|
||||
|
||||
@@ -6,6 +6,9 @@ import { delayUnlessAborted } from '../../../shared/abort-aware-delay'
|
||||
|
||||
export abstract class RateLimitServiceInactiveAccounts extends RateLimitServicePolling {
|
||||
async fetchInactiveClaudeAccountsOnOpen(): Promise<void> {
|
||||
if (!this.isClaudeOAuthUsageEnabled()) {
|
||||
return
|
||||
}
|
||||
if (Date.now() - this.lastInactiveClaudeFetchAt < INACTIVE_FETCH_DEBOUNCE_MS) {
|
||||
return
|
||||
}
|
||||
@@ -21,7 +24,7 @@ export abstract class RateLimitServiceInactiveAccounts extends RateLimitServiceP
|
||||
}
|
||||
const fetchGeneration = this.inactiveClaudeAccountsGeneration
|
||||
const controller = this.beginFetchCycle()
|
||||
const signal = controller.signal
|
||||
const signal = this.claudeUsageFetchSignal(controller.signal)
|
||||
|
||||
for (const account of accounts) {
|
||||
this.inactiveClaudeFetching.add(account.id)
|
||||
|
||||
@@ -87,6 +87,7 @@ export abstract class RateLimitServicePolling extends RateLimitServiceFetchQueue
|
||||
const museEnabled = this.museUsageEnabledResolver?.() ?? true
|
||||
return Object.entries(byProvider)
|
||||
.filter(([provider]) => provider !== 'muse' || museEnabled)
|
||||
.filter(([provider]) => provider !== 'claude' || this.isClaudeOAuthUsageEnabled())
|
||||
.map(([provider, limits]) => ({
|
||||
provider: provider as ActiveRateLimitProvider,
|
||||
limits
|
||||
|
||||
@@ -94,10 +94,15 @@ export abstract class RateLimitServiceProviderCycles extends RateLimitServiceFul
|
||||
// Why: capture before the resolver await so an account switch during it invalidates both the snapshot and the state apply.
|
||||
const claudeGeneration = this.claudeFetchGeneration
|
||||
const claudeAuthPreparation = await this.claudeAuthPreparationResolver?.(claudeTarget)
|
||||
if (signal.aborted) {
|
||||
if (signal.aborted || claudeGeneration !== this.claudeFetchGeneration) {
|
||||
return
|
||||
}
|
||||
this.rememberClaudeAuthSnapshot(claudeAuthPreparation, claudeGeneration, claudeTarget)
|
||||
// After the snapshot: statusLine posts are dropped until one names the selected account.
|
||||
if (!this.isClaudeOAuthUsageEnabled()) {
|
||||
this.updateState({ ...this.state, claude: this.claudeUsageWithoutOAuth() })
|
||||
return
|
||||
}
|
||||
const claudeProvenance = claudeAuthPreparation?.provenance ?? 'system'
|
||||
const previousState = this.state
|
||||
|
||||
@@ -111,7 +116,7 @@ export abstract class RateLimitServiceProviderCycles extends RateLimitServiceFul
|
||||
allowPtyFallback: this.shouldAllowClaudePtyFallback(claudeAuthPreparation),
|
||||
allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(),
|
||||
networkProxySettings: this.networkProxySettingsResolver?.(),
|
||||
signal
|
||||
signal: this.claudeUsageFetchSignal(signal)
|
||||
}).catch((err): ProviderRateLimits => ({
|
||||
provider: 'claude',
|
||||
session: null,
|
||||
|
||||
@@ -2,10 +2,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ProviderRateLimits } from '../../../shared/rate-limit-types'
|
||||
import { RateLimitService } from './rate-limit-service'
|
||||
import { fetchActiveClaudeRateLimits } from '../providers/claude/claude-active-usage-fetch'
|
||||
import { fetchInactiveClaudeAccountUsage } from '../providers/claude/claude-managed-account-usage'
|
||||
import { fetchCodexRateLimits } from '../providers/codex/codex-fetcher'
|
||||
import {
|
||||
deferred,
|
||||
flushMicrotasks,
|
||||
mockFreshBackgroundProviderFetches,
|
||||
okProvider,
|
||||
resetRateLimitProviderMocks
|
||||
} from './rate-limit-service-test-harness'
|
||||
@@ -95,4 +97,213 @@ describe('RateLimitService', () => {
|
||||
await flushMicrotasks(60)
|
||||
expect(service.getState().claude?.session?.usedPercent).toBe(55)
|
||||
})
|
||||
|
||||
it('keeps live Claude usage through refreshes without reading OAuth usage when disabled', async () => {
|
||||
mockFreshBackgroundProviderFetches()
|
||||
const service = new RateLimitService()
|
||||
service.setClaudeOAuthUsageEnabledResolver(() => false)
|
||||
service.setInactiveClaudeAccountsResolver(() => [{ id: 'other' }])
|
||||
let home = '/claude/account-a'
|
||||
service.setClaudeAuthPreparationResolver(async () => ({
|
||||
configDir: home,
|
||||
envPatch: { CLAUDE_CONFIG_DIR: home },
|
||||
provenance: home
|
||||
}))
|
||||
service.start({ fetchImmediately: false })
|
||||
await flushMicrotasks(20)
|
||||
service.stop()
|
||||
expect(service.getState().claude).toMatchObject({
|
||||
session: null,
|
||||
weekly: null,
|
||||
status: 'unavailable'
|
||||
})
|
||||
service.ingestLiveClaudeRateLimits({
|
||||
configDir: '/claude/account-a',
|
||||
fiveHour: { used_percentage: 33 },
|
||||
sevenDay: { used_percentage: 62 }
|
||||
})
|
||||
await service.refreshClaudeForTarget()
|
||||
await service.refresh()
|
||||
await service.fetchInactiveClaudeAccountsOnOpen()
|
||||
|
||||
expect(service.getState().claude).toMatchObject({
|
||||
session: { usedPercent: 33 },
|
||||
weekly: { usedPercent: 62 },
|
||||
status: 'ok',
|
||||
usageMetadata: { source: 'live-session' }
|
||||
})
|
||||
expect(service.getState().inactiveClaudeAccounts).toEqual([])
|
||||
expect(service.usageReadingAccount('claude')).toEqual({
|
||||
home: '/claude/account-a',
|
||||
provenance: '/claude/account-a'
|
||||
})
|
||||
expect(service.requestUsageReading('claude')).toBe(false)
|
||||
expect(fetchActiveClaudeRateLimits).not.toHaveBeenCalled()
|
||||
expect(fetchInactiveClaudeAccountUsage).not.toHaveBeenCalled()
|
||||
|
||||
home = '/claude/account-b'
|
||||
await service.refreshForClaudeAccountChange('outgoing')
|
||||
expect(service.getState().claude?.session).toBeNull()
|
||||
expect(service.getState().claude?.status).toBe('unavailable')
|
||||
service.ingestLiveClaudeRateLimits({
|
||||
configDir: '/claude/account-b',
|
||||
fiveHour: { used_percentage: 12 },
|
||||
sevenDay: null
|
||||
})
|
||||
expect(service.getState().claude?.session?.usedPercent).toBe(12)
|
||||
expect(service.usageReadingAccount('claude')).toEqual({
|
||||
home: '/claude/account-b',
|
||||
provenance: '/claude/account-b'
|
||||
})
|
||||
})
|
||||
|
||||
it('cancels pending Claude usage and rejects its reply even after polling is enabled again', async () => {
|
||||
mockFreshBackgroundProviderFetches()
|
||||
const service = new RateLimitService()
|
||||
let enabled = true
|
||||
service.setClaudeOAuthUsageEnabledResolver(() => enabled)
|
||||
const started = deferred<AbortSignal | undefined>()
|
||||
const reply = deferred<ProviderRateLimits>()
|
||||
vi.mocked(fetchActiveClaudeRateLimits).mockImplementationOnce((options) => {
|
||||
started.resolve(options?.signal)
|
||||
return reply.promise
|
||||
})
|
||||
const refresh = service.refresh()
|
||||
const signal = await started.promise
|
||||
|
||||
enabled = false
|
||||
service.discardClaudeOAuthUsage()
|
||||
expect(signal?.aborted).toBe(true)
|
||||
service.ingestLiveClaudeRateLimits({
|
||||
configDir: null,
|
||||
fiveHour: { used_percentage: 41 },
|
||||
sevenDay: null
|
||||
})
|
||||
enabled = true
|
||||
reply.resolve(okProvider('claude', 90))
|
||||
await refresh
|
||||
|
||||
expect(service.getState().claude).toMatchObject({
|
||||
session: { usedPercent: 41 },
|
||||
status: 'ok',
|
||||
usageMetadata: { source: 'live-session' }
|
||||
})
|
||||
})
|
||||
|
||||
it('retains only terminal-observed windows when disabling OAuth-enriched usage', async () => {
|
||||
mockFreshBackgroundProviderFetches()
|
||||
const service = new RateLimitService()
|
||||
let enabled = true
|
||||
service.setClaudeOAuthUsageEnabledResolver(() => enabled)
|
||||
vi.mocked(fetchActiveClaudeRateLimits).mockResolvedValueOnce({
|
||||
...okProvider('claude', 20),
|
||||
weekly: { usedPercent: 50, windowMinutes: 10080, resetsAt: null, resetDescription: null },
|
||||
fableWeekly: {
|
||||
usedPercent: 70,
|
||||
windowMinutes: 10080,
|
||||
resetsAt: null,
|
||||
resetDescription: null
|
||||
},
|
||||
extraUsage: {
|
||||
unit: 'credits',
|
||||
enabled: true,
|
||||
balance: 100,
|
||||
unlimited: false,
|
||||
disabledReason: null,
|
||||
resetsAt: null
|
||||
}
|
||||
})
|
||||
await service.refresh()
|
||||
service.ingestLiveClaudeRateLimits({
|
||||
configDir: null,
|
||||
fiveHour: { used_percentage: 35 },
|
||||
sevenDay: null
|
||||
})
|
||||
expect(service.getState().claude?.weekly?.usedPercent).toBe(50)
|
||||
|
||||
enabled = false
|
||||
service.discardClaudeOAuthUsage()
|
||||
await service.refresh()
|
||||
|
||||
expect(service.getState().claude).toMatchObject({
|
||||
session: { usedPercent: 35 },
|
||||
weekly: null,
|
||||
usageMetadata: { source: 'live-session' }
|
||||
})
|
||||
expect(service.getState().claude?.fableWeekly).toBeFalsy()
|
||||
expect(service.getState().claude?.extraUsage).toBeFalsy()
|
||||
await service.refreshForClaudeAccountChange('outgoing')
|
||||
expect(service.getState().claude?.session).toBeNull()
|
||||
})
|
||||
|
||||
it('discards pending inactive-account usage when polling is disabled', async () => {
|
||||
const service = new RateLimitService()
|
||||
let enabled = true
|
||||
service.setClaudeOAuthUsageEnabledResolver(() => enabled)
|
||||
service.setInactiveClaudeAccountsResolver(() => [{ id: 'other' }, { id: 'another' }])
|
||||
const started = deferred<AbortSignal | undefined>()
|
||||
const reply = deferred<ProviderRateLimits>()
|
||||
vi.mocked(fetchInactiveClaudeAccountUsage).mockImplementationOnce((_account, options) => {
|
||||
started.resolve(options?.signal)
|
||||
return reply.promise
|
||||
})
|
||||
const refresh = service.fetchInactiveClaudeAccountsOnOpen()
|
||||
const signal = await started.promise
|
||||
|
||||
enabled = false
|
||||
service.discardClaudeOAuthUsage()
|
||||
expect(signal?.aborted).toBe(true)
|
||||
enabled = true
|
||||
reply.resolve(okProvider('claude', 90))
|
||||
await refresh
|
||||
|
||||
expect(service.getState().inactiveClaudeAccounts).toEqual([])
|
||||
expect(service.getState().claude).toMatchObject({
|
||||
session: null,
|
||||
weekly: null,
|
||||
status: 'unavailable'
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['all', 'claude'] as const)(
|
||||
'does not revive a canceled %s request still preparing account attribution',
|
||||
async (cycle) => {
|
||||
mockFreshBackgroundProviderFetches()
|
||||
const service = new RateLimitService()
|
||||
let enabled = true
|
||||
service.setClaudeOAuthUsageEnabledResolver(() => enabled)
|
||||
const oldAuth = { configDir: '/claude/old', envPatch: {}, provenance: 'old' }
|
||||
const preparation = deferred<typeof oldAuth>()
|
||||
service.setClaudeAuthPreparationResolver(
|
||||
vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(preparation.promise)
|
||||
.mockResolvedValue({
|
||||
configDir: '/claude/current',
|
||||
envPatch: { CLAUDE_CONFIG_DIR: '/claude/current' },
|
||||
provenance: 'current'
|
||||
})
|
||||
)
|
||||
const refresh = cycle === 'all' ? service.refresh() : service.refreshClaudeForTarget()
|
||||
enabled = false
|
||||
service.discardClaudeOAuthUsage()
|
||||
enabled = true
|
||||
preparation.resolve(oldAuth)
|
||||
await refresh
|
||||
|
||||
expect(fetchActiveClaudeRateLimits).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ authPreparation: oldAuth })
|
||||
)
|
||||
service.ingestLiveClaudeRateLimits({
|
||||
configDir: '/claude/current',
|
||||
fiveHour: { used_percentage: 15 },
|
||||
sevenDay: null
|
||||
})
|
||||
expect(service.getState().claude?.session?.usedPercent).toBe(15)
|
||||
expect(service.usageReadingAccount('claude')).toEqual({
|
||||
home: '/claude/current',
|
||||
provenance: 'current'
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type OpenCodeGoRateLimitConfig,
|
||||
type MiniMaxRateLimitConfig,
|
||||
type AntigravityUsageEnabledResolver,
|
||||
type ClaudeOAuthUsageEnabledResolver,
|
||||
type MuseUsageEnabledResolver,
|
||||
type ZcodePlanRateLimitConfig,
|
||||
type GeminiCliOAuthEnabledResolver,
|
||||
@@ -90,10 +91,16 @@ export abstract class RateLimitServiceState {
|
||||
protected fetchIdleResolvers: (() => void)[] = []
|
||||
protected codexFetchGeneration = 0
|
||||
protected claudeFetchGeneration = 0
|
||||
protected claudeUsageAbortController = new AbortController()
|
||||
protected liveClaudeUsage: ProviderRateLimits | null = null
|
||||
// Why: cached display receipts cannot establish when the selected account was last checked.
|
||||
protected lastClaudeCheckCompletedAt: number | null = null
|
||||
// Capture account attribution once per cycle instead of rerunning auth preparation on every post.
|
||||
protected lastClaudeAuthSnapshot: { configDir: string | null; provenance: string } | null = null
|
||||
protected lastClaudeAuthSnapshot: {
|
||||
configDir: string | null
|
||||
home: string | undefined
|
||||
provenance: string
|
||||
} | null = null
|
||||
/** Where the applied host-runtime Claude/Codex readings were taken. */
|
||||
protected usageReadingAccounts: Partial<
|
||||
Record<'claude' | 'codex', { home: string; provenance: string }>
|
||||
@@ -122,6 +129,7 @@ export abstract class RateLimitServiceState {
|
||||
protected miniMaxConfigResolver: (() => MiniMaxRateLimitConfig) | null = null
|
||||
protected zcodePlanConfigResolver: (() => ZcodePlanRateLimitConfig) | null = null
|
||||
protected geminiCliOAuthEnabledResolver: GeminiCliOAuthEnabledResolver | null = null
|
||||
protected claudeOAuthUsageEnabledResolver: ClaudeOAuthUsageEnabledResolver | null = null
|
||||
protected antigravityUsageEnabledResolver: AntigravityUsageEnabledResolver | null = null
|
||||
protected antigravityFetchTarget: LocalAccountRuntimeTarget = { runtime: 'host', wslDistro: null }
|
||||
protected antigravityAccountUsageRecorder: AntigravityAccountUsageRecorder | null = null
|
||||
|
||||
@@ -62,6 +62,7 @@ export type ZcodePlanResolvedConfig = {
|
||||
export type GeminiCliOAuthEnabledResolver = () => boolean
|
||||
|
||||
export type AntigravityUsageEnabledResolver = () => boolean
|
||||
export type ClaudeOAuthUsageEnabledResolver = () => boolean
|
||||
export type MuseUsageEnabledResolver = () => boolean
|
||||
export type ActiveRateLimitProvider = ProviderRateLimits['provider']
|
||||
export type ActiveProviderState = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ButtonGroup } from '../../../ui/button-group'
|
||||
import { Label } from '../../../ui/label'
|
||||
import { ClaudeIcon } from '../../../status-bar/icons'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { ClaudeOAuthUsageSetting } from './claude-oauth-usage-setting'
|
||||
import { ClaudeRunningSessionSwitchSetting } from './claude-running-session-switch-setting'
|
||||
import {
|
||||
getProviderAccountRuntime,
|
||||
@@ -45,6 +46,7 @@ export function renderClaudeAccountsSection(
|
||||
claudeAccounts,
|
||||
claudeAction,
|
||||
isRemoteAccountScope,
|
||||
recordFeatureInteraction,
|
||||
remoteAccountScopeNotice,
|
||||
runClaudeAccountAction,
|
||||
setRemoveClaudeTarget,
|
||||
@@ -383,6 +385,13 @@ export function renderClaudeAccountsSection(
|
||||
)}
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
{isRemoteAccountScope ? null : (
|
||||
<ClaudeOAuthUsageSetting
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
recordFeatureInteraction={recordFeatureInteraction}
|
||||
/>
|
||||
)}
|
||||
{isRemoteAccountScope ? null : (
|
||||
<ClaudeRunningSessionSwitchSetting settings={settings} updateSettings={updateSettings} />
|
||||
)}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { defineFixture } from '../../../../component-fixtures/component-fixture'
|
||||
import { getDefaultSettings } from '../../../../../../shared/constants'
|
||||
import { useAppStore } from '../../../../store'
|
||||
import { SettingsGroup } from '../../section-layout/SettingsGroup'
|
||||
import { ClaudeOAuthUsageSetting } from './claude-oauth-usage-setting'
|
||||
|
||||
function UsageSettingFixture(): React.JSX.Element | null {
|
||||
const settings = useAppStore((state) => state.settings)
|
||||
const updateSettings = useAppStore((state) => state.updateSettings)
|
||||
if (!settings) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="settings-view-shell bg-background p-8 text-foreground">
|
||||
<SettingsGroup title="Claude">
|
||||
<ClaudeOAuthUsageSetting
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
recordFeatureInteraction={() => {}}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const on = defineFixture({ width: 720, render: () => <UsageSettingFixture /> })
|
||||
|
||||
export const off = defineFixture({
|
||||
...on,
|
||||
store: { settings: { ...getDefaultSettings('/fixture'), claudeOAuthUsageEnabled: false } }
|
||||
})
|
||||
|
||||
export const narrow = defineFixture({ ...off, width: 360 })
|
||||
|
||||
export const infoTooltip = defineFixture({
|
||||
...on,
|
||||
interact: (frame) => {
|
||||
frame
|
||||
.querySelector<HTMLButtonElement>('button[aria-label="About Claude usage polling"]')
|
||||
?.focus()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Info } from 'lucide-react'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { Label } from '../../../ui/label'
|
||||
import { Switch } from '../../../ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import type { AccountsPaneSectionModel } from '../accounts-pane-types'
|
||||
|
||||
type ClaudeOAuthUsageSettingProps = Pick<
|
||||
AccountsPaneSectionModel,
|
||||
'settings' | 'updateSettings' | 'recordFeatureInteraction'
|
||||
>
|
||||
|
||||
export function ClaudeOAuthUsageSetting({
|
||||
settings,
|
||||
updateSettings,
|
||||
recordFeatureInteraction
|
||||
}: ClaudeOAuthUsageSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const title = t('accounts.claude.oauthUsage.title', 'Claude usage polling')
|
||||
const description = t(
|
||||
'accounts.claude.oauthUsage.description',
|
||||
'Uses your Claude Code sign-in to check usage, credits, and inactive accounts. Turn off to use live terminal readings only.'
|
||||
)
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={['claude', 'usage', 'oauth', 'credentials', 'rate limit', 'status bar']}
|
||||
className="flex items-center justify-between gap-4 py-3"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<Label htmlFor="claude-oauth-usage" className="min-w-0">
|
||||
<span className="truncate">{title}</span>
|
||||
</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={t('accounts.claude.oauthUsage.about', 'About Claude usage polling')}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Info className="size-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4} className="max-w-xs">
|
||||
{description}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span id="claude-oauth-usage-description" className="sr-only">
|
||||
{description}
|
||||
</span>
|
||||
<Switch
|
||||
id="claude-oauth-usage"
|
||||
aria-describedby="claude-oauth-usage-description"
|
||||
checked={settings.claudeOAuthUsageEnabled !== false}
|
||||
onCheckedChange={(checked) => {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
updateSettings({ claudeOAuthUsageEnabled: checked })
|
||||
}}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
)
|
||||
}
|
||||
@@ -21958,6 +21958,11 @@
|
||||
"reasonSharedSession": "Auch in einem anderen Bereich geöffnet",
|
||||
"sharedOne": "1 Claude-Sitzung bleibt auf {{previous}}, weil dieselbe Unterhaltung in einem anderen Bereich geöffnet ist. Ein Neustart könnte die Unterhaltung des anderen Bereichs erneut öffnen.",
|
||||
"sharedOther": "{{count}} Claude-Sitzungen bleiben auf {{previous}}, weil dieselbe Unterhaltung in einem anderen Bereich geöffnet ist. Ein Neustart könnte die Unterhaltung des anderen Bereichs erneut öffnen."
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Claude-Nutzungsabfrage",
|
||||
"description": "Verwendet deine Claude-Code-Anmeldung, um Nutzung, Guthaben und inaktive Konten abzufragen. Deaktiviere diese Option, um nur Live-Daten aus Terminalsitzungen zu verwenden.",
|
||||
"about": "Über die Claude-Nutzungsabfrage"
|
||||
}
|
||||
},
|
||||
"managedData": {
|
||||
|
||||
@@ -23494,6 +23494,11 @@
|
||||
"body_one": "{{count}} Claude session is still running on {{previous}}. Restarting continues the conversation on {{next}}. Busy sessions wait for idle before restarting.",
|
||||
"body_other": "{{count}} Claude sessions are still running on {{previous}}. Restarting continues each conversation on {{next}}. Busy sessions wait for idle before restarting.",
|
||||
"restartCount_one": "Restart {{count}} session"
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Claude usage polling",
|
||||
"description": "Uses your Claude Code sign-in to check usage, credits, and inactive accounts. Turn off to use live terminal readings only.",
|
||||
"about": "About Claude usage polling"
|
||||
}
|
||||
},
|
||||
"managedData": {
|
||||
|
||||
@@ -21566,6 +21566,11 @@
|
||||
"reasonSharedSession": "También abierta en otro panel",
|
||||
"sharedOne": "1 sesión de Claude permanece en {{previous}} porque otro panel tiene abierta la misma conversación. Reiniciarla podría volver a abrir la conversación del otro panel.",
|
||||
"sharedOther": "{{count}} sesiones de Claude permanecen en {{previous}} porque otro panel tiene abierta la misma conversación. Reiniciarlas podría volver a abrir la conversación del otro panel."
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Consulta de uso de Claude",
|
||||
"description": "Usa tu inicio de sesión de Claude Code para consultar el uso, los créditos y las cuentas inactivas. Desactívala para usar solo las lecturas en directo de las sesiones de terminal.",
|
||||
"about": "Acerca de la consulta de uso de Claude"
|
||||
}
|
||||
},
|
||||
"hostSignIn": {
|
||||
|
||||
@@ -22234,6 +22234,11 @@
|
||||
"reasonSharedSession": "Également ouverte dans un autre volet",
|
||||
"sharedOne": "1 session Claude reste sur {{previous}} car la même conversation est ouverte dans un autre volet. Un redémarrage pourrait rouvrir la conversation de l’autre volet.",
|
||||
"sharedOther": "{{count}} sessions Claude restent sur {{previous}} car la même conversation est ouverte dans un autre volet. Un redémarrage pourrait rouvrir la conversation de l’autre volet."
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Suivi de l’utilisation de Claude",
|
||||
"description": "Utilise votre connexion Claude Code pour consulter l’utilisation, les crédits et les comptes inactifs. Désactivez cette option pour utiliser uniquement les données en direct des sessions de terminal.",
|
||||
"about": "À propos du suivi de l’utilisation de Claude"
|
||||
}
|
||||
},
|
||||
"hostSignIn": {
|
||||
|
||||
@@ -21561,6 +21561,11 @@
|
||||
"reasonSharedSession": "別のペインでも開いています",
|
||||
"sharedOne": "別のペインで同じ会話が開いているため、1 件の Claude セッションは {{previous}} のままです。再起動すると別のペインの会話が再び開く可能性があります。",
|
||||
"sharedOther": "別のペインで同じ会話が開いているため、{{count}} 件の Claude セッションは {{previous}} のままです。再起動すると別のペインの会話が再び開く可能性があります。"
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Claude の使用量の定期取得",
|
||||
"description": "Claude Code のログイン情報を使って、使用量、クレジット、非アクティブなアカウントを確認します。オフにすると、実行中のターミナルセッションからの使用量データのみを表示します。",
|
||||
"about": "Claude の使用量の定期取得について"
|
||||
}
|
||||
},
|
||||
"hostSignIn": {
|
||||
|
||||
@@ -21553,6 +21553,11 @@
|
||||
"reasonSharedSession": "다른 패널에서도 열려 있음",
|
||||
"sharedOne": "다른 패널에서 같은 대화를 열고 있어 Claude 세션 1개가 {{previous}}에 유지됩니다. 다시 시작하면 다른 패널의 대화가 다시 열릴 수 있습니다.",
|
||||
"sharedOther": "다른 패널에서 같은 대화를 열고 있어 Claude 세션 {{count}}개가 {{previous}}에 유지됩니다. 다시 시작하면 다른 패널의 대화가 다시 열릴 수 있습니다."
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Claude 사용량 주기적 조회",
|
||||
"description": "Claude Code 로그인 정보로 사용량, 크레딧 및 비활성 계정을 확인합니다. 끄면 실행 중인 터미널 세션에서 제공하는 실시간 사용량만 표시합니다.",
|
||||
"about": "Claude 사용량 주기적 조회 정보"
|
||||
}
|
||||
},
|
||||
"hostSignIn": {
|
||||
|
||||
@@ -22931,6 +22931,11 @@
|
||||
"reasonSharedSession": "Também aberta em outro painel",
|
||||
"sharedOne": "1 sessão do Claude permanece em {{previous}} porque outro painel tem a mesma conversa aberta. Reiniciar pode reabrir a conversa do outro painel.",
|
||||
"sharedOther": "{{count}} sessões do Claude permanecem em {{previous}} porque outro painel tem a mesma conversa aberta. Reiniciar pode reabrir a conversa do outro painel."
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Consulta de uso do Claude",
|
||||
"description": "Usa seu login do Claude Code para consultar o uso, os créditos e as contas inativas. Desative para usar apenas as leituras em tempo real das sessões do terminal.",
|
||||
"about": "Sobre a consulta de uso do Claude"
|
||||
}
|
||||
},
|
||||
"managedData": {
|
||||
|
||||
@@ -22661,7 +22661,12 @@
|
||||
"sharedOne": "1 сеанс Claude остаётся на {{previous}}, поскольку тот же разговор открыт в другой панели. При перезапуске может снова открыться разговор из другой панели.",
|
||||
"sharedOther": "Сеансы Claude ({{count}}) остаются на {{previous}}, поскольку тот же разговор открыт в другой панели. При перезапуске может снова открыться разговор из другой панели."
|
||||
},
|
||||
"unnamedAccount": "Учётная запись Claude"
|
||||
"unnamedAccount": "Учётная запись Claude",
|
||||
"oauthUsage": {
|
||||
"title": "Опрос использования Claude",
|
||||
"description": "Использует ваш вход в Claude Code для проверки использования, кредитов и неактивных аккаунтов. Отключите, чтобы получать данные только из активных сеансов терминала.",
|
||||
"about": "Об опросе использования Claude"
|
||||
}
|
||||
},
|
||||
"managedData": {
|
||||
"description": "Добавляйте учётные записи этой командой в терминале сервера Orca. Выбор применяется к новым явным запускам агента на этом сервере; прямой SSH-ретранслятор и запуски WSL из Windows используют собственные учётные данные.",
|
||||
|
||||
@@ -21928,6 +21928,11 @@
|
||||
"reasonSharedSession": "Başka bir bölmede de açık",
|
||||
"sharedOne": "Aynı konuşma başka bir bölmede açık olduğu için 1 Claude oturumu {{previous}} üzerinde kalır. Yeniden başlatmak diğer bölmedeki konuşmayı yeniden açabilir.",
|
||||
"sharedOther": "Aynı konuşma başka bir bölmede açık olduğu için {{count}} Claude oturumu {{previous}} üzerinde kalır. Yeniden başlatmak diğer bölmedeki konuşmayı yeniden açabilir."
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Claude kullanımını sorgula",
|
||||
"description": "Kullanımı, kredileri ve etkin olmayan hesapları kontrol etmek için Claude Code oturumunuzu kullanır. Yalnızca çalışan terminal oturumlarından gelen canlı verileri kullanmak için kapatın.",
|
||||
"about": "Claude kullanım sorgulaması hakkında"
|
||||
}
|
||||
},
|
||||
"managedData": {
|
||||
|
||||
@@ -21631,6 +21631,11 @@
|
||||
"reasonSharedSession": "在另一個窗格中也已開啟",
|
||||
"sharedOne": "由於另一個窗格開啟了相同對話,1 個 Claude 工作階段會保留在 {{previous}}。重新啟動可能會重新開啟另一個窗格的對話。",
|
||||
"sharedOther": "由於另一個窗格開啟了相同對話,{{count}} 個 Claude 工作階段會保留在 {{previous}}。重新啟動可能會重新開啟另一個窗格的對話。"
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Claude 用量輪詢",
|
||||
"description": "使用 Claude Code 登入憑證查詢用量、額度和非使用中帳戶。關閉後,僅顯示執行中終端機工作階段提供的即時用量。",
|
||||
"about": "關於 Claude 用量輪詢"
|
||||
}
|
||||
},
|
||||
"managedData": {
|
||||
|
||||
@@ -21901,6 +21901,11 @@
|
||||
"reasonSharedSession": "在另一个窗格中也已打开",
|
||||
"sharedOne": "由于另一个窗格打开了同一对话,1 个 Claude 会话将保持在 {{previous}}。重新启动可能会重新打开另一个窗格的对话。",
|
||||
"sharedOther": "由于另一个窗格打开了同一对话,{{count}} 个 Claude 会话将保持在 {{previous}}。重新启动可能会重新打开另一个窗格的对话。"
|
||||
},
|
||||
"oauthUsage": {
|
||||
"title": "Claude 用量轮询",
|
||||
"description": "使用 Claude Code 登录凭据检查用量、额度和非活跃账号。关闭后,仅显示运行中终端会话提供的实时用量。",
|
||||
"about": "关于 Claude 用量轮询"
|
||||
}
|
||||
},
|
||||
"hostSignIn": {
|
||||
|
||||
@@ -249,6 +249,7 @@ export function buildDefaultSettings(args: {
|
||||
minimaxEndpoint: 'overseas',
|
||||
zcodePlanSite: 'zai',
|
||||
geminiCliOAuthEnabled: false,
|
||||
claudeOAuthUsageEnabled: true,
|
||||
agentCmdOverrides: {},
|
||||
// Arguments hold only the user's own text; the permission flag comes from the choice below.
|
||||
agentDefaultArgs: {},
|
||||
|
||||
@@ -375,6 +375,8 @@ export type GlobalSettings = GlobalSettingsBase & {
|
||||
zcodePlanSite?: ZcodePlanSite
|
||||
/** Extract OAuth credentials from the local Gemini CLI for rate-limit fetching. Off by default (explicit opt-in). */
|
||||
geminiCliOAuthEnabled: boolean
|
||||
/** Poll Claude usage with existing CLI credentials. Off uses live terminal readings only. */
|
||||
claudeOAuthUsageEnabled: boolean
|
||||
/** Per-agent CLI command overrides. A missing key means use the catalog default binary name. */
|
||||
agentCmdOverrides: Partial<Record<TuiAgent, string>>
|
||||
/** Off keeps each Codex home's history to itself: Orca links no sessions between accounts or
|
||||
|
||||
Reference in New Issue
Block a user