From 459e524062cb7a8bc545077627bf58a54f3b1825 Mon Sep 17 00:00:00 2001 From: Rishang Date: Fri, 17 Jul 2026 15:48:57 +0530 Subject: [PATCH] feat: show usage only on demand --- README.md | 23 +-- extensions/usage-bars/index.ts | 355 +++------------------------------ 2 files changed, 27 insertions(+), 351 deletions(-) diff --git a/README.md b/README.md index 44cb185..c6d903b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,8 @@ # pi-usage-bars -Usage indicator for **pi**. +Usage viewer for **pi**. -![Codex footer bar](https://raw.githubusercontent.com/javargasm/pi-usage-bars/main/assets/codex.png) - -It adds: -- a footer status bar for the active provider -- a `/usage` command with all connected providers +It adds a `/usage` command with all connected providers. Usage is shown only while this command is open; it does not persist in the footer status line. Supported providers: - OpenAI Codex (multiple subscriptions) @@ -35,19 +31,8 @@ pi install npm:@javargasm/pi-usage-bars ## Use -### 1) Footer usage bars -When your active model is supported, the footer shows color-coded bars for: -- Session usage (5-hour rolling window) -- Weekly usage (7-day rolling window) -- Monthly usage (when available) -- Reset countdowns - -Bar colors: 🟒 green (<70%) β†’ 🟑 yellow (70-89%) β†’ πŸ”΄ red (β‰₯90%) - -![Claude footer bar](https://raw.githubusercontent.com/javargasm/pi-usage-bars/main/assets/claude.png) - -### 2) `/usage` command -Opens an interactive list with all providers that have credentials. Supports search/filter and shows the active provider with a βœ“ badge. +### `/usage` command +Opens an interactive list with all providers that have credentials. Supports search/filter and shows the active provider with a βœ“ badge. Bars are color-coded: 🟒 green (<70%) β†’ 🟑 yellow (70–89%) β†’ πŸ”΄ red (β‰₯90%). ![/usage command](https://raw.githubusercontent.com/javargasm/pi-usage-bars/main/assets/usage-command.png) diff --git a/extensions/usage-bars/index.ts b/extensions/usage-bars/index.ts index f256b6c..58cb7a7 100644 --- a/extensions/usage-bars/index.ts +++ b/extensions/usage-bars/index.ts @@ -1,9 +1,7 @@ /** * Usage Extension - Minimal API usage indicator for pi * - * Shows Codex (OpenAI), Anthropic (Claude), Z.AI, and optionally - * Google Gemini CLI / Antigravity usage as color-coded percentages - * in the footer status bar. + * Shows usage for connected providers in the /usage command UI. */ import { DynamicBorder, type ExtensionAPI } from "@mariozechner/pi-coding-agent"; @@ -16,40 +14,18 @@ import { type Focusable, } from "@mariozechner/pi-tui"; import { - canShowForProvider, clampPercent, colorForPercent, detectProvider, fetchAllUsages, - fetchClaudeUsageWithFallback, - fetchCodexUsage, - fetchGoogleUsage, - fetchZaiUsage, - fetchKiroUsage, - fetchOpencodeGoUsage, - resolveOpencodeGoConfig, - ensureFreshAuthForProviders, - providerToOAuthProviderId, - readAuth, resolveUsageEndpoints, type ProviderKey, type UsageByProviderMulti, type UsageData, } from "./core"; -const POLL_INTERVAL_MS = 2 * 60 * 1000; const STATUS_KEY = "usage-bars"; -const PROVIDER_LABELS: Record = { - codex: "Codex", - claude: "Claude", - zai: "Z.AI", - gemini: "Gemini", - antigravity: "Antigravity", - "opencode-go": "OpenCode Go", - kiro: "Kiro", -}; - interface SubscriptionItem { name: string; provider: ProviderKey; @@ -367,320 +343,35 @@ class UsageSelectorComponent extends Container implements Focusable { } } -interface UsageState extends UsageByProvider { - lastPoll: number; - activeProvider: ProviderKey | null; - activeProviderName: string | null; -} - export default function (pi: ExtensionAPI) { const endpoints = resolveUsageEndpoints(); - const state: UsageState = { - codex: null, - claude: null, - zai: null, - gemini: null, - antigravity: null, - "opencode-go": null, - kiro: null, - lastPoll: 0, - activeProvider: null, - activeProviderName: null, - }; - - let pollTimer: ReturnType | null = null; - let pollInFlight: Promise | null = null; - let pollQueued = false; - let ctx: any = null; - - function renderPercent(theme: any, value: number): string { - const v = clampPercent(value); - return theme.fg(colorForPercent(v), `${v}%`); - } - - function renderBar(theme: any, value: number): string { - const v = clampPercent(value); - const width = 8; - const filled = Math.round((v / 100) * width); - const full = "β–ˆ".repeat(Math.max(0, Math.min(width, filled))); - const empty = "β–‘".repeat(Math.max(0, width - filled)); - return theme.fg(colorForPercent(v), full) + theme.fg("dim", empty); - } - - function pickDataForProvider(provider: ProviderKey | null): UsageData | null { - if (!provider) return null; - return state[provider]; - } - - function updateStatus() { - const active = state.activeProvider; - const data = pickDataForProvider(active); - - if (data && !data.error) { - pi.events.emit("usage:update", { - session: data.session, - weekly: data.weekly, - sessionResetsIn: data.sessionResetsIn, - weeklyResetsIn: data.weeklyResetsIn, - }); - } - - if (!ctx?.hasUI) return; - - if (!active) { - ctx.ui.setStatus(STATUS_KEY, undefined); - return; - } - - const auth = readAuth(); - if (!canShowForProvider(active, auth, endpoints, state.activeProviderName)) { - ctx.ui.setStatus(STATUS_KEY, undefined); - return; - } - - const theme = ctx.ui.theme; - const label = PROVIDER_LABELS[active]; - - if (!data) { - ctx.ui.setStatus(STATUS_KEY, theme.fg("dim", `${label} usage: loading…`)); - return; - } - - if (data.error) { - ctx.ui.setStatus(STATUS_KEY, theme.fg("warning", `${label} usage unavailable (${data.error})`)); - return; - } - - const session = clampPercent(data.session); - const weekly = clampPercent(data.weekly); - - const sessionReset = data.sessionResetsIn ? theme.fg("dim", ` ⟳ ${data.sessionResetsIn}`) : ""; - const weeklyReset = data.weeklyResetsIn ? theme.fg("dim", ` ⟳ ${data.weeklyResetsIn}`) : ""; - const staleSuffix = data.stale ? theme.fg("warning", " stale") : ""; - const warningSuffix = data.warning && !data.stale ? theme.fg("warning", " ⚠") : ""; - - const monthlyReset = data.monthlyResetsIn ? theme.fg("dim", ` ⟳ ${data.monthlyResetsIn}`) : ""; - - let status: string; - - // Kiro uses a single credits bar instead of session+weekly - if (active === "kiro") { - const creditsRaw = Math.max(0, Math.min(100, data.session)); - const creditsClamped = clampPercent(creditsRaw); - const creditsLabel = `${creditsRaw.toFixed(2)}%`; - const creditsReset = data.monthlyResetsIn ? theme.fg("dim", ` ⟳ ${data.monthlyResetsIn}`) : ""; - const planSuffix = data.planTitle ? `${data.planTitle} ` : ""; - status = - theme.fg("dim", `${label} `) + - theme.fg("accent", planSuffix) + - theme.fg("muted", "C ") + - renderBar(theme, creditsClamped) + - " " + - theme.fg(colorForPercent(creditsClamped), creditsLabel) + - creditsReset; - } else { - status = - theme.fg("dim", `${label} `) + - theme.fg("muted", "S ") + - renderBar(theme, session) + - " " + - renderPercent(theme, session) + - sessionReset + - theme.fg("muted", " W ") + - renderBar(theme, weekly) + - " " + - renderPercent(theme, weekly) + - weeklyReset; - } - - if (typeof data.monthly === "number" && active !== "kiro") { - const monthly = clampPercent(data.monthly); - status += - theme.fg("muted", " M ") + - renderBar(theme, monthly) + - " " + - renderPercent(theme, monthly) + - monthlyReset; - } - - status += staleSuffix + warningSuffix; - - ctx.ui.setStatus(STATUS_KEY, status); - } - - function updateProviderFrom(modelLike: any): boolean { - const previous = state.activeProvider; - const previousName = state.activeProviderName; - state.activeProvider = detectProvider(modelLike); - state.activeProviderName = modelLike && typeof modelLike === "object" ? modelLike.provider ?? null : null; - - if (previous !== state.activeProvider || previousName !== state.activeProviderName) { - updateStatus(); - return true; - } - - return false; - } - - async function runPoll() { - let auth = readAuth(); - const active = state.activeProvider; - const activeName = state.activeProviderName; - - const setActiveError = (message: string) => { - if (!active) return; - state[active] = { session: 0, weekly: 0, error: message }; - }; - - if (!canShowForProvider(active, auth, endpoints, activeName)) { - state.lastPoll = Date.now(); - updateStatus(); - return; - } - - const oauthProviderId = providerToOAuthProviderId(active, activeName); - if (oauthProviderId && auth) { - const refreshed = await ensureFreshAuthForProviders([oauthProviderId], { auth }); - auth = refreshed.auth; - - const refreshError = refreshed.refreshErrors[oauthProviderId]; - if (refreshError) { - setActiveError(`auth refresh failed (${refreshError})`); - state.lastPoll = Date.now(); - updateStatus(); - return; - } - } - - if (!auth && active !== "opencode-go") { - state.lastPoll = Date.now(); - updateStatus(); - return; - } - - if (active === "codex") { - const key = activeName ?? "openai-codex"; - const access = (auth as any)[key]?.access; - const accountId = (auth as any)[key]?.accountId; - state.codex = access - ? await fetchCodexUsage(access, accountId) - : { session: 0, weekly: 0, error: "missing access token (try /login again)" }; - } else if (active === "claude") { - state.claude = auth?.anthropic?.access || auth?.anthropic?.refresh - ? (await fetchClaudeUsageWithFallback({ auth })).usage - : { session: 0, weekly: 0, error: "missing access token (try /login again)" }; - } else if (active === "zai") { - const token = auth?.zai?.access || auth?.zai?.key; - state.zai = token - ? await fetchZaiUsage(token, { endpoints }) - : { session: 0, weekly: 0, error: "missing token (try /login again)" }; - } else if (active === "gemini") { - const creds = auth?.["google-gemini-cli"]; - state.gemini = creds?.access - ? await fetchGoogleUsage(creds.access, endpoints.gemini, creds.projectId, "gemini", { endpoints }) - : { session: 0, weekly: 0, error: "missing access token (try /login again)" }; - } else if (active === "antigravity") { - const creds = auth?.["google-antigravity"]; - state.antigravity = creds?.access - ? await fetchGoogleUsage(creds.access, endpoints.antigravity, creds.projectId, "antigravity", { endpoints }) - : { session: 0, weekly: 0, error: "missing access token (try /login again)" }; - } else if (active === "opencode-go") { - state["opencode-go"] = await fetchOpencodeGoUsage({ endpoints }); - } else if (active === "kiro") { - state.kiro = auth?.kiro?.access || auth?.kiro?.refresh - ? await fetchKiroUsage({ endpoints }) - : { session: 0, weekly: 0, error: "missing credentials (try /login)" }; - } - - state.lastPoll = Date.now(); - updateStatus(); - } - async function poll() { - if (pollInFlight) { - pollQueued = true; - await pollInFlight; - return; - } - - do { - pollQueued = false; - pollInFlight = runPoll() - .catch(() => { - // Never crash extension event handlers on transient polling errors. - }) - .finally(() => { - pollInFlight = null; - }); - - await pollInFlight; - } while (pollQueued); - } - - pi.on("session_start", async (_event, _ctx) => { - ctx = _ctx; - updateProviderFrom(_ctx.model); - - await poll(); - - if (pollTimer) clearInterval(pollTimer); - pollTimer = setInterval(() => { - void poll(); - }, POLL_INTERVAL_MS); - }); - - pi.on("session_shutdown", async (_event, _ctx) => { - if (pollTimer) { - clearInterval(pollTimer); - pollTimer = null; - } - - if (_ctx?.hasUI) { - _ctx.ui.setStatus(STATUS_KEY, undefined); - } - }); - - pi.on("turn_start", async (_event, _ctx) => { - ctx = _ctx; - const changed = updateProviderFrom(_ctx.model); - if (changed) await poll(); - }); - - pi.on("turn_end", async (_event, _ctx) => { - ctx = _ctx; - updateProviderFrom(_ctx.model); - await poll(); - }); - - pi.on("model_select", async (event, _ctx) => { - ctx = _ctx; - const changed = updateProviderFrom(event.model ?? _ctx.model); - if (changed) await poll(); + // Clear a footer value left by an earlier version of this extension. Usage is + // intentionally shown only in the /usage command UI. + pi.on("session_start", async (_event, ctx) => { + if (ctx?.hasUI) ctx.ui.setStatus(STATUS_KEY, undefined); }); pi.registerCommand("usage", { description: "Show API usage for all subscriptions", - handler: async (_args, _ctx) => { - ctx = _ctx; - updateProviderFrom(_ctx.model); - - try { - if (_ctx?.hasUI) { - await _ctx.ui.custom((tui, theme, _keybindings, done) => { - const selector = new UsageSelectorComponent( - tui, - theme, - state.activeProvider, - state.activeProviderName, - () => fetchAllUsages({ endpoints }), - () => done(), - ); - return selector; - }); - } - } finally { - await poll(); - } + handler: async (_args, ctx) => { + const model = ctx.model; + const activeProvider = detectProvider(model); + const activeProviderName = model && typeof model === "object" ? model.provider ?? null : null; + + if (!ctx?.hasUI) return; + + await ctx.ui.custom((tui, theme, _keybindings, done) => { + const selector = new UsageSelectorComponent( + tui, + theme, + activeProvider, + activeProviderName, + () => fetchAllUsages({ endpoints }), + done, + ); + return selector; + }); }, }); }