Luigit
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,
};