Files
orca/mobile/src/components/account-usage-state.ts
T

125 lines
4.1 KiB
TypeScript

// Why: keep these shapes in lockstep with src/shared/types.ts and
// src/shared/rate-limit-types.ts. We don't import from desktop here because
// the mobile bundle must not pull in Electron-coupled type files.
//
// Pure state/selectors live here (no React Native imports) so they can be
// unit-tested directly; AccountUsage.tsx re-exports them alongside the
// UsageBar component.
import { formatResetCountdown } from '../../../src/shared/rate-limit-reset-format'
import type {
AccountsSnapshot,
InactiveAccountUsage,
ProviderRateLimits
} from './accounts-snapshot'
export {
AccountsSnapshotSchema,
decodeAccountsSnapshot,
isInvalidAccountsSnapshotError,
ProviderRateLimitsSchema,
RateLimitRuntimeTargetSchema,
type AccountsSnapshot,
type ClaudeAccountSummary,
type CodexAccountSummary,
type InactiveAccountUsage,
type ProviderRateLimits,
type RateLimitRuntimeTarget,
type RateLimitWindow
} from './accounts-snapshot'
export type ProviderKey = 'claude' | 'codex'
export type UsageBarState = {
usedPercent: number | null
unavailable: boolean
loading: boolean
}
export function getActiveProviderRateLimits(
snapshot: AccountsSnapshot,
provider: ProviderKey
): ProviderRateLimits | null {
return provider === 'claude' ? snapshot.rateLimits.claude : snapshot.rateLimits.codex
}
export function getInactiveProviderUsage(
snapshot: AccountsSnapshot,
provider: ProviderKey,
accountId: string
): InactiveAccountUsage | null {
const list =
provider === 'claude'
? snapshot.rateLimits.inactiveClaudeAccounts
: snapshot.rateLimits.inactiveCodexAccounts
return list.find((u) => u.accountId === accountId) ?? null
}
// Why: rate limits are fetched for the active target even when no Orca-managed
// account exists (the default target is the agent's own system-default login).
// Treat a provider as having usage worth showing when a fetch succeeded or any
// window has data; an unavailable/error provider with no windows means the
// system-default login has no credentials for it, so there is nothing to show.
export function hasActiveProviderUsage(limits: ProviderRateLimits | null): boolean {
if (!limits) {
return false
}
if (
limits.session != null ||
limits.weekly != null ||
limits.monthly != null ||
(limits.buckets && limits.buckets.length > 0)
) {
return true
}
return limits.status === 'ok'
}
// Why: transient errors keep the last successful window data, so availability
// is per window rather than per provider status.
export function getUsageBarState(
limits: ProviderRateLimits | null,
windowKey: 'session' | 'weekly',
isFetchingOverride?: boolean
): UsageBarState {
const window = limits?.[windowKey] ?? null
const fetching =
isFetchingOverride ?? (limits?.status === 'fetching' || limits?.status === 'idle')
return {
usedPercent: window?.usedPercent ?? null,
unavailable: window == null && !fetching,
loading: fetching && window == null
}
}
/**
* Reset countdown for one window, e.g. "Resets in 3h 54m" / "Resets now",
* or null when the window has no reset timestamp (so the UI degrades to
* today's bars-only layout).
*
* Why: shares formatResetCountdown with the desktop status-bar tooltip so the
* copy stays identical across surfaces. `now` is a parameter so the function
* stays pure and unit-testable.
*/
export function getWindowResetLabel(
limits: ProviderRateLimits | null,
windowKey: 'session' | 'weekly',
now: number
): string | null {
const resetsAt = limits?.[windowKey]?.resetsAt
if (resetsAt == null) {
return null
}
return formatResetCountdown(resetsAt - now)
}
// Why: the usage UI must render for the system-default login, not only for
// Orca-managed accounts. Show a provider when it has at least one managed
// account OR active rate-limit data for the system-default target.
export function hasRenderableUsage(snapshot: AccountsSnapshot, provider: ProviderKey): boolean {
const accounts = provider === 'claude' ? snapshot.claude.accounts : snapshot.codex.accounts
if (accounts.length > 0) {
return true
}
return hasActiveProviderUsage(getActiveProviderRateLimits(snapshot, provider))
}