/** * /quota — subscription plan usage across providers. * Emits quota summary into footer llm zone; /quota toggles detailed widget. * Supports: Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, MiniMax, MiniMax CN, Zai, Kimi. * Providers without configured keys are silently skipped. */ import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { ExtensionAPI, ExtensionContext, ThemeColor, } from "@earendil-works/pi-coding-agent"; import { closeDebug, dbg, span } from "./src/debug.ts"; import { offerFooterSegment, removeFooterSegment, } from "./src/pi-ext-footer-segment.ts"; import { MAX_TIMER_MS, parseNumberSetting, resolveSetting, type SettingDeclaration, } from "./src/pi-ext-settings.ts"; const STATUS_ID = "quota"; const DEFAULT_REFRESH_MS = 5 * 60_000; const REFRESH_SETTING: SettingDeclaration = { key: "quota.refreshIntervalMs", env: "PI_QUOTA_REFRESH_INTERVAL_MS", default: DEFAULT_REFRESH_MS, parse(raw, source) { const value = parseNumberSetting(raw, source); if (value === undefined) return undefined; if (!Number.isInteger(value) || value < 10_000 || value > MAX_TIMER_MS) throw new Error(`expected an integer from 10000 to ${MAX_TIMER_MS}`); return value; }, }; // ── Types ───────────────────────────────────────────────────────────────── interface MinimaxResponse { base_resp: { status_code: number; status_msg: string }; model_remains: Array<{ model_name: string; current_interval_total_count: number; current_interval_usage_count: number; current_weekly_total_count?: number; current_weekly_usage_count?: number; current_interval_remaining_percent?: number; current_weekly_remaining_percent?: number; remains_time?: number; start_time?: number; end_time?: number; weekly_start_time?: number; weekly_end_time?: number; }>; } interface CopilotResponse { quota_reset_date_utc?: string; quota_snapshots?: { premium_interactions?: { percent_remaining?: number }; chat?: { unlimited?: boolean; percent_remaining?: number }; }; } interface GeminiResponse { buckets?: Array<{ modelId?: string; remainingFraction?: number }>; } interface ZaiResponse { code: number; data: { limits: Array<{ unit: number; percentage?: number; nextResetTime?: number; }>; level: string; }; success: boolean; } interface KimiUsage { limit: string; used: string; remaining: string; resetTime?: string; } interface KimiLimit { window?: { duration?: number; timeUnit?: string }; detail?: KimiUsage; } interface KimiResponse { usage?: KimiUsage; limits?: KimiLimit[]; totalQuota?: { limit: string; remaining: string }; } interface Quota { used: number; total: number; remaining: number; resetAt?: number; } interface ProviderUsage { name: string; fiveH: Quota; weekly: Quota; } // ── API clients ──────────────────────────────────────────────────────────── async function fetchJson(url: string, init: RequestInit) { const r = await fetch(url, { ...init, signal: AbortSignal.timeout(15_000) }); if (!r.ok) throw new Error(`${r.status}`); return r.json(); } async function fetchAnthropic(key: string) { return fetchJson("https://api.anthropic.com/api/oauth/usage", { headers: { Authorization: `Bearer ${key}`, "anthropic-beta": "oauth-2025-04-20", Accept: "application/json", }, }); } async function fetchCodex(key: string, accountId: string) { return fetchJson("https://chatgpt.com/backend-api/wham/usage", { headers: { Authorization: `Bearer ${key}`, "ChatGPT-Account-Id": accountId, Accept: "application/json", Origin: "https://chatgpt.com", Referer: "https://chatgpt.com/", "User-Agent": "Mozilla/5.0", }, }); } async function fetchMinimax(key: string, cn = false) { const endpoint = cn ? "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains" : "https://api.minimax.io/v1/api/openplatform/coding_plan/remains"; const r = await fetch(endpoint, { headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", }, signal: AbortSignal.timeout(10_000), }); if (!r.ok) throw new Error(`${r.status}`); const data = (await r.json()) as MinimaxResponse; if (data.base_resp?.status_code !== 0) throw new Error(data.base_resp?.status_msg ?? "API error"); return data; } async function fetchCopilot(key: string) { return fetchJson("https://api.github.com/copilot_internal/user", { headers: { "Editor-Version": "vscode/1.96.2", "User-Agent": "GitHubCopilotChat/0.26.7", "X-Github-Api-Version": "2025-04-01", Accept: "application/json", Authorization: `token ${key}`, }, }) as Promise; } async function fetchGemini(key: string) { return fetchJson( "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", }, body: "{}", }, ) as Promise; } async function fetchZai(key: string) { const r = await fetch("https://api.z.ai/api/monitor/usage/quota/limit", { headers: { Authorization: key, "Content-Type": "application/json" }, signal: AbortSignal.timeout(10_000), }); if (!r.ok) throw new Error(`${r.status}`); const d = (await r.json()) as ZaiResponse; if (!d.success) throw new Error(`${d.code}`); return d; } async function fetchKimi(key: string) { const r = await fetch("https://api.kimi.com/coding/v1/usages", { headers: { Authorization: `Bearer ${key}`, Accept: "application/json" }, signal: AbortSignal.timeout(10_000), }); if (!r.ok) throw new Error(`${r.status}`); return r.json() as Promise; } // ── Parsing ──────────────────────────────────────────────────────────────── function parseDateish(value: unknown): number { if (typeof value === "number") { const ms = value > 10 ** 11 ? value : value * 1000; return ms; } if (typeof value === "string") return new Date(value).getTime(); return 0; } function parseAnthropic(data: any): ProviderUsage { const fiveHour = data?.five_hour; const sevenDay = data?.seven_day; const used5 = Number(fiveHour?.utilization ?? 0); const reset5 = fiveHour?.resets_at ? parseDateish(fiveHour.resets_at) : undefined; const used7 = Number(sevenDay?.utilization ?? 0); const reset7 = sevenDay?.resets_at ? parseDateish(sevenDay.resets_at) : undefined; return { name: "Anthropic", fiveH: { used: used5, total: 100, remaining: 100 - used5, resetAt: reset5 }, weekly: { used: used7, total: 100, remaining: 100 - used7, resetAt: reset7, }, }; } function percentLeftToUsed(limit: any): number { if (limit?.percent_left != null) return Math.max(0, 100 - Number(limit.percent_left)); if (limit?.remaining_percent != null) return Math.max(0, 100 - Number(limit.remaining_percent)); if (limit?.used_percent != null) return Number(limit.used_percent); return 0; } function parseCodex(data: any): ProviderUsage { const rl = data?.rate_limit ?? data?.rate_limits ?? {}; const primary = rl.primary_window ?? rl.primary ?? rl.five_hour_limit ?? rl.five_hour; const secondary = rl.secondary_window ?? rl.secondary ?? rl.weekly_limit ?? rl.weekly; const used5 = primary ? percentLeftToUsed(primary) : 0; const reset5 = primary ? parseDateish(primary.reset_at ?? primary.reset_time_ms) : undefined; const used7 = secondary ? percentLeftToUsed(secondary) : 0; const reset7 = secondary ? parseDateish(secondary.reset_at ?? secondary.reset_time_ms) : undefined; return { name: "OpenAI Codex", fiveH: { used: used5, total: 100, remaining: 100 - used5, resetAt: reset5 }, weekly: { used: used7, total: 100, remaining: 100 - used7, resetAt: reset7, }, }; } function parseMinimax(data: MinimaxResponse, name = "MiniMax"): ProviderUsage { const models = data.model_remains.filter( (m) => /^minimax-m/i.test(m.model_name) || /minimax/i.test(m.model_name), ); const bucket = models[0] ?? data.model_remains[0]; if (!bucket) return { name, fiveH: { used: 0, total: 0, remaining: 0 }, weekly: { used: 0, total: 0, remaining: 0 }, }; function quota( totalValue: unknown, remainingValue: unknown, percentValue: unknown, ) { const total = Number(totalValue) || 0; const remaining = Number(remainingValue) || 0; if (total > 0) return { used: total - remaining, total, remaining }; const percent = Number(percentValue); if (Number.isFinite(percent) && percent > 0) return { used: 100 - percent, total: 100, remaining: percent }; return { used: 0, total: 0, remaining: 0 }; } // API field is misleadingly named — usage_count is remaining count. return { name, fiveH: { ...quota( bucket.current_interval_total_count, bucket.current_interval_usage_count, bucket.current_interval_remaining_percent, ), resetAt: parseDateish(bucket.end_time) || (bucket.remains_time ? Date.now() + bucket.remains_time : undefined), }, weekly: { ...quota( bucket.current_weekly_total_count, bucket.current_weekly_usage_count, bucket.current_weekly_remaining_percent, ), resetAt: parseDateish(bucket.weekly_end_time) || undefined, }, }; } function parseCopilot(data: CopilotResponse): ProviderUsage { const resetAt = parseDateish(data.quota_reset_date_utc) || undefined; const premiumLeft = Number( data.quota_snapshots?.premium_interactions?.percent_remaining ?? 0, ); const chat = data.quota_snapshots?.chat; const chatLeft = chat?.unlimited ? 100 : Number(chat?.percent_remaining ?? 0); return { name: "GitHub Copilot", fiveH: { used: Math.max(0, 100 - premiumLeft), total: 100, remaining: premiumLeft, resetAt, }, weekly: { used: Math.max(0, 100 - chatLeft), total: chat?.unlimited ? 0 : 100, remaining: chatLeft, resetAt, }, }; } function parseGemini(data: GeminiResponse): ProviderUsage { let proLeft = 1; let flashLeft = 1; let hasPro = false; let hasFlash = false; for (const bucket of data.buckets ?? []) { const model = bucket.modelId?.toLowerCase() ?? ""; const left = Number(bucket.remainingFraction ?? 1); if (model.includes("pro")) { hasPro = true; proLeft = Math.min(proLeft, left); } if (model.includes("flash")) { hasFlash = true; flashLeft = Math.min(flashLeft, left); } } return { name: "Gemini", fiveH: hasPro ? { used: (1 - proLeft) * 100, total: 100, remaining: proLeft * 100 } : { used: 0, total: 0, remaining: 0 }, weekly: hasFlash ? { used: (1 - flashLeft) * 100, total: 100, remaining: flashLeft * 100 } : { used: 0, total: 0, remaining: 0 }, }; } function parseZai(data: ZaiResponse): ProviderUsage { const fiveH = data.data.limits.find((l) => l.unit === 3); const weekly = data.data.limits.find((l) => l.unit === 6); let used5H = fiveH?.percentage ?? 0; const usedW = weekly?.percentage ?? 0; const reset5H = weekly && usedW >= 100 ? weekly.nextResetTime : fiveH?.nextResetTime; if (weekly && usedW >= 100) used5H = 100; return { name: `Zai (${data.data.level})`, fiveH: { used: used5H, total: 100, remaining: 100 - used5H, resetAt: reset5H, }, weekly: { used: usedW, total: 100, remaining: 100 - usedW, resetAt: weekly?.nextResetTime, }, }; } function parseKimi(data: KimiResponse): ProviderUsage { const usage = data?.usage; const uLimit = Number(usage?.limit ?? 0); const uUsed = Number(usage?.used ?? 0); const uReset = usage?.resetTime ? new Date(usage.resetTime).getTime() : undefined; // 5h window is the first rate limit (duration=300, TIME_UNIT_MINUTE) const limits = data?.limits ?? []; const firstLimit = limits[0]?.detail; const wLimit = Number(firstLimit?.limit ?? 0); const wUsed = Number(firstLimit?.used ?? 0); const wReset = firstLimit?.resetTime ? new Date(firstLimit.resetTime).getTime() : undefined; return { name: "Kimi", fiveH: { used: wUsed, total: wLimit, remaining: wLimit - wUsed, resetAt: wReset, }, weekly: { used: uUsed, total: uLimit, remaining: uLimit - uUsed, resetAt: uReset, }, }; } // ── Formatters ──────────────────────────────────────────────────────────── function bar(pct: number, w = 15): string { const n = Math.round((pct / 100) * w); return "█".repeat(n) + "░".repeat(w - n); } function fmtReset(ts?: number): string { if (!ts) return ""; const diff = ts - Date.now(); if (diff <= 0) return ""; const h = Math.floor(diff / 3_600_000); const m = Math.floor((diff % 3_600_000) / 60_000); return h > 0 ? `${h}h${m}m` : `${m}m`; } function color(used: number): ThemeColor { return used < 50 ? "accent" : used < 80 ? "warning" : "error"; } // ── Labels ────────────────────────────────────────────────────────── function visibleRows( providers: readonly ProviderUsage[], active: string | null, ): ProviderUsage[] { return providers .filter((p) => p.fiveH.total > 0) .filter((p) => !active || p.name.startsWith(active)); } function fiveHPercent(p: ProviderUsage): number { return Math.round((p.fiveH.used / p.fiveH.total) * 100); } function shortName(p: ProviderUsage): string { return p.name.split(" ")[0]; } /** * TUI segment text. The active provider drops its name, because the model * segment already shows that provider's icon; inactive rows keep it. */ function segmentText(p: ProviderUsage, isActive: boolean): string[] { const used = fiveHPercent(p); const reset = fmtReset(p.fiveH.resetAt); const prefix = isActive ? "" : `${shortName(p)} `; return [ `${prefix}${used}%${reset ? ` (${reset})` : ""}`, `${prefix}${used}%`, `${used}%`, ]; } /** * TUI fallback when the footer rejects segments. No bullet icon is rendered * there, so the provider name has to carry the identity. */ function quotaStatusLabel( providers: readonly ProviderUsage[], active: string | null, ): string | undefined { const rows = visibleRows(providers, active).map((p) => { const reset = fmtReset(p.fiveH.resetAt); return `${shortName(p)} ${fiveHPercent(p)}%${reset ? ` (${reset})` : ""}`; }); return rows.length > 0 ? rows.join(" · ") : undefined; } /** Non-TUI label: spelled out, since other clients show plain status text. */ function quotaPlainLabel( providers: readonly ProviderUsage[], active: string | null, ): string | undefined { const rows = visibleRows(providers, active).map((p) => { const reset = fmtReset(p.fiveH.resetAt); const suffix = reset ? `, resets in ${reset}` : ""; return `${shortName(p)} quota ${fiveHPercent(p)}% used${suffix}`; }); return rows.length > 0 ? rows.join("; ") : undefined; } // ── Extension ───────────────────────────────────────────────────────────── export default function usageExtension(pi: ExtensionAPI) { let providers: ProviderUsage[] = []; let fetchedAt = 0; let fetchPromise: Promise | null = null; let widgetVisible = false; let refreshTimer: ReturnType | null = null; let refreshMs = DEFAULT_REFRESH_MS; let generation = 0; const cache = new Map(); let providerFetchOutcome: "success" | "cached" | "unavailable" = "success"; const PROVIDERS: Record = { anthropic: "Anthropic", klaus: "Anthropic", "openai-codex": "OpenAI Codex", "github-copilot": "GitHub Copilot", "google-gemini-cli": "Gemini", minimax: "MiniMax", "minimax-cn": "MiniMax CN", zai: "Zai", "kimi-coding": "Kimi", }; async function keepCached( name: string, run: () => Promise, ): Promise { try { const usage = await run(); providerFetchOutcome = usage.fiveH.total > 0 || usage.weekly.total > 0 ? "success" : "unavailable"; if (providerFetchOutcome === "success") cache.set(name, usage); return usage; } catch { const cached = cache.get(name); providerFetchOutcome = cached ? "cached" : "unavailable"; dbg?.("quota.provider", { outcome: providerFetchOutcome }); return cached; } } function activeProviderKey(ctx: ExtensionContext): string | undefined { return ctx.model?.provider?.toLowerCase(); } async function codexAccountId(): Promise { try { const pi = (await import("@earendil-works/pi-coding-agent")) as any; const cred = pi.readStoredCredential?.("openai-codex") as any; if (typeof cred?.accountId === "string") return cred.accountId; } catch {} try { const raw = JSON.parse( readFileSync(join(homedir(), ".codex", "auth.json"), "utf8"), ) as any; return raw?.tokens?.account_id ?? raw?.tokens?.accountId; } catch {} return undefined; } async function fetchActive( ctx: ExtensionContext, key: string, ): Promise { if (key === "anthropic") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); return apiKey ? keepCached("Anthropic", async () => parseAnthropic(await fetchAnthropic(apiKey)), ) : undefined; } if (key === "klaus") { // Klaus streams the Claude subscription through Pi's Anthropic OAuth // token, so its quota is the Anthropic subscription quota. const apiKey = await ctx.modelRegistry.getApiKeyForProvider("anthropic"); return apiKey ? keepCached("Anthropic", async () => parseAnthropic(await fetchAnthropic(apiKey)), ) : undefined; } if (key === "github-copilot") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); return apiKey ? keepCached("GitHub Copilot", async () => parseCopilot(await fetchCopilot(apiKey)), ) : undefined; } if (key === "google-gemini-cli") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); return apiKey ? keepCached("Gemini", async () => parseGemini(await fetchGemini(apiKey)), ) : undefined; } if (key === "openai-codex") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); const accountId = await codexAccountId(); return apiKey && accountId ? keepCached("OpenAI Codex", async () => parseCodex(await fetchCodex(apiKey, accountId)), ) : undefined; } if (key === "minimax") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); return apiKey ? keepCached("MiniMax", async () => parseMinimax(await fetchMinimax(apiKey)), ) : undefined; } if (key === "minimax-cn") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); return apiKey ? keepCached("MiniMax CN", async () => parseMinimax(await fetchMinimax(apiKey, true), "MiniMax CN"), ) : undefined; } if (key === "zai") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); return apiKey ? keepCached("Zai", async () => parseZai(await fetchZai(apiKey))) : undefined; } if (key === "kimi-coding") { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(key); return apiKey ? keepCached("Kimi", async () => parseKimi(await fetchKimi(apiKey))) : undefined; } return undefined; } async function fetchAll(ctx: ExtensionContext) { const key = activeProviderKey(ctx); const finish = span?.("quota.fetch"); let outcome: | "success" | "cached" | "unavailable" | "no_provider" | "stale" | "error" = key ? "success" : "no_provider"; providerFetchOutcome = "success"; try { const usage = key ? await fetchActive(ctx, key) : undefined; if (key !== activeProviderKey(ctx)) { finish?.("finish", { outcome: "stale" }); return; } if (key) outcome = usage ? providerFetchOutcome : "unavailable"; providers = usage ? [usage] : []; fetchedAt = Date.now(); finish?.("finish", { outcome }); } catch (error) { finish?.("error", { outcome: "error" }); throw error; } } async function getOrFetch(ctx: ExtensionContext, force = false) { if (!force && fetchedAt && Date.now() - fetchedAt < refreshMs) return; if (fetchPromise) return fetchPromise; fetchPromise = fetchAll(ctx).finally(() => { fetchPromise = null; }); await fetchPromise; } function activeProvider(ctx: ExtensionContext): string | null { const p = activeProviderKey(ctx); return p ? (PROVIDERS[p] ?? p) : null; } // ── Footer llm segment ────────────────────────────────────────────────── const footerIds = new Set(); function clearFooter() { for (const id of footerIds) removeFooterSegment(pi, id); footerIds.clear(); } function updateFooter(ctx: ExtensionContext) { if (!ctx.hasUI) return; const active = activeProvider(ctx); if (ctx.mode !== "tui") { clearFooter(); ctx.ui.setStatus(STATUS_ID, quotaPlainLabel(providers, active)); return; } let offered = false; let accepted = true; const seen = new Set(); for (const p of visibleRows(providers, active)) { const used = fiveHPercent(p); const isActive = !!active && p.name.startsWith(active); const [text, ...variants] = segmentText(p, isActive); const id = `quota:${p.name}`; seen.add(id); footerIds.add(id); offered = true; accepted = offerFooterSegment(pi, { id, icon: isActive ? "●" : "◌", text, variants, color: isActive ? color(used) : "muted", zone: "llm", order: 4, }) && accepted; } for (const id of [...footerIds]) { if (seen.has(id)) continue; offered = true; accepted = removeFooterSegment(pi, id) && accepted; footerIds.delete(id); } accepted = offered && accepted; if (!accepted) clearFooter(); ctx.ui.setStatus( STATUS_ID, accepted ? undefined : quotaStatusLabel(providers, active), ); } // ── Widget ───────────────────────────────────────────────────────────── function widgetLines(ctx: ExtensionContext): string[] { const th = ctx.ui.theme; const active = activeProvider(ctx); const lines: string[] = []; for (const p of providers.filter( (p) => !active || p.name.startsWith(active), )) { if (p.fiveH.total === 0) continue; const isActive = active && p.name.startsWith(active); const label = p.name.split(" ")[0].padEnd(7); const bullet = isActive ? th.fg("accent", `●`) : th.fg("muted", ` `); const name = th.bold( isActive ? th.fg("accent", label) : th.fg("muted", label), ); const used5 = Math.round((p.fiveH.used / p.fiveH.total) * 100) || 0; const r5 = p.fiveH.resetAt ? fmtReset(p.fiveH.resetAt).padStart(6) : " "; let line = `${bullet} ${name} 5h ${th.fg(color(used5) as any, bar(used5))} ${th.fg(color(used5) as any, `${used5}%`.padStart(4))} ${th.fg("dim" as any, r5)}`; if (p.weekly.total > 0) { const usedW = Math.round((p.weekly.used / p.weekly.total) * 100); const rW = p.weekly.resetAt ? fmtReset(p.weekly.resetAt).padStart(6) : " "; line += ` wk ${th.fg(color(usedW) as any, bar(usedW))} ${th.fg(color(usedW) as any, `${usedW}%`.padStart(4))} ${th.fg("dim" as any, rW)}`; } lines.push(line); } const age = Math.round((Date.now() - fetchedAt) / 1000); lines.push( th.fg( "dim", ` ↻ ${age < 60 ? `${age}s ago` : `${Math.round(age / 60)}m ago`}`, ), ); return lines; } function startRefresh(ctx: ExtensionContext, ticket: number) { if (refreshTimer) clearInterval(refreshTimer); refreshTimer = setInterval(async () => { try { await getOrFetch(ctx, true); if (ticket !== generation) return; updateFooter(ctx); if (widgetVisible) ctx.ui.setWidget("usage", widgetLines(ctx)); } catch {} }, refreshMs); } function stopRefresh() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } } // ── Command ───────────────────────────────────────────────────────────── pi.registerCommand("quota", { description: "Show/hide subscription quota usage", async handler(_args, ctx) { if (!ctx.hasUI) { ctx.ui.notify("/quota requires interactive mode", "error"); return; } widgetVisible = !widgetVisible; if (widgetVisible) { activate(ctx); ctx.ui.setWidget("usage", widgetLines(ctx)); try { await getOrFetch(ctx, true); updateFooter(ctx); } catch {} ctx.ui.setWidget("usage", widgetLines(ctx)); } else { ctx.ui.setWidget("usage", undefined); } }, }); // ── Events ───────────────────────────────────────────────────────────── const activate = (ctx: ExtensionContext, ticket = generation) => { if (!ctx.hasUI) return; ctx.ui.setStatus(STATUS_ID, undefined); if (!refreshTimer) startRefresh(ctx, ticket); void (async () => { try { await getOrFetch(ctx); if (ticket !== generation) return; updateFooter(ctx); } catch { if (ticket === generation && !providers.length) { clearFooter(); ctx.ui.setStatus(STATUS_ID, undefined); } } })(); }; pi.on("session_start", (_e, ctx) => { dbg?.("session.start"); generation++; fetchedAt = 0; widgetVisible = false; clearFooter(); // Without UI nobody can read quota, so skip the status line and the // recurring provider requests entirely. if (!ctx.hasUI) return; const setting = resolveSetting(pi, ctx, REFRESH_SETTING); refreshMs = setting.ok ? setting.value : DEFAULT_REFRESH_MS; if (!setting.ok) ctx.ui.notify( `${setting.error}; using ${DEFAULT_REFRESH_MS} ms`, "warning", ); ctx.ui.setStatus(STATUS_ID, undefined); }); pi.on("agent_start", async (_e, ctx) => { activate(ctx); updateFooter(ctx); }); pi.on("agent_end", async (_e, ctx) => { if (!ctx.hasUI) return; try { await getOrFetch(ctx); updateFooter(ctx); } catch {} }); pi.on("model_select", async (_e, ctx) => { if (!ctx.hasUI) return; activate(ctx); try { await getOrFetch(ctx, true); updateFooter(ctx); } catch {} }); pi.on("session_shutdown", async (_e, ctx) => { dbg?.("session.shutdown"); generation++; stopRefresh(); widgetVisible = false; clearFooter(); ctx.ui.setStatus(STATUS_ID, undefined); ctx.ui.setWidget("usage", undefined); closeDebug(); }); } export const __test = { parseCopilot, parseGemini, parseMinimax, quotaPlainLabel, quotaStatusLabel, segmentText, };