repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/quota/index.ts
Raw/**
* /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<number> = {
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<CopilotResponse>;
}
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<GeminiResponse>;
}
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<KimiResponse>;
}
// ── 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<void> | null = null;
let widgetVisible = false;
let refreshTimer: ReturnType<typeof setInterval> | null = null;
let refreshMs = DEFAULT_REFRESH_MS;
let generation = 0;
const cache = new Map<string, ProviderUsage>();
let providerFetchOutcome: "success" | "cached" | "unavailable" = "success";
const PROVIDERS: Record<string, string> = {
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<ProviderUsage>,
): Promise<ProviderUsage | undefined> {
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<string | undefined> {
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<ProviderUsage | undefined> {
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<string>();
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<string>();
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,
};