import { formatQuotaLabel } from "./utils"; /** * Per-provider column schema for the grouped Provider Quota layout. * * Each entry lists the canonical quota keys we want to surface as * fixed-width table columns. Matching is done both exact (by `quota.name`) * and via the normalized label (so MiniMax's `"session (5h)"` still lands in * the `"session"` column). * * Providers not listed here fall back to a dynamic schema: take the first * `MAX_DYNAMIC_COLUMNS` quotas in the order returned by `parseQuotaData()` * and surface them as columns; everything else becomes "+N more". */ const PROVIDER_COLUMNS: Record = { codex: ["session", "weekly"], claude: ["session", "weekly"], glm: ["session", "weekly", "mcp_monthly"], "glm-cn": ["session", "weekly", "mcp_monthly"], glmt: ["session", "weekly", "mcp_monthly"], zai: ["session", "weekly", "mcp_monthly"], github: ["chat", "completions", "premium_interactions"], minimax: ["session"], "minimax-cn": ["session"], "kimi-coding": ["session", "weekly"], }; /** Hard cap for the dynamic schema (Antigravity, Gemini-CLI, fallback). */ export const MAX_DYNAMIC_COLUMNS = 3; export interface ResolvedColumn { /** Stable column key — used for React keys and column-picker state. */ key: string; /** Human-readable header label. */ label: string; /** The matching quota for this account, or `null` if the account * doesn't expose that window. Rendered as an em-dash cell in the UI. */ quota: any | null; } export interface ResolvedSchema { columns: ResolvedColumn[]; /** Quotas present on the account but not surfaced as columns. * Rendered as "+N more" in the row's trailing cell. */ overflowCount: number; } function matchQuotaByKey(quotas: any[], key: string): any | null { // Exact match on quota.name first const exact = quotas.find( (q) => q && typeof q.name === "string" && q.name.toLowerCase() === key.toLowerCase() ); if (exact) return exact; // Then exact match on modelKey (for antigravity etc.) const byModel = quotas.find( (q) => q && typeof q.modelKey === "string" && q.modelKey.toLowerCase() === key.toLowerCase() ); if (byModel) return byModel; // Finally, normalized label match — handles "session (5h)" → "session" const normalized = formatQuotaLabel(key).toLowerCase(); return ( quotas.find( (q) => q && typeof q.name === "string" && formatQuotaLabel(q.name).toLowerCase() === normalized ) || null ); } /** * Resolve which columns to render for a given (provider, quotas) pair. * * - Named providers: use the static schema; missing windows render as `null`. * - Unknown providers: take the first N non-credit quotas in array order. * - Credits (`isCredits === true`) are never used as columns — they render * in the overflow tooltip / expanded panel as a balance, not as a %. */ export function getProviderColumns(provider: string, quotas: any[] = []): ResolvedSchema { const safe = Array.isArray(quotas) ? quotas : []; const nonCredits = safe.filter((q) => q && !q.isCredits); const credits = safe.filter((q) => q && q.isCredits); const named = PROVIDER_COLUMNS[String(provider || "").toLowerCase()]; if (named && named.length > 0) { const columns: ResolvedColumn[] = named.map((key) => ({ key, label: formatQuotaLabel(key), quota: matchQuotaByKey(nonCredits, key), })); const matchedQuotas = new Set(columns.map((c) => c.quota).filter(Boolean)); const overflowCount = nonCredits.filter((q) => !matchedQuotas.has(q)).length + credits.length; return { columns, overflowCount }; } // Dynamic schema — take the first N non-credit quotas in array order const visible = nonCredits.slice(0, MAX_DYNAMIC_COLUMNS); const columns: ResolvedColumn[] = visible.map((q) => ({ key: q.modelKey || q.name, label: q.displayName || formatQuotaLabel(q.name), quota: q, })); const overflowCount = Math.max(0, nonCredits.length - visible.length) + credits.length; return { columns, overflowCount }; } /** * Group a flat list of connection objects by their `provider` key. * Preserves the input order (the upstream sort by status + soonest reset is * meaningful inside each group; the provider order itself is controlled by * the caller via `PROVIDER_ORDER` in index.tsx). */ export function groupConnectionsByProvider( connections: T[] ): Map { const groups = new Map(); for (const conn of connections) { const key = conn.provider || "unknown"; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(conn); } return groups; }