fuck sub bar

This commit is contained in:
2026-03-12 14:18:06 +00:00
parent 7a9e2b94ff
commit 32752b42e0
69 changed files with 2 additions and 14777 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"lastChangelogVersion": "0.57.1",
"defaultProvider": "opencode-go",
"defaultModel": "kimi-k2.5",
"defaultProvider": "openrouter",
"defaultModel": "openai/gpt-5.3-codex",
"defaultThinkingLevel": "medium",
"theme": "matugen",
"lsp": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,16 +0,0 @@
{
"name": "sub-bar",
"version": "1.3.0",
"description": "Usage widget extension for pi-coding-agent",
"type": "module",
"license": "MIT",
"pi": {
"extensions": [
"./index.ts",
"./sub-core/index.ts"
]
},
"peerDependencies": {
"@mariozechner/pi-coding-agent": "*"
}
}
@@ -1,6 +0,0 @@
/**
* Re-export shared types and metadata from src/shared.
* Files in src/settings/, src/providers/, src/usage/ import "../../shared.js"
* which resolves here.
*/
export * from "./src/shared.js";
@@ -1,25 +0,0 @@
/**
* Core settings fallbacks for sub-bar when sub-core settings are unavailable.
*/
import type { CoreSettings } from "./shared.js";
import type { Settings } from "./settings-types.js";
import { PROVIDERS, PROVIDER_METADATA } from "./providers/metadata.js";
export function getFallbackCoreSettings(settings: Settings): CoreSettings {
const providers = {} as CoreSettings["providers"];
for (const provider of PROVIDERS) {
providers[provider] = {
enabled: "auto",
fetchStatus: Boolean(PROVIDER_METADATA[provider]?.status),
};
}
return {
providers,
behavior: settings.behavior,
statusRefresh: settings.statusRefresh ?? settings.behavior,
providerOrder: settings.providerOrder,
defaultProvider: settings.defaultProvider ?? null,
};
}
@@ -1,48 +0,0 @@
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
import { visibleWidth } from "@mariozechner/pi-tui";
import type { DividerCharacter } from "./settings-types.js";
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
const DIVIDER_JOIN_MAP: Partial<Record<DividerCharacter, { top: string; bottom: string; line: string }>> = {
"|": { top: "┬", bottom: "┴", line: "─" },
"│": { top: "┬", bottom: "┴", line: "─" },
"┆": { top: "┬", bottom: "┴", line: "─" },
"┃": { top: "┳", bottom: "┻", line: "━" },
"┇": { top: "┳", bottom: "┻", line: "━" },
"║": { top: "╦", bottom: "╩", line: "═" },
};
export function buildDividerLine(
width: number,
baseLine: string,
dividerChar: DividerCharacter,
joinEnabled: boolean,
position: "top" | "bottom",
dividerColor: ThemeColor,
theme: Theme
): string {
let lineChar = "─";
let joinChar: string | undefined;
if (joinEnabled) {
const joinInfo = DIVIDER_JOIN_MAP[dividerChar];
if (joinInfo) {
lineChar = joinInfo.line;
joinChar = position === "top" ? joinInfo.top : joinInfo.bottom;
}
}
const lineChars = Array.from(lineChar.repeat(Math.max(1, width)));
if (joinChar) {
const stripped = baseLine.replace(ANSI_REGEX, "");
let pos = 0;
for (const { segment } of SEGMENTER.segment(stripped)) {
if (pos >= lineChars.length) break;
if (segment === dividerChar) {
lineChars[pos] = joinChar;
}
pos += Math.max(1, visibleWidth(segment));
}
}
return theme.fg(dividerColor, lineChars.join(""));
}
@@ -1,71 +0,0 @@
/**
* Error utilities for the sub-bar extension
*/
import type { UsageError, UsageErrorCode } from "./types.js";
export function createError(code: UsageErrorCode, message: string, httpStatus?: number): UsageError {
return { code, message, httpStatus };
}
export function noCredentials(): UsageError {
return createError("NO_CREDENTIALS", "No credentials found");
}
export function noCli(cliName: string): UsageError {
return createError("NO_CLI", `${cliName} CLI not found`);
}
export function notLoggedIn(): UsageError {
return createError("NOT_LOGGED_IN", "Not logged in");
}
export function fetchFailed(reason?: string): UsageError {
return createError("FETCH_FAILED", reason ?? "Fetch failed");
}
export function httpError(status: number): UsageError {
return createError("HTTP_ERROR", `HTTP ${status}`, status);
}
export function apiError(message: string): UsageError {
return createError("API_ERROR", message);
}
export function timeout(): UsageError {
return createError("TIMEOUT", "Request timed out");
}
/**
* Check if an error should be considered "no data available" vs actual error
* These are expected states when provider isn't configured
*/
export function isExpectedMissingData(error: UsageError): boolean {
const ignoreCodes = new Set<UsageErrorCode>(["NO_CREDENTIALS", "NO_CLI", "NOT_LOGGED_IN"]);
return ignoreCodes.has(error.code);
}
/**
* Format error for display in the usage widget
*/
export function formatErrorForDisplay(error: UsageError): string {
switch (error.code) {
case "NO_CREDENTIALS":
return "No creds";
case "NO_CLI":
return "No CLI";
case "NOT_LOGGED_IN":
return "Not logged in";
case "HTTP_ERROR":
if (error.httpStatus === 401) {
return "token no longer valid please /login again";
}
return `${error.httpStatus}`;
case "FETCH_FAILED":
case "API_ERROR":
case "TIMEOUT":
case "UNKNOWN":
default:
return "Fetch failed";
}
}
@@ -1,937 +0,0 @@
/**
* UI formatting utilities for the sub-bar extension
*/
import type { Theme } from "@mariozechner/pi-coding-agent";
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
import type { RateWindow, UsageSnapshot, ProviderStatus, ModelInfo } from "./types.js";
import type {
BaseTextColor,
BarStyle,
BarType,
BarCharacter,
BarWidth,
ColorScheme,
DividerBlanks,
ResetTimerContainment,
Settings,
} from "./settings-types.js";
import { isBackgroundColor, resolveBaseTextColor, resolveDividerColor } from "./settings-types.js";
import { formatErrorForDisplay, isExpectedMissingData } from "./errors.js";
import { getStatusIcon, getStatusLabel } from "./status.js";
import { shouldShowWindow } from "./providers/windows.js";
import { getUsageExtras } from "./providers/extras.js";
import { normalizeTokens } from "./utils.js";
export interface UsageWindowParts {
label: string;
bar: string;
pct: string;
reset: string;
}
/**
* Context window usage info from the pi framework
*/
export interface ContextInfo {
tokens: number;
contextWindow: number;
percent: number;
}
type ModelInput = ModelInfo | string | undefined;
function resolveModelInfo(model?: ModelInput): ModelInfo | undefined {
if (!model) return undefined;
return typeof model === "string" ? { id: model } : model;
}
function isCodexSparkModel(model?: ModelInput): boolean {
const tokens = normalizeTokens(typeof model === "string" ? model : model?.id ?? "");
return tokens.includes("codex") && tokens.includes("spark");
}
function isCodexSparkWindow(window: RateWindow): boolean {
const tokens = normalizeTokens(window.label ?? "");
return tokens.includes("codex") && tokens.includes("spark");
}
function getDisplayWindowLabel(window: RateWindow, model?: ModelInput): string {
if (!isCodexSparkWindow(window)) return window.label;
if (!isCodexSparkModel(model)) return window.label;
const parts = window.label.trim().split(/\s+/);
const suffix = parts.at(-1) ?? "";
if (/^\d+h$/i.test(suffix) || /^day$/i.test(suffix) || /^week$/i.test(suffix)) {
return suffix;
}
return window.label;
}
/**
* Get the characters to use for progress bars
*/
function getBarCharacters(barCharacter: BarCharacter): { filled: string; empty: string } {
let filled = "━";
let empty = "━";
switch (barCharacter) {
case "light":
filled = "─";
empty = "─";
break;
case "heavy":
filled = "━";
empty = "━";
break;
case "double":
filled = "═";
empty = "═";
break;
case "block":
filled = "█";
empty = "█";
break;
default: {
const raw = String(barCharacter);
const trimmed = raw.trim();
if (!trimmed) return { filled, empty };
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
const segments = Array.from(segmenter.segment(raw), (entry) => entry.segment);
const first = segments[0] ?? trimmed[0] ?? "━";
const second = segments[1];
filled = first;
empty = second ?? first;
break;
}
}
return { filled, empty };
}
/**
* Get color based on percentage and color scheme
*/
function getUsageColor(
percent: number,
isRemaining: boolean,
colorScheme: ColorScheme,
errorThreshold: number = 25,
warningThreshold: number = 50,
successThreshold: number = 75
): "error" | "warning" | "base" | "success" {
if (colorScheme === "monochrome") {
return "base";
}
// For remaining percentage (Codex style), invert the logic
const effectivePercent = isRemaining ? percent : 100 - percent;
if (colorScheme === "success-base-warning-error") {
// >75%: success, >50%: base, >25%: warning, <=25%: error
if (effectivePercent < errorThreshold) return "error";
if (effectivePercent < warningThreshold) return "warning";
if (effectivePercent < successThreshold) return "base";
return "success";
}
// base-warning-error (default)
// >50%: base, >25%: warning, <=25%: error
if (effectivePercent < errorThreshold) return "error";
if (effectivePercent < warningThreshold) return "warning";
return "base";
}
function clampPercent(value: number): number {
return Math.max(0, Math.min(100, value));
}
function getStatusColor(
indicator: NonNullable<UsageSnapshot["status"]>["indicator"],
colorScheme: ColorScheme
): "error" | "warning" | "success" | "base" {
if (colorScheme === "monochrome") {
return "base";
}
if (indicator === "minor" || indicator === "maintenance") {
return "warning";
}
if (indicator === "major" || indicator === "critical") {
return "error";
}
if (indicator === "none") {
return colorScheme === "success-base-warning-error" ? "success" : "base";
}
return "base";
}
function resolveStatusTintColor(
color: "error" | "warning" | "success" | "base",
baseTextColor: BaseTextColor
): BaseTextColor {
return color === "base" ? baseTextColor : color;
}
function fgFromBgAnsi(ansi: string): string {
return ansi.replace(/\x1b\[48;/g, "\x1b[38;").replace(/\x1b\[49m/g, "\x1b[39m");
}
function applyBaseTextColor(theme: Theme, color: BaseTextColor, text: string): string {
if (isBackgroundColor(color)) {
const fgAnsi = fgFromBgAnsi(theme.getBgAnsi(color as Parameters<Theme["getBgAnsi"]>[0]));
return `${fgAnsi}${text}\x1b[39m`;
}
return theme.fg(resolveDividerColor(color), text);
}
function resolveUsageColorTargets(settings?: Settings): {
title: boolean;
timer: boolean;
bar: boolean;
usageLabel: boolean;
status: boolean;
} {
const targets = settings?.display.usageColorTargets;
return {
title: targets?.title ?? true,
timer: targets?.timer ?? true,
bar: targets?.bar ?? true,
usageLabel: targets?.usageLabel ?? true,
status: targets?.status ?? true,
};
}
function formatElapsedSince(timestamp: number): string {
const diffMs = Date.now() - timestamp;
if (diffMs < 60000) {
const seconds = Math.max(1, Math.floor(diffMs / 1000));
return `${seconds}s`;
}
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m`;
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
if (hours < 24) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
const days = Math.floor(hours / 24);
const remHours = hours % 24;
return remHours > 0 ? `${days}d${remHours}h` : `${days}d`;
}
const RESET_CONTAINMENT_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
function wrapResetContainment(text: string, containment: ResetTimerContainment): { wrapped: string; attachWithSpace: boolean } {
switch (containment) {
case "none":
return { wrapped: text, attachWithSpace: true };
case "blank":
return { wrapped: text, attachWithSpace: true };
case "[]":
return { wrapped: `[${text}]`, attachWithSpace: true };
case "<>":
return { wrapped: `<${text}>`, attachWithSpace: true };
case "()":
return { wrapped: `(${text})`, attachWithSpace: true };
default: {
const trimmed = String(containment).trim();
if (!trimmed) return { wrapped: `(${text})`, attachWithSpace: true };
const segments = Array.from(RESET_CONTAINMENT_SEGMENTER.segment(trimmed), (entry) => entry.segment)
.map((segment) => segment.trim())
.filter(Boolean);
if (segments.length === 0) return { wrapped: `(${text})`, attachWithSpace: true };
const left = segments[0];
const right = segments[1] ?? left;
return { wrapped: `${left}${text}${right}`, attachWithSpace: true };
}
}
}
function formatResetDateTime(resetAt: string): string {
const date = new Date(resetAt);
if (Number.isNaN(date.getTime())) return resetAt;
return new Intl.DateTimeFormat(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
function getBarTypeLevels(barType: BarType): string[] | null {
switch (barType) {
case "horizontal-single":
return ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
case "vertical":
return ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
case "braille":
return ["⡀", "⡄", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"];
case "shade":
return ["░", "▒", "▓", "█"];
default:
return null;
}
}
function renderBarSegments(
percent: number,
width: number,
levels: string[],
options?: { allowMinimum?: boolean; emptyChar?: string }
): { segments: Array<{ char: string; filled: boolean }>; minimal: boolean } {
const totalUnits = Math.max(1, width) * levels.length;
let filledUnits = Math.round((percent / 100) * totalUnits);
let minimal = false;
if (options?.allowMinimum && percent > 0 && filledUnits === 0) {
filledUnits = 1;
minimal = true;
}
const emptyChar = options?.emptyChar ?? " ";
const segments: Array<{ char: string; filled: boolean }> = [];
for (let i = 0; i < Math.max(1, width); i++) {
if (filledUnits >= levels.length) {
segments.push({ char: levels[levels.length - 1], filled: true });
filledUnits -= levels.length;
continue;
}
if (filledUnits > 0) {
segments.push({ char: levels[Math.min(levels.length - 1, filledUnits - 1)], filled: true });
filledUnits = 0;
continue;
}
segments.push({ char: emptyChar, filled: false });
}
return { segments, minimal };
}
function formatProviderLabel(theme: Theme, usage: UsageSnapshot, settings?: Settings, model?: ModelInput): string {
const showProviderName = settings?.display.showProviderName ?? true;
const showStatus = settings?.providers[usage.provider]?.showStatus ?? true;
const error = usage.error;
const fetchError = Boolean(error && !isExpectedMissingData(error));
const baseStatus = showStatus ? usage.status : undefined;
const lastSuccessAt = usage.lastSuccessAt;
const elapsed = lastSuccessAt ? formatElapsedSince(lastSuccessAt) : undefined;
const fetchDescription = elapsed
? (elapsed === "just now" ? "Last upd.: just now" : `Last upd.: ${elapsed} ago`)
: "Fetch failed";
const fetchStatus: ProviderStatus | undefined = fetchError
? { indicator: "minor", description: fetchDescription }
: undefined;
const status = showStatus ? (fetchStatus ?? baseStatus) : undefined;
const statusDismissOk = settings?.display.statusDismissOk ?? true;
const statusModeRaw = settings?.display.statusIndicatorMode ?? "icon";
const statusMode = statusModeRaw === "icon" || statusModeRaw === "text" || statusModeRaw === "icon+text"
? statusModeRaw
: "icon";
const statusIconPack = settings?.display.statusIconPack ?? "emoji";
const statusIconCustom = settings?.display.statusIconCustom;
const providerLabelSetting = settings?.display.providerLabel ?? "none";
const showColon = settings?.display.providerLabelColon ?? true;
const boldProviderLabel = settings?.display.providerLabelBold ?? false;
const baseTextColor = resolveBaseTextColor(settings?.display.baseTextColor);
const usageTargets = resolveUsageColorTargets(settings);
const statusActive = Boolean(status && (!statusDismissOk || status.indicator !== "none"));
const showIcon = statusActive && (statusMode === "icon" || statusMode === "icon+text");
const showText = statusActive && (statusMode === "text" || statusMode === "icon+text");
const labelSuffix = providerLabelSetting === "plan"
? "Plan"
: providerLabelSetting === "subscription"
? "Subscription"
: providerLabelSetting === "sub"
? "Sub."
: providerLabelSetting === "none"
? ""
: String(providerLabelSetting);
const rawName = usage.displayName?.trim() ?? "";
const baseName = rawName.replace(/\s+(plan|subscription|sub\.?)[\s]*$/i, "").trim();
const resolvedProviderName = baseName || rawName;
const isSpark = usage.provider === "codex" && isCodexSparkModel(model);
const providerName = isSpark ? `${resolvedProviderName} (Spark)` : resolvedProviderName;
const providerLabel = showProviderName
? [providerName, labelSuffix].filter(Boolean).join(" ")
: "";
const providerLabelWithColon = providerLabel && showColon ? `${providerLabel}:` : providerLabel;
const icon = showIcon && status ? getStatusIcon(status, statusIconPack, statusIconCustom) : "";
const statusText = showText && status ? getStatusLabel(status) : "";
const rawStatusColor = status
? getStatusColor(status.indicator, settings?.display.colorScheme ?? "base-warning-error")
: "base";
const statusTint = usageTargets.status
? resolveStatusTintColor(rawStatusColor, baseTextColor)
: baseTextColor;
const statusColor = statusTint;
const dividerEnabled = settings?.display.statusProviderDivider ?? false;
const dividerChar = settings?.display.dividerCharacter ?? "│";
const dividerColor = resolveDividerColor(settings?.display.dividerColor);
const dividerGlyph = dividerChar === "none"
? ""
: dividerChar === "blank"
? " "
: dividerChar;
const statusParts: string[] = [];
if (icon) statusParts.push(applyBaseTextColor(theme, statusColor, icon));
if (statusText) statusParts.push(applyBaseTextColor(theme, statusColor, statusText));
const parts: string[] = [];
if (statusParts.length > 0) {
parts.push(statusParts.join(" "));
}
if (providerLabelWithColon) {
if (statusParts.length > 0 && dividerEnabled && dividerGlyph) {
parts.push(theme.fg(dividerColor, dividerGlyph));
}
const colored = applyBaseTextColor(theme, baseTextColor, providerLabelWithColon);
parts.push(boldProviderLabel ? theme.bold(colored) : colored);
}
if (parts.length === 0) return "";
return parts.join(" ");
}
/**
* Format a single usage window as a styled string
*/
export function formatUsageWindow(
theme: Theme,
window: RateWindow,
isCodex: boolean,
settings?: Settings,
usage?: UsageSnapshot,
options?: { useNormalColors?: boolean; barWidthOverride?: number },
model?: ModelInput
): string {
const parts = formatUsageWindowParts(theme, window, isCodex, settings, usage, options, model);
const baseTextColor = resolveBaseTextColor(settings?.display.baseTextColor);
const usageTargets = resolveUsageColorTargets(settings);
// Special handling for Extra usage label
if (window.label.startsWith("Extra [")) {
const match = window.label.match(/^(Extra \[)(on|active)(\] .*)$/);
if (match) {
const [, prefix, status, suffix] = match;
const styledLabel =
status === "active"
? applyBaseTextColor(theme, baseTextColor, prefix)
+ theme.fg("text", status)
+ applyBaseTextColor(theme, baseTextColor, suffix)
: applyBaseTextColor(theme, baseTextColor, window.label);
const extraParts = [styledLabel, parts.bar, parts.pct].filter(Boolean);
return extraParts.join(" ");
}
if (!usageTargets.title) {
const extraParts = [applyBaseTextColor(theme, baseTextColor, window.label), parts.bar, parts.pct].filter(Boolean);
return extraParts.join(" ");
}
const extraColor = getUsageColor(window.usedPercent, false, settings?.display.colorScheme ?? "base-warning-error");
const extraTextColor = (options?.useNormalColors && extraColor === "base")
? "text"
: extraColor === "base"
? baseTextColor
: extraColor;
const extraParts = [applyBaseTextColor(theme, extraTextColor, window.label), parts.bar, parts.pct].filter(Boolean);
return extraParts.join(" ");
}
const joinedParts = [parts.label, parts.bar, parts.pct, parts.reset].filter(Boolean);
return joinedParts.join(" ");
}
export function formatUsageWindowParts(
theme: Theme,
window: RateWindow,
isCodex: boolean,
settings?: Settings,
usage?: UsageSnapshot,
options?: { useNormalColors?: boolean; barWidthOverride?: number },
model?: ModelInput
): UsageWindowParts {
const barStyle: BarStyle = settings?.display.barStyle ?? "both";
const barWidthSetting = settings?.display.barWidth;
const containBar = settings?.display.containBar ?? false;
const barWidth = options?.barWidthOverride ?? (typeof barWidthSetting === "number" ? barWidthSetting : 6);
const barType: BarType = settings?.display.barType ?? "horizontal-bar";
const brailleFillEmpty = settings?.display.brailleFillEmpty ?? false;
const brailleFullBlocks = settings?.display.brailleFullBlocks ?? false;
const barCharacter: BarCharacter = settings?.display.barCharacter ?? "heavy";
const colorScheme: ColorScheme = settings?.display.colorScheme ?? "base-warning-error";
const resetTimePosition = settings?.display.resetTimePosition ?? "front";
const resetTimeFormat = settings?.display.resetTimeFormat ?? "relative";
const showUsageLabels = settings?.display.showUsageLabels ?? true;
const showWindowTitle = settings?.display.showWindowTitle ?? true;
const boldWindowTitle = settings?.display.boldWindowTitle ?? false;
const baseTextColor = resolveBaseTextColor(settings?.display.baseTextColor);
const errorThreshold = settings?.display.errorThreshold ?? 25;
const warningThreshold = settings?.display.warningThreshold ?? 50;
const successThreshold = settings?.display.successThreshold ?? 75;
const rawUsedPct = Math.round(window.usedPercent);
const usedPct = clampPercent(rawUsedPct);
const displayPct = isCodex ? clampPercent(100 - usedPct) : usedPct;
const isRemaining = isCodex;
const barPercent = clampPercent(displayPct);
const filled = Math.round((barPercent / 100) * barWidth);
const empty = Math.max(0, barWidth - filled);
const baseColor = getUsageColor(displayPct, isRemaining, colorScheme, errorThreshold, warningThreshold, successThreshold);
const usageTargets = resolveUsageColorTargets(settings);
const usageTextColor = (options?.useNormalColors && baseColor === "base")
? "text"
: baseColor === "base"
? baseTextColor
: baseColor;
const neutralTextColor = options?.useNormalColors ? "text" : baseTextColor;
const titleColor = usageTargets.title ? usageTextColor : neutralTextColor;
const timerColor = usageTargets.timer ? usageTextColor : neutralTextColor;
const usageLabelColor = usageTargets.usageLabel ? usageTextColor : neutralTextColor;
const barUsageColor = (options?.useNormalColors && baseColor === "base") ? "text" : baseColor === "base" ? "muted" : baseColor;
const neutralBarColor = baseTextColor === "dim" ? "dim" : "muted";
const barColor = usageTargets.bar ? barUsageColor : neutralBarColor;
const { filled: filledChar, empty: emptyChar } = getBarCharacters(barCharacter);
const emptyColor = "dim";
let barStr = "";
if ((barStyle === "bar" || barStyle === "both") && barWidth > 0) {
let levels = getBarTypeLevels(barType);
if (barType === "braille" && brailleFullBlocks) {
levels = ["⣿"];
}
if (!levels || barType === "horizontal-bar") {
const filledCharWidth = Math.max(1, visibleWidth(filledChar));
const emptyCharWidth = Math.max(1, visibleWidth(emptyChar));
const segmentCount = barWidth > 0 ? Math.floor(barWidth / filledCharWidth) : 0;
const filledSegments = segmentCount > 0 ? Math.round((barPercent / 100) * segmentCount) : 0;
const filledStr = filledChar.repeat(filledSegments);
const filledWidth = filledSegments * filledCharWidth;
const remainingWidth = Math.max(0, barWidth - filledWidth);
const emptySegments = emptyCharWidth > 0 ? Math.floor(remainingWidth / emptyCharWidth) : 0;
const emptyStr = emptyChar.repeat(emptySegments);
const emptyRendered = emptyChar === " " ? emptyStr : theme.fg(emptyColor, emptyStr);
barStr = theme.fg(barColor as Parameters<typeof theme.fg>[0], filledStr) + emptyRendered;
const barVisualWidth = visibleWidth(barStr);
if (barVisualWidth < barWidth) {
barStr += " ".repeat(barWidth - barVisualWidth);
}
} else {
const emptyChar = barType === "braille" && brailleFillEmpty && barWidth > 1 ? "⣿" : " ";
const { segments, minimal } = renderBarSegments(barPercent, barWidth, levels, {
allowMinimum: true,
emptyChar,
});
const filledColor = minimal ? "dim" : barColor;
barStr = segments
.map((segment) => {
if (segment.filled) {
return theme.fg(filledColor as Parameters<typeof theme.fg>[0], segment.char);
}
if (segment.char === " ") {
return segment.char;
}
return theme.fg("dim", segment.char);
})
.join("");
}
if (settings?.display.containBar && barStr) {
const leftCap = theme.fg(barColor as Parameters<typeof theme.fg>[0], "▕");
const rightCap = theme.fg(barColor as Parameters<typeof theme.fg>[0], "▏");
barStr = leftCap + barStr + rightCap;
}
}
let pctStr = "";
if (barStyle === "percentage" || barStyle === "both") {
// Special handling for Copilot Month window - can show percentage or requests
if (window.label === "Month" && usage?.provider === "copilot") {
const quotaDisplay = settings?.providers.copilot.quotaDisplay ?? "percentage";
if (quotaDisplay === "requests" && usage.requestsRemaining !== undefined && usage.requestsEntitlement !== undefined) {
const used = usage.requestsEntitlement - usage.requestsRemaining;
const suffix = showUsageLabels ? " used" : "";
pctStr = applyBaseTextColor(theme, usageLabelColor, `${used}/${usage.requestsEntitlement}${suffix}`);
} else {
const suffix = showUsageLabels ? " used" : "";
pctStr = applyBaseTextColor(theme, usageLabelColor, `${usedPct}%${suffix}`);
}
} else if (isCodex) {
const suffix = showUsageLabels ? " rem." : "";
pctStr = applyBaseTextColor(theme, usageLabelColor, `${displayPct}%${suffix}`);
} else {
const suffix = showUsageLabels ? " used" : "";
pctStr = applyBaseTextColor(theme, usageLabelColor, `${usedPct}%${suffix}`);
}
}
const isActiveReset = window.resetDescription === "__ACTIVE__";
const resetText = isActiveReset
? undefined
: resetTimeFormat === "datetime"
? (window.resetAt ? formatResetDateTime(window.resetAt) : window.resetDescription)
: window.resetDescription;
const resetContainment = settings?.display.resetTimeContainment ?? "()";
const leftSuffix = resetText && resetTimeFormat === "relative" && showUsageLabels ? " left" : "";
const displayLabel = getDisplayWindowLabel(window, model);
const coloredTitle = applyBaseTextColor(theme, titleColor, displayLabel);
const titlePart = showWindowTitle ? (boldWindowTitle ? theme.bold(coloredTitle) : coloredTitle) : "";
let labelPart = titlePart;
if (resetText) {
const resetBody = `${resetText}${leftSuffix}`;
const { wrapped, attachWithSpace } = wrapResetContainment(resetBody, resetContainment);
const coloredReset = applyBaseTextColor(theme, timerColor, wrapped);
if (resetTimePosition === "front") {
if (!titlePart) {
labelPart = coloredReset;
} else {
labelPart = attachWithSpace ? `${titlePart} ${coloredReset}` : `${titlePart}${coloredReset}`;
}
} else if (resetTimePosition === "integrated") {
labelPart = titlePart ? `${applyBaseTextColor(theme, timerColor, `${wrapped}/`)}${titlePart}` : coloredReset;
} else if (resetTimePosition === "back") {
labelPart = titlePart;
}
} else if (!titlePart) {
labelPart = "";
}
const resetPart =
resetTimePosition === "back" && resetText
? applyBaseTextColor(theme, timerColor, wrapResetContainment(`${resetText}${leftSuffix}`, resetContainment).wrapped)
: "";
return {
label: labelPart,
bar: barStr,
pct: pctStr,
reset: resetPart,
};
}
/**
* Format context window usage as a progress bar
*/
export function formatContextBar(
theme: Theme,
context: ContextInfo,
settings?: Settings,
options?: { barWidthOverride?: number }
): string {
// Create a pseudo-RateWindow for context display
const contextWindow: RateWindow = {
label: "Ctx",
usedPercent: context.percent,
// No reset description for context
};
// Format using the same window formatting logic, but with "used" semantics (not inverted)
return formatUsageWindow(theme, contextWindow, false, settings, undefined, options);
}
/**
* Format a complete usage snapshot as a usage line
*/
export function formatUsageStatus(
theme: Theme,
usage: UsageSnapshot,
model?: ModelInput,
settings?: Settings,
context?: ContextInfo
): string | undefined {
const baseTextColor = resolveBaseTextColor(settings?.display.baseTextColor);
const modelInfo = resolveModelInfo(model);
const label = formatProviderLabel(theme, usage, settings, modelInfo);
// If no windows, just show the provider name with error
if (usage.windows.length === 0) {
const errorMsg = usage.error
? applyBaseTextColor(theme, baseTextColor, `(${formatErrorForDisplay(usage.error)})`)
: "";
if (!label) {
return errorMsg;
}
return errorMsg ? `${label} ${errorMsg}` : label;
}
// Build usage bars
const parts: string[] = [];
const isCodex = usage.provider === "codex";
const invertUsage = isCodex && (settings?.providers.codex.invertUsage ?? false);
const modelId = modelInfo?.id;
// Add context bar as leftmost element if enabled
const showContextBar = settings?.display.showContextBar ?? false;
if (showContextBar && context && context.contextWindow > 0) {
parts.push(formatContextBar(theme, context, settings));
}
for (const w of usage.windows) {
// Skip windows that are disabled in settings
if (!shouldShowWindow(usage, w, settings, modelInfo)) {
continue;
}
parts.push(formatUsageWindow(theme, w, invertUsage, settings, usage, undefined, modelInfo));
}
// Add extra usage lines (extra usage off, copilot multiplier, etc.)
const extras = getUsageExtras(usage, settings, modelId);
for (const extra of extras) {
parts.push(applyBaseTextColor(theme, baseTextColor, extra.label));
}
// Build divider from settings
const dividerChar = settings?.display.dividerCharacter ?? "•";
const dividerColor = resolveDividerColor(settings?.display.dividerColor);
const blanksSetting = settings?.display.dividerBlanks ?? 1;
const showProviderDivider = settings?.display.showProviderDivider ?? false;
const blanksPerSide = typeof blanksSetting === "number" ? blanksSetting : 1;
const spacing = " ".repeat(blanksPerSide);
const charToDisplay = dividerChar === "blank" ? " " : dividerChar === "none" ? "" : dividerChar;
const divider = charToDisplay ? spacing + theme.fg(dividerColor, charToDisplay) + spacing : spacing + spacing;
const labelGap = label && parts.length > 0
? showProviderDivider && charToDisplay !== ""
? divider
: spacing
: "";
return label + labelGap + parts.join(divider);
}
export function formatUsageStatusWithWidth(
theme: Theme,
usage: UsageSnapshot,
width: number,
model?: ModelInput,
settings?: Settings,
options?: { labelGapFill?: boolean },
context?: ContextInfo
): string | undefined {
const labelGapFill = options?.labelGapFill ?? false;
const baseTextColor = resolveBaseTextColor(settings?.display.baseTextColor);
const modelInfo = resolveModelInfo(model);
const label = formatProviderLabel(theme, usage, settings, modelInfo);
const showContextBar = settings?.display.showContextBar ?? false;
const hasContext = showContextBar && context && context.contextWindow > 0;
// If no windows, just show the provider name with error
if (usage.windows.length === 0) {
const errorMsg = usage.error
? applyBaseTextColor(theme, baseTextColor, `(${formatErrorForDisplay(usage.error)})`)
: "";
if (!label) {
return errorMsg;
}
return errorMsg ? `${label} ${errorMsg}` : label;
}
const barStyle: BarStyle = settings?.display.barStyle ?? "both";
const hasBar = barStyle === "bar" || barStyle === "both";
const barWidthSetting = settings?.display.barWidth ?? 6;
const dividerBlanksSetting = settings?.display.dividerBlanks ?? 1;
const dividerColor = resolveDividerColor(settings?.display.dividerColor);
const showProviderDivider = settings?.display.showProviderDivider ?? false;
const containBar = settings?.display.containBar ?? false;
const barFill = barWidthSetting === "fill";
const barBaseWidth = typeof barWidthSetting === "number" ? barWidthSetting : (hasBar ? 1 : 0);
const barContainerExtra = containBar && hasBar ? 2 : 0;
const barBaseContentWidth = barFill ? 0 : barBaseWidth;
const barBaseWidthCalc = barFill ? 0 : barBaseContentWidth + barContainerExtra;
const barTotalBaseWidth = barBaseWidthCalc;
const baseDividerBlanks = typeof dividerBlanksSetting === "number" ? dividerBlanksSetting : 1;
const dividerFill = dividerBlanksSetting === "fill";
// Build usage windows
const windows: RateWindow[] = [];
const isCodex = usage.provider === "codex";
const invertUsage = isCodex && (settings?.providers.codex.invertUsage ?? false);
const modelId = modelInfo?.id;
// Add context window as first entry if enabled
let contextWindowIndex = -1;
if (hasContext) {
contextWindowIndex = windows.length;
windows.push({
label: "Ctx",
usedPercent: context!.percent,
});
}
for (const w of usage.windows) {
if (!shouldShowWindow(usage, w, settings, modelInfo)) {
continue;
}
windows.push(w);
}
const barEligibleCount = hasBar ? windows.length : 0;
const extras = getUsageExtras(usage, settings, modelId);
const extraParts = extras.map((extra) => applyBaseTextColor(theme, baseTextColor, extra.label));
const barSpacerWidth = hasBar ? 1 : 0;
const baseWindowWidths = windows.map((w, i) => {
// Context window uses false for invertUsage (always show used percentage)
const isContext = i === contextWindowIndex;
return (
visibleWidth(
formatUsageWindow(
theme,
w,
isContext ? false : invertUsage,
settings,
isContext ? undefined : usage,
{ barWidthOverride: 0 },
modelInfo
)
) + barSpacerWidth
);
});
const extraWidths = extraParts.map((part) => visibleWidth(part));
const partCount = windows.length + extraParts.length;
const dividerCount = Math.max(0, partCount - 1);
const dividerChar = settings?.display.dividerCharacter ?? "•";
const charToDisplay = dividerChar === "blank" ? " " : dividerChar === "none" ? "" : dividerChar;
const dividerBaseWidth = (charToDisplay ? 1 : 0) + baseDividerBlanks * 2;
const labelGapEnabled = partCount > 0 && (label !== "" || labelGapFill);
const providerDividerActive = showProviderDivider && charToDisplay !== "" && label !== "";
const labelGapBaseWidth = labelGapEnabled
? providerDividerActive
? dividerBaseWidth
: baseDividerBlanks
: 0;
const labelWidth = visibleWidth(label);
const baseTotalWidth =
labelWidth +
labelGapBaseWidth +
baseWindowWidths.reduce((sum, w) => sum + w, 0) +
extraWidths.reduce((sum, w) => sum + w, 0) +
(barEligibleCount * barTotalBaseWidth) +
(dividerCount * dividerBaseWidth);
let remainingWidth = width - baseTotalWidth;
if (remainingWidth < 0) {
remainingWidth = 0;
}
const useBars = barFill && barEligibleCount > 0;
const labelGapUnits = labelGapEnabled ? (providerDividerActive ? 2 : 1) : 0;
const dividerSlots = dividerCount + (labelGapEnabled ? 1 : 0);
const dividerUnits = dividerCount * 2 + labelGapUnits;
const useDividers = dividerFill && dividerUnits > 0;
let barExtraTotal = 0;
let dividerExtraTotal = 0;
if (remainingWidth > 0 && (useBars || useDividers)) {
const barWeight = useBars ? barEligibleCount : 0;
const dividerWeight = useDividers ? dividerUnits : 0;
const totalWeight = barWeight + dividerWeight;
if (totalWeight > 0) {
barExtraTotal = Math.floor((remainingWidth * barWeight) / totalWeight);
dividerExtraTotal = remainingWidth - barExtraTotal;
}
}
const barWidths: number[] = windows.map(() => barBaseWidthCalc);
if (useBars && barEligibleCount > 0) {
const perBar = Math.floor(barExtraTotal / barEligibleCount);
let remainder = barExtraTotal % barEligibleCount;
for (let i = 0; i < barWidths.length; i++) {
barWidths[i] = barBaseWidthCalc + perBar + (remainder > 0 ? 1 : 0);
if (remainder > 0) remainder -= 1;
}
}
let labelBlanks = labelGapEnabled ? baseDividerBlanks : 0;
const dividerBlanks: number[] = [];
if (dividerUnits > 0) {
const baseUnit = useDividers ? Math.floor(dividerExtraTotal / dividerUnits) : 0;
let remainderUnits = useDividers ? dividerExtraTotal % dividerUnits : 0;
if (labelGapEnabled) {
if (useDividers && providerDividerActive) {
let extraUnits = baseUnit * 2;
if (remainderUnits >= 2) {
extraUnits += 2;
remainderUnits -= 2;
}
labelBlanks = baseDividerBlanks + Math.floor(extraUnits / 2);
} else if (useDividers) {
labelBlanks = baseDividerBlanks + baseUnit + (remainderUnits > 0 ? 1 : 0);
if (remainderUnits > 0) remainderUnits -= 1;
}
}
for (let i = 0; i < dividerCount; i++) {
let extraUnits = baseUnit * 2;
if (remainderUnits >= 2) {
extraUnits += 2;
remainderUnits -= 2;
}
const blanks = baseDividerBlanks + Math.floor(extraUnits / 2);
dividerBlanks.push(blanks);
}
}
const parts: string[] = [];
for (let i = 0; i < windows.length; i++) {
const totalWidth = barWidths[i] ?? barBaseWidthCalc;
const contentWidth = containBar ? Math.max(0, totalWidth - barContainerExtra) : totalWidth;
const isContext = i === contextWindowIndex;
parts.push(
formatUsageWindow(
theme,
windows[i],
isContext ? false : invertUsage,
settings,
isContext ? undefined : usage,
{ barWidthOverride: contentWidth },
modelInfo
)
);
}
for (const extra of extraParts) {
parts.push(extra);
}
let rest = "";
for (let i = 0; i < parts.length; i++) {
rest += parts[i];
if (i < dividerCount) {
const blanks = dividerBlanks[i] ?? baseDividerBlanks;
const spacing = " ".repeat(Math.max(0, blanks));
rest += charToDisplay
? spacing + theme.fg(dividerColor, charToDisplay) + spacing
: spacing + spacing;
}
}
let labelGapExtra = 0;
if (labelGapFill && labelGapEnabled) {
const restWidth = visibleWidth(rest);
const labelGapWidth = providerDividerActive
? (Math.max(0, labelBlanks) * 2) + (charToDisplay ? 1 : 0)
: Math.max(0, labelBlanks);
const totalWidth = visibleWidth(label) + restWidth + labelGapWidth;
labelGapExtra = Math.max(0, width - totalWidth);
}
let output = label;
if (labelGapEnabled) {
if (providerDividerActive) {
const spacing = " ".repeat(Math.max(0, labelBlanks));
output += spacing + theme.fg(dividerColor, charToDisplay) + spacing + " ".repeat(labelGapExtra);
} else {
output += " ".repeat(Math.max(0, labelBlanks + labelGapExtra));
}
}
output += rest;
if (width > 0 && visibleWidth(output) > width) {
return truncateToWidth(output, width, "");
}
return output;
}
@@ -1,21 +0,0 @@
/**
* Shared path helpers for sub-bar settings storage.
*/
import { getAgentDir } from "@mariozechner/pi-coding-agent";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const SETTINGS_FILE_NAME = "pi-sub-bar-settings.json";
export function getExtensionDir(): string {
return join(dirname(fileURLToPath(import.meta.url)), "..");
}
export function getSettingsPath(): string {
return join(getAgentDir(), SETTINGS_FILE_NAME);
}
export function getLegacySettingsPath(): string {
return join(getExtensionDir(), "settings.json");
}
@@ -1,21 +0,0 @@
/**
* Provider-specific extra usage lines (non-window info).
*/
import type { UsageSnapshot } from "../types.js";
import type { Settings } from "../settings-types.js";
import { PROVIDER_METADATA, type UsageExtra } from "./metadata.js";
export type { UsageExtra } from "./metadata.js";
export function getUsageExtras(
usage: UsageSnapshot,
settings?: Settings,
modelId?: string
): UsageExtra[] {
const handler = PROVIDER_METADATA[usage.provider]?.getExtras;
if (handler) {
return handler(usage, settings, modelId);
}
return [];
}
@@ -1,202 +0,0 @@
/**
* Provider metadata shared across the extension.
*/
import type { RateWindow, UsageSnapshot, ProviderName, ModelInfo } from "../types.js";
import type { Settings } from "../settings-types.js";
import { getModelMultiplier, normalizeTokens } from "../utils.js";
import { PROVIDER_METADATA as BASE_METADATA, type ProviderMetadata as BaseProviderMetadata } from "../../shared.js";
export { PROVIDERS, PROVIDER_DISPLAY_NAMES } from "../../shared.js";
export type { ProviderStatusConfig, ProviderDetectionConfig } from "../../shared.js";
export interface UsageExtra {
label: string;
}
export interface ProviderMetadata extends BaseProviderMetadata {
isWindowVisible?: (usage: UsageSnapshot, window: RateWindow, settings?: Settings, model?: ModelInfo) => boolean;
getExtras?: (usage: UsageSnapshot, settings?: Settings, modelId?: string) => UsageExtra[];
}
const anthropicWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
if (!settings) return true;
const ps = settings.providers.anthropic;
if (window.label === "5h") return ps.windows.show5h;
if (window.label === "Week") return ps.windows.show7d;
if (window.label.startsWith("Extra [")) return ps.windows.showExtra;
return true;
};
const copilotWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
if (!settings) return true;
const ps = settings.providers.copilot;
if (window.label === "Month") return ps.windows.showMonth;
return true;
};
const geminiWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
if (!settings) return true;
const ps = settings.providers.gemini;
if (window.label === "Pro") return ps.windows.showPro;
if (window.label === "Flash") return ps.windows.showFlash;
return true;
};
const antigravityWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, model) => {
if (!settings) return true;
const ps = settings.providers.antigravity;
const label = window.label.trim();
const normalized = label.toLowerCase().replace(/\s+/g, "_");
if (normalized === "tab_flash_lite_preview") return false;
const labelTokens = normalizeTokens(label);
const modelProvider = model?.provider?.toLowerCase() ?? "";
const modelId = model?.id;
const providerMatches = modelProvider.includes("antigravity");
if (ps.showCurrentModel && providerMatches && modelId) {
const modelTokens = normalizeTokens(modelId);
const match = modelTokens.length > 0 && modelTokens.every((token) => labelTokens.includes(token));
if (match) return true;
}
if (ps.showScopedModels) {
const scopedPatterns = model?.scopedModelPatterns ?? [];
const matchesScoped = scopedPatterns.some((pattern) => {
if (!pattern) return false;
const [rawPattern] = pattern.split(":");
const trimmed = rawPattern?.trim();
if (!trimmed) return false;
const hasProvider = trimmed.includes("/");
if (!hasProvider) return false;
const providerPart = trimmed.slice(0, trimmed.indexOf("/")).trim().toLowerCase();
if (!providerPart.includes("antigravity")) return false;
const base = trimmed.slice(trimmed.lastIndexOf("/") + 1);
const tokens = normalizeTokens(base);
return tokens.length > 0 && tokens.every((token) => labelTokens.includes(token));
});
if (matchesScoped) return true;
}
const visibility = ps.modelVisibility?.[label];
return visibility === true;
};
const codexWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, model) => {
if (!settings) return true;
const ps = settings.providers.codex;
const isSparkModel = isCodexSparkModel(model);
const isSparkWindow = isCodexSparkWindow(window);
if (isSparkWindow) {
if (!isSparkModel) return false;
return shouldShowCodexWindowBySetting(ps, window);
}
if (isSparkModel) {
return false;
}
return shouldShowCodexWindowBySetting(ps, window);
};
const isCodexSparkModel = (model?: ModelInfo): boolean => {
const tokens = normalizeTokens(model?.id ?? "");
return tokens.includes("codex") && tokens.includes("spark");
};
const isCodexSparkWindow = (window: RateWindow): boolean => {
const tokens = normalizeTokens(window.label ?? "");
return tokens.includes("codex") && tokens.includes("spark");
};
const shouldShowCodexWindowBySetting = (
ps: Settings["providers"]["codex"],
window: RateWindow
): boolean => {
if (window.label === "") return true;
if (/\b\d+h$/.test(window.label.trim())) {
return ps.windows.showPrimary;
}
if (window.label === "Day" || window.label === "Week" || /\b(day|week)\b/.test(window.label.toLowerCase())) {
return ps.windows.showSecondary;
}
return true;
};
const kiroWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
if (!settings) return true;
const ps = settings.providers.kiro;
if (window.label === "Credits") return ps.windows.showCredits;
return true;
};
const zaiWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
if (!settings) return true;
const ps = settings.providers.zai;
if (window.label === "Tokens") return ps.windows.showTokens;
if (window.label === "Monthly") return ps.windows.showMonthly;
return true;
};
const anthropicExtras: ProviderMetadata["getExtras"] = (usage, settings) => {
const extras: UsageExtra[] = [];
const showExtraWindow = settings?.providers.anthropic.windows.showExtra ?? true;
if (showExtraWindow && usage.extraUsageEnabled === false) {
extras.push({ label: "Extra [off]" });
}
return extras;
};
const copilotExtras: ProviderMetadata["getExtras"] = (usage, settings, modelId) => {
const extras: UsageExtra[] = [];
const showMultiplier = settings?.providers.copilot.showMultiplier ?? true;
const showRequestsLeft = settings?.providers.copilot.showRequestsLeft ?? true;
if (!showMultiplier) return extras;
const multiplier = getModelMultiplier(modelId);
const remaining = usage.requestsRemaining;
if (multiplier !== undefined) {
let multiplierStr = `Model multiplier: ${multiplier}x`;
if (showRequestsLeft && remaining !== undefined) {
const leftCount = Math.floor(remaining / Math.max(multiplier, 0.0001));
multiplierStr += ` (${leftCount} req. left)`;
}
extras.push({ label: multiplierStr });
}
return extras;
};
export const PROVIDER_METADATA: Record<ProviderName, ProviderMetadata> = {
anthropic: {
...BASE_METADATA.anthropic,
isWindowVisible: anthropicWindowVisible,
getExtras: anthropicExtras,
},
copilot: {
...BASE_METADATA.copilot,
isWindowVisible: copilotWindowVisible,
getExtras: copilotExtras,
},
gemini: {
...BASE_METADATA.gemini,
isWindowVisible: geminiWindowVisible,
},
antigravity: {
...BASE_METADATA.antigravity,
isWindowVisible: antigravityWindowVisible,
},
codex: {
...BASE_METADATA.codex,
isWindowVisible: codexWindowVisible,
},
kiro: {
...BASE_METADATA.kiro,
isWindowVisible: kiroWindowVisible,
},
zai: {
...BASE_METADATA.zai,
isWindowVisible: zaiWindowVisible,
},
"opencode-go": {
...BASE_METADATA["opencode-go"],
},
};
@@ -1,359 +0,0 @@
/**
* Provider-specific settings helpers.
*/
import type { SettingItem } from "@mariozechner/pi-tui";
import type { ProviderName } from "../types.js";
import type {
Settings,
BaseProviderSettings,
AnthropicProviderSettings,
CopilotProviderSettings,
GeminiProviderSettings,
AntigravityProviderSettings,
CodexProviderSettings,
KiroProviderSettings,
ZaiProviderSettings,
} from "../settings-types.js";
function buildBaseProviderItems(ps: BaseProviderSettings): SettingItem[] {
return [
{
id: "showStatus",
label: "Show Status Indicator",
currentValue: ps.showStatus ? "on" : "off",
values: ["on", "off"],
description: "Show status indicator for this provider.",
},
];
}
function applyBaseProviderSetting(ps: BaseProviderSettings, id: string, value: string): boolean {
switch (id) {
case "showStatus":
ps.showStatus = value === "on";
return true;
default:
return false;
}
}
/**
* Build settings items for a specific provider.
*/
export function buildProviderSettingsItems(settings: Settings, provider: ProviderName): SettingItem[] {
const ps = settings.providers[provider];
const items: SettingItem[] = [...buildBaseProviderItems(ps)];
if (provider === "anthropic") {
const anthroSettings = ps as AnthropicProviderSettings;
items.push(
{
id: "show5h",
label: "Show 5h Window",
currentValue: anthroSettings.windows.show5h ? "on" : "off",
values: ["on", "off"],
description: "Show the 5-hour usage window.",
},
{
id: "show7d",
label: "Show Week Window",
currentValue: anthroSettings.windows.show7d ? "on" : "off",
values: ["on", "off"],
description: "Show the weekly usage window.",
},
{
id: "showExtra",
label: "Show Extra Window",
currentValue: anthroSettings.windows.showExtra ? "on" : "off",
values: ["on", "off"],
description: "Show the extra usage window.",
},
);
}
if (provider === "copilot") {
const copilotSettings = ps as CopilotProviderSettings;
items.push(
{
id: "showMultiplier",
label: "Show Model Multiplier",
currentValue: copilotSettings.showMultiplier ? "on" : "off",
values: ["on", "off"],
description: "Show request cost multiplier for the current model.",
},
{
id: "showRequestsLeft",
label: "Show Requests Remaining",
currentValue: copilotSettings.showRequestsLeft ? "on" : "off",
values: ["on", "off"],
description: "Estimate requests remaining based on the multiplier.",
},
{
id: "quotaDisplay",
label: "Show Quota in",
currentValue: copilotSettings.quotaDisplay,
values: ["percentage", "requests"],
description: "Display Copilot usage as percentage or requests.",
},
{
id: "showMonth",
label: "Show Month Window",
currentValue: copilotSettings.windows.showMonth ? "on" : "off",
values: ["on", "off"],
description: "Show the monthly usage window.",
},
);
}
if (provider === "gemini") {
const geminiSettings = ps as GeminiProviderSettings;
items.push(
{
id: "showPro",
label: "Show Pro Window",
currentValue: geminiSettings.windows.showPro ? "on" : "off",
values: ["on", "off"],
description: "Show the Pro quota window.",
},
{
id: "showFlash",
label: "Show Flash Window",
currentValue: geminiSettings.windows.showFlash ? "on" : "off",
values: ["on", "off"],
description: "Show the Flash quota window.",
},
);
}
if (provider === "antigravity") {
const antigravitySettings = ps as AntigravityProviderSettings;
items.push(
{
id: "showCurrentModel",
label: "Always Show Current Model",
currentValue: antigravitySettings.showCurrentModel ? "on" : "off",
values: ["on", "off"],
description: "Show the active Antigravity model even if hidden.",
},
{
id: "showScopedModels",
label: "Show Scoped Models",
currentValue: antigravitySettings.showScopedModels ? "on" : "off",
values: ["on", "off"],
description: "Show Antigravity models that are in the scoped model rotation.",
},
);
const modelVisibility = antigravitySettings.modelVisibility ?? {};
const modelOrder = antigravitySettings.modelOrder?.length
? antigravitySettings.modelOrder
: Object.keys(modelVisibility).sort((a, b) => a.localeCompare(b));
const seenModels = new Set<string>();
for (const model of modelOrder) {
if (!model || seenModels.has(model)) continue;
seenModels.add(model);
const normalized = model.toLowerCase().replace(/\s+/g, "_");
if (normalized === "tab_flash_lite_preview") continue;
const visible = modelVisibility[model] !== false;
items.push({
id: `model:${model}`,
label: model,
currentValue: visible ? "on" : "off",
values: ["on", "off"],
description: "Toggle this model window.",
});
}
}
if (provider === "codex") {
const codexSettings = ps as CodexProviderSettings;
items.push(
{
id: "invertUsage",
label: "Invert Usage",
currentValue: codexSettings.invertUsage ? "on" : "off",
values: ["on", "off"],
description: "Show remaining-style usage for Codex.",
},
{
id: "showPrimary",
label: "Show Primary Window",
currentValue: codexSettings.windows.showPrimary ? "on" : "off",
values: ["on", "off"],
description: "Show the primary usage window.",
},
{
id: "showSecondary",
label: "Show Secondary Window",
currentValue: codexSettings.windows.showSecondary ? "on" : "off",
values: ["on", "off"],
description: "Show secondary windows (day/week).",
},
);
}
if (provider === "kiro") {
const kiroSettings = ps as KiroProviderSettings;
items.push({
id: "showCredits",
label: "Show Credits Window",
currentValue: kiroSettings.windows.showCredits ? "on" : "off",
values: ["on", "off"],
description: "Show the credits usage window.",
});
}
if (provider === "zai") {
const zaiSettings = ps as ZaiProviderSettings;
items.push(
{
id: "showTokens",
label: "Show Tokens Window",
currentValue: zaiSettings.windows.showTokens ? "on" : "off",
values: ["on", "off"],
description: "Show the tokens usage window.",
},
{
id: "showMonthly",
label: "Show Monthly Window",
currentValue: zaiSettings.windows.showMonthly ? "on" : "off",
values: ["on", "off"],
description: "Show the monthly usage window.",
},
);
}
return items;
}
/**
* Apply a provider settings change in-place.
*/
export function applyProviderSettingsChange(
settings: Settings,
provider: ProviderName,
id: string,
value: string
): Settings {
const ps = settings.providers[provider];
if (applyBaseProviderSetting(ps, id, value)) {
return settings;
}
if (provider === "anthropic") {
const anthroSettings = ps as AnthropicProviderSettings;
switch (id) {
case "show5h":
anthroSettings.windows.show5h = value === "on";
break;
case "show7d":
anthroSettings.windows.show7d = value === "on";
break;
case "showExtra":
anthroSettings.windows.showExtra = value === "on";
break;
}
}
if (provider === "copilot") {
const copilotSettings = ps as CopilotProviderSettings;
switch (id) {
case "showMultiplier":
copilotSettings.showMultiplier = value === "on";
break;
case "showRequestsLeft":
copilotSettings.showRequestsLeft = value === "on";
break;
case "quotaDisplay":
copilotSettings.quotaDisplay = value as "percentage" | "requests";
break;
case "showMonth":
copilotSettings.windows.showMonth = value === "on";
break;
}
}
if (provider === "gemini") {
const geminiSettings = ps as GeminiProviderSettings;
switch (id) {
case "showPro":
geminiSettings.windows.showPro = value === "on";
break;
case "showFlash":
geminiSettings.windows.showFlash = value === "on";
break;
}
}
if (provider === "antigravity") {
const antigravitySettings = ps as AntigravityProviderSettings;
switch (id) {
case "showModels":
antigravitySettings.windows.showModels = value === "on";
break;
case "showCurrentModel":
antigravitySettings.showCurrentModel = value === "on";
break;
case "showScopedModels":
antigravitySettings.showScopedModels = value === "on";
break;
default:
if (id.startsWith("model:")) {
const model = id.slice("model:".length);
if (model) {
if (!antigravitySettings.modelVisibility) {
antigravitySettings.modelVisibility = {};
}
antigravitySettings.modelVisibility[model] = value === "on";
if (!antigravitySettings.modelOrder) {
antigravitySettings.modelOrder = [];
}
if (!antigravitySettings.modelOrder.includes(model)) {
antigravitySettings.modelOrder.push(model);
}
}
}
break;
}
}
if (provider === "codex") {
const codexSettings = ps as CodexProviderSettings;
switch (id) {
case "invertUsage":
codexSettings.invertUsage = value === "on";
break;
case "showPrimary":
codexSettings.windows.showPrimary = value === "on";
break;
case "showSecondary":
codexSettings.windows.showSecondary = value === "on";
break;
}
}
if (provider === "kiro") {
const kiroSettings = ps as KiroProviderSettings;
switch (id) {
case "showCredits":
kiroSettings.windows.showCredits = value === "on";
break;
}
}
if (provider === "zai") {
const zaiSettings = ps as ZaiProviderSettings;
switch (id) {
case "showTokens":
zaiSettings.windows.showTokens = value === "on";
break;
case "showMonthly":
zaiSettings.windows.showMonthly = value === "on";
break;
}
}
return settings;
}
@@ -1,23 +0,0 @@
/**
* Provider-specific window visibility rules.
*/
import type { RateWindow, UsageSnapshot, ModelInfo } from "../types.js";
import type { Settings } from "../settings-types.js";
import { PROVIDER_METADATA } from "./metadata.js";
/**
* Check if a window should be shown based on settings.
*/
export function shouldShowWindow(
usage: UsageSnapshot,
window: RateWindow,
settings?: Settings,
model?: ModelInfo
): boolean {
const handler = PROVIDER_METADATA[usage.provider]?.isWindowVisible;
if (handler) {
return handler(usage, window, settings, model);
}
return true;
}
@@ -1,611 +0,0 @@
/**
* Settings types and defaults for sub-bar
*/
import type { CoreSettings, ProviderName } from "./shared.js";
import { PROVIDERS } from "./shared.js";
import type { ThemeColor } from "@mariozechner/pi-coding-agent";
/**
* Bar display style
*/
export type BarStyle = "bar" | "percentage" | "both";
/**
* Bar rendering type
*/
export type BarType = "horizontal-bar" | "horizontal-single" | "vertical" | "braille" | "shade";
/**
* Color scheme for usage bars
*/
export type ColorScheme = "monochrome" | "base-warning-error" | "success-base-warning-error";
/**
* Progress bar character style
*/
export type BarCharacter = "light" | "heavy" | "double" | "block" | (string & {});
/**
* Divider character style
*/
export type DividerCharacter =
| "none"
| "blank"
| "|"
| "│"
| "┃"
| "┆"
| "┇"
| "║"
| "•"
| "●"
| "○"
| "◇"
| (string & {});
/**
* Widget overflow mode
*/
export type OverflowMode = "truncate" | "wrap";
export type WidgetWrapping = OverflowMode;
/**
* Widget placement
*/
export type WidgetPlacement = "belowEditor";
/**
* Alignment for the widget
*/
export type DisplayAlignment = "left" | "center" | "right" | "split";
/**
* Provider label prefix
*/
export type ProviderLabel = "plan" | "subscription" | "sub" | "none" | (string & {});
/**
* Reset timer format
*/
export type ResetTimeFormat = "relative" | "datetime";
/**
* Reset timer containment style
*/
export type ResetTimerContainment = "none" | "blank" | "()" | "[]" | "<>" | (string & {});
/**
* Status indicator display mode
*/
export type StatusIndicatorMode = "icon" | "text" | "icon+text";
/**
* Status icon pack selection
*/
export type StatusIconPack = "minimal" | "emoji" | "custom";
export interface UsageColorTargets {
title: boolean;
timer: boolean;
bar: boolean;
usageLabel: boolean;
status: boolean;
}
/**
* Divider color options (subset of theme colors).
*/
export const DIVIDER_COLOR_OPTIONS = [
"primary",
"text",
"muted",
"dim",
"success",
"warning",
"error",
"border",
"borderMuted",
"borderAccent",
] as const;
export type DividerColor = (typeof DIVIDER_COLOR_OPTIONS)[number];
/**
* Background color options (theme background colors).
*/
export const BACKGROUND_COLOR_OPTIONS = [
"selectedBg",
"userMessageBg",
"customMessageBg",
"toolPendingBg",
"toolSuccessBg",
"toolErrorBg",
] as const;
export type BackgroundColor = (typeof BACKGROUND_COLOR_OPTIONS)[number];
/**
* Base text/background color options.
*/
export const BASE_COLOR_OPTIONS = [...DIVIDER_COLOR_OPTIONS, ...BACKGROUND_COLOR_OPTIONS] as const;
/**
* Base text color for widget labels
*/
export type BaseTextColor = (typeof BASE_COLOR_OPTIONS)[number];
export function normalizeDividerColor(value?: string): DividerColor {
if (!value) return "borderMuted";
if (value === "accent" || value === "primary") return "primary";
if ((DIVIDER_COLOR_OPTIONS as readonly string[]).includes(value)) {
return value as DividerColor;
}
return "borderMuted";
}
export function resolveDividerColor(value?: string): ThemeColor {
const normalized = normalizeDividerColor(value);
switch (normalized) {
case "primary":
return "accent";
case "border":
case "borderMuted":
case "borderAccent":
case "success":
case "warning":
case "error":
case "muted":
case "dim":
case "text":
return normalized as ThemeColor;
default:
return "borderMuted";
}
}
export function isBackgroundColor(value?: BaseTextColor): value is BackgroundColor {
return !!value && (BACKGROUND_COLOR_OPTIONS as readonly string[]).includes(value);
}
export function normalizeBaseTextColor(value?: string): BaseTextColor {
if (!value) return "dim";
if (value === "accent" || value === "primary") return "primary";
if ((BASE_COLOR_OPTIONS as readonly string[]).includes(value)) {
return value as BaseTextColor;
}
return "dim";
}
export function resolveBaseTextColor(value?: string): BaseTextColor {
return normalizeBaseTextColor(value);
}
/**
* Bar width configuration
*/
export type BarWidth = number | "fill";
/**
* Divider blank spacing configuration
*/
export type DividerBlanks = number | "fill";
/**
* Provider settings (UI-only)
*/
export interface BaseProviderSettings {
/** Show status indicator */
showStatus: boolean;
}
export interface AnthropicProviderSettings extends BaseProviderSettings {
windows: {
show5h: boolean;
show7d: boolean;
showExtra: boolean;
};
}
export interface CopilotProviderSettings extends BaseProviderSettings {
showMultiplier: boolean;
showRequestsLeft: boolean;
quotaDisplay: "percentage" | "requests";
windows: {
showMonth: boolean;
};
}
export interface GeminiProviderSettings extends BaseProviderSettings {
windows: {
showPro: boolean;
showFlash: boolean;
};
}
export interface AntigravityProviderSettings extends BaseProviderSettings {
showCurrentModel: boolean;
showScopedModels: boolean;
windows: {
showModels: boolean;
};
modelVisibility: Record<string, boolean>;
modelOrder: string[];
}
export interface CodexProviderSettings extends BaseProviderSettings {
invertUsage: boolean;
windows: {
showPrimary: boolean;
showSecondary: boolean;
};
}
export interface KiroProviderSettings extends BaseProviderSettings {
windows: {
showCredits: boolean;
};
}
export interface ZaiProviderSettings extends BaseProviderSettings {
windows: {
showTokens: boolean;
showMonthly: boolean;
};
}
export interface ProviderSettingsMap {
anthropic: AnthropicProviderSettings;
copilot: CopilotProviderSettings;
gemini: GeminiProviderSettings;
antigravity: AntigravityProviderSettings;
codex: CodexProviderSettings;
kiro: KiroProviderSettings;
zai: ZaiProviderSettings;
"opencode-go": BaseProviderSettings;
}
export type { BehaviorSettings, CoreSettings } from "./shared.js";
/**
* Keybinding settings.
* Values are key-combo strings accepted by pi's registerShortcut (e.g. "ctrl+alt+p").
* Use "none" to disable a shortcut.
* Changes take effect after pi restart.
*/
export interface KeybindingSettings {
/** Shortcut to cycle through providers */
cycleProvider: string;
/** Shortcut to toggle reset timer format */
toggleResetFormat: string;
}
/**
* Display settings
*/
export interface DisplaySettings {
/** Alignment */
alignment: DisplayAlignment;
/** Bar display style */
barStyle: BarStyle;
/** Bar type */
barType: BarType;
/** Width of the progress bar in characters */
barWidth: BarWidth;
/** Progress bar character */
barCharacter: BarCharacter;
/** Contain bar within ▕ and ▏ */
containBar: boolean;
/** Fill empty braille segments with dim full blocks */
brailleFillEmpty: boolean;
/** Use full braille blocks for filled segments */
brailleFullBlocks: boolean;
/** Color scheme for bars */
colorScheme: ColorScheme;
/** Elements colored by the usage scheme */
usageColorTargets: UsageColorTargets;
/** Reset time display position */
resetTimePosition: "off" | "front" | "back" | "integrated";
/** Reset time format */
resetTimeFormat: ResetTimeFormat;
/** Reset timer containment */
resetTimeContainment: ResetTimerContainment;
/** Status indicator mode */
statusIndicatorMode: StatusIndicatorMode;
/** Status icon pack */
statusIconPack: StatusIconPack;
/** Custom status icon pack (four characters) */
statusIconCustom: string;
/** Show divider between status and provider */
statusProviderDivider: boolean;
/** Dismiss status when operational */
statusDismissOk: boolean;
/** Show provider display name */
showProviderName: boolean;
/** Provider label prefix */
providerLabel: ProviderLabel;
/** Show colon after provider label */
providerLabelColon: boolean;
/** Bold provider name and colon */
providerLabelBold: boolean;
/** Base text color for widget labels */
baseTextColor: BaseTextColor;
/** Background color for the widget line */
backgroundColor: BaseTextColor;
/** Show window titles (5h, Week, etc.) */
showWindowTitle: boolean;
/** Bold window titles (5h, Week, etc.) */
boldWindowTitle: boolean;
/** Show usage labels (used/rem.) */
showUsageLabels: boolean;
/** Divider character */
dividerCharacter: DividerCharacter;
/** Divider color */
dividerColor: DividerColor;
/** Blanks before and after divider */
dividerBlanks: DividerBlanks;
/** Show divider between provider label and usage */
showProviderDivider: boolean;
/** Connect divider glyphs to the bottom divider line */
dividerFooterJoin: boolean;
/** Show divider line above the bar */
showTopDivider: boolean;
/** Show divider line below the bar */
showBottomDivider: boolean;
/** Widget overflow mode */
overflow: OverflowMode;
/** Left padding inside widget */
paddingLeft: number;
/** Right padding inside widget */
paddingRight: number;
/** Widget placement */
widgetPlacement: WidgetPlacement;
/** Show context window usage as leftmost progress bar */
showContextBar: boolean;
/** Error threshold (percentage remaining below this = red) */
errorThreshold: number;
/** Warning threshold (percentage remaining below this = yellow) */
warningThreshold: number;
/** Success threshold (percentage remaining above this = green, gradient only) */
successThreshold: number;
}
/**
* All settings
*/
export interface DisplayTheme {
id: string;
name: string;
display: DisplaySettings;
source?: "saved" | "imported";
}
export interface Settings extends Omit<CoreSettings, "providers"> {
/** Version for migration */
version: number;
/** Provider-specific UI settings */
providers: ProviderSettingsMap;
/** Display settings */
display: DisplaySettings;
/** Stored display themes */
displayThemes: DisplayTheme[];
/** Snapshot of the previous display theme */
displayUserTheme: DisplaySettings | null;
/** Pinned provider override for display */
pinnedProvider: ProviderName | null;
/** Keybinding settings (changes require pi restart) */
keybindings: KeybindingSettings;
}
/**
* Current settings version
*/
export const SETTINGS_VERSION = 2;
/**
* Default settings
*/
export function getDefaultSettings(): Settings {
return {
version: SETTINGS_VERSION,
providers: {
anthropic: {
showStatus: true,
windows: {
show5h: true,
show7d: true,
showExtra: false,
},
},
copilot: {
showStatus: true,
showMultiplier: true,
showRequestsLeft: true,
quotaDisplay: "percentage",
windows: {
showMonth: true,
},
},
gemini: {
showStatus: true,
windows: {
showPro: true,
showFlash: true,
},
},
antigravity: {
showStatus: true,
showCurrentModel: true,
showScopedModels: true,
windows: {
showModels: true,
},
modelVisibility: {},
modelOrder: [],
},
codex: {
showStatus: true,
invertUsage: false,
windows: {
showPrimary: true,
showSecondary: true,
},
},
kiro: {
showStatus: false,
windows: {
showCredits: true,
},
},
zai: {
showStatus: false,
windows: {
showTokens: true,
showMonthly: true,
},
},
"opencode-go": {
showStatus: false,
},
},
display: {
alignment: "split",
barStyle: "bar",
barType: "horizontal-bar",
barWidth: "fill",
barCharacter: "light",
containBar: false,
brailleFillEmpty: false,
brailleFullBlocks: false,
colorScheme: "base-warning-error",
usageColorTargets: {
title: true,
timer: true,
bar: true,
usageLabel: true,
status: true,
},
resetTimePosition: "front",
resetTimeFormat: "relative",
resetTimeContainment: "blank",
statusIndicatorMode: "icon",
statusIconPack: "emoji",
statusIconCustom: "✓⚠×?",
statusProviderDivider: false,
statusDismissOk: true,
showProviderName: false,
providerLabel: "none",
providerLabelColon: false,
providerLabelBold: true,
baseTextColor: "muted",
backgroundColor: "text",
showWindowTitle: true,
boldWindowTitle: true,
showUsageLabels: true,
dividerCharacter: "│",
dividerColor: "dim",
dividerBlanks: 1,
showProviderDivider: true,
dividerFooterJoin: true,
showTopDivider: false,
showBottomDivider: true,
paddingLeft: 1,
paddingRight: 1,
widgetPlacement: "belowEditor",
showContextBar: false,
errorThreshold: 25,
warningThreshold: 50,
overflow: "wrap",
successThreshold: 75,
},
displayThemes: [],
displayUserTheme: null,
pinnedProvider: null,
keybindings: {
cycleProvider: "ctrl+alt+p",
toggleResetFormat: "ctrl+alt+r",
},
behavior: {
refreshInterval: 60,
minRefreshInterval: 10,
refreshOnTurnStart: false,
refreshOnToolResult: false,
},
statusRefresh: {
refreshInterval: 60,
minRefreshInterval: 10,
refreshOnTurnStart: false,
refreshOnToolResult: false,
},
providerOrder: [...PROVIDERS],
defaultProvider: null,
};
}
/**
* Deep merge two objects
*/
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
const result = { ...target };
for (const key of Object.keys(source) as (keyof T)[]) {
const sourceValue = source[key];
const targetValue = target[key];
if (
sourceValue !== undefined &&
typeof sourceValue === "object" &&
sourceValue !== null &&
!Array.isArray(sourceValue) &&
typeof targetValue === "object" &&
targetValue !== null &&
!Array.isArray(targetValue)
) {
result[key] = deepMerge(targetValue, sourceValue as Partial<typeof targetValue>);
} else if (sourceValue !== undefined) {
result[key] = sourceValue as T[keyof T];
}
}
return result;
}
/**
* Merge settings with defaults (no legacy migrations).
*/
export function mergeSettings(loaded: Partial<Settings>): Settings {
const migrated = migrateSettings(loaded);
return deepMerge(getDefaultSettings(), migrated);
}
function migrateDisplaySettings(display?: Partial<DisplaySettings> | null): void {
if (!display) return;
const displayAny = display as Partial<DisplaySettings> & { widgetWrapping?: OverflowMode; paddingX?: number };
if (displayAny.widgetWrapping !== undefined && displayAny.overflow === undefined) {
displayAny.overflow = displayAny.widgetWrapping;
}
if (displayAny.paddingX !== undefined) {
if (displayAny.paddingLeft === undefined) {
displayAny.paddingLeft = displayAny.paddingX;
}
if (displayAny.paddingRight === undefined) {
displayAny.paddingRight = displayAny.paddingX;
}
delete (displayAny as { paddingX?: unknown }).paddingX;
}
if ("widgetWrapping" in displayAny) {
delete (displayAny as { widgetWrapping?: unknown }).widgetWrapping;
}
}
function migrateSettings(loaded: Partial<Settings>): Partial<Settings> {
migrateDisplaySettings(loaded.display);
migrateDisplaySettings(loaded.displayUserTheme);
if (Array.isArray(loaded.displayThemes)) {
for (const theme of loaded.displayThemes) {
migrateDisplaySettings(theme.display as Partial<DisplaySettings> | undefined);
}
}
return loaded;
}
@@ -1,5 +0,0 @@
/**
* Settings UI entry point (re-export).
*/
export { showSettingsUI } from "./settings/ui.js";
@@ -1,176 +0,0 @@
/**
* Settings persistence for sub-bar
*/
import * as path from "node:path";
import type { Settings } from "./settings-types.js";
import { getDefaultSettings, mergeSettings } from "./settings-types.js";
import { getStorage } from "./storage.js";
import { getLegacySettingsPath, getSettingsPath } from "./paths.js";
/**
* Settings file path
*/
export const SETTINGS_PATH = getSettingsPath();
const LEGACY_SETTINGS_PATH = getLegacySettingsPath();
/**
* In-memory settings cache
*/
let cachedSettings: Settings | undefined;
/**
* Ensure the settings directory exists
*/
function ensureSettingsDir(): void {
const storage = getStorage();
const dir = path.dirname(SETTINGS_PATH);
storage.ensureDir(dir);
}
/**
* Parse settings file contents
*/
function parseSettings(content: string): Settings {
const loaded = JSON.parse(content) as Partial<Settings>;
return mergeSettings({
version: loaded.version,
display: loaded.display,
providers: loaded.providers,
displayThemes: loaded.displayThemes,
displayUserTheme: loaded.displayUserTheme,
pinnedProvider: loaded.pinnedProvider,
keybindings: loaded.keybindings,
} as Partial<Settings>);
}
function loadSettingsFromDisk(settingsPath: string): Settings | null {
const storage = getStorage();
if (storage.exists(settingsPath)) {
const content = storage.readFile(settingsPath);
if (content) {
return parseSettings(content);
}
}
return null;
}
function tryLoadSettings(settingsPath: string): Settings | null {
try {
return loadSettingsFromDisk(settingsPath);
} catch (error) {
console.error(`Failed to load settings from ${settingsPath}:`, error);
return null;
}
}
/**
* Load settings from disk
*/
export function loadSettings(): Settings {
if (cachedSettings) {
return cachedSettings;
}
const diskSettings = tryLoadSettings(SETTINGS_PATH);
if (diskSettings) {
cachedSettings = diskSettings;
return cachedSettings;
}
const legacySettings = tryLoadSettings(LEGACY_SETTINGS_PATH);
if (legacySettings) {
const saved = saveSettings(legacySettings);
if (saved) {
getStorage().removeFile(LEGACY_SETTINGS_PATH);
}
cachedSettings = legacySettings;
return cachedSettings;
}
// Return defaults if file doesn't exist or failed to load
cachedSettings = getDefaultSettings();
return cachedSettings;
}
/**
* Save settings to disk
*/
export function saveSettings(settings: Settings): boolean {
const storage = getStorage();
try {
ensureSettingsDir();
let next = settings;
if (cachedSettings) {
const diskSettings = loadSettingsFromDisk(SETTINGS_PATH);
if (diskSettings) {
const displayChanged = JSON.stringify(settings.display) !== JSON.stringify(cachedSettings.display);
const providersChanged = JSON.stringify(settings.providers) !== JSON.stringify(cachedSettings.providers);
const themesChanged = JSON.stringify(settings.displayThemes) !== JSON.stringify(cachedSettings.displayThemes);
const userThemeChanged = JSON.stringify(settings.displayUserTheme) !== JSON.stringify(cachedSettings.displayUserTheme);
const pinnedChanged = settings.pinnedProvider !== cachedSettings.pinnedProvider;
const keybindingsChanged = JSON.stringify(settings.keybindings) !== JSON.stringify(cachedSettings.keybindings);
next = {
...diskSettings,
version: settings.version,
display: displayChanged ? settings.display : diskSettings.display,
providers: providersChanged ? settings.providers : diskSettings.providers,
displayThemes: themesChanged ? settings.displayThemes : diskSettings.displayThemes,
displayUserTheme: userThemeChanged ? settings.displayUserTheme : diskSettings.displayUserTheme,
pinnedProvider: pinnedChanged ? settings.pinnedProvider : diskSettings.pinnedProvider,
keybindings: keybindingsChanged ? settings.keybindings : diskSettings.keybindings,
};
}
}
const content = JSON.stringify({
version: next.version,
display: next.display,
providers: next.providers,
displayThemes: next.displayThemes,
displayUserTheme: next.displayUserTheme,
pinnedProvider: next.pinnedProvider,
keybindings: next.keybindings,
}, null, 2);
storage.writeFile(SETTINGS_PATH, content);
cachedSettings = next;
return true;
} catch (error) {
console.error(`Failed to save settings to ${SETTINGS_PATH}:`, error);
return false;
}
}
/**
* Reset settings to defaults
*/
export function resetSettings(): Settings {
const defaults = getDefaultSettings();
const current = getSettings();
const next = {
...current,
display: defaults.display,
providers: defaults.providers,
displayThemes: defaults.displayThemes,
displayUserTheme: defaults.displayUserTheme,
pinnedProvider: defaults.pinnedProvider,
keybindings: defaults.keybindings,
version: defaults.version,
};
saveSettings(next);
return next;
}
/**
* Get current settings (cached)
*/
export function getSettings(): Settings {
return loadSettings();
}
/**
* Clear the settings cache (force reload on next access)
*/
export function clearSettingsCache(): void {
cachedSettings = undefined;
}
@@ -1,718 +0,0 @@
/**
* Display settings UI helpers.
*/
import type { SettingItem } from "@mariozechner/pi-tui";
import type {
Settings,
BarStyle,
BarType,
ColorScheme,
BarCharacter,
DividerCharacter,
WidgetWrapping,
DisplayAlignment,
BarWidth,
DividerBlanks,
ProviderLabel,
BaseTextColor,
ResetTimeFormat,
ResetTimerContainment,
StatusIndicatorMode,
StatusIconPack,
DividerColor,
UsageColorTargets,
} from "../settings-types.js";
import {
BASE_COLOR_OPTIONS,
DIVIDER_COLOR_OPTIONS,
normalizeBaseTextColor,
normalizeDividerColor,
} from "../settings-types.js";
import { CUSTOM_OPTION } from "../ui/settings-list.js";
export function buildDisplayLayoutItems(settings: Settings): SettingItem[] {
return [
{
id: "showContextBar",
label: "Show Context Bar",
currentValue: settings.display.showContextBar ? "on" : "off",
values: ["on", "off"],
description: "Show context window usage as leftmost progress bar.",
},
{
id: "alignment",
label: "Alignment",
currentValue: settings.display.alignment,
values: ["left", "center", "right", "split"] as DisplayAlignment[],
description: "Align the usage line inside the widget.",
},
{
id: "overflow",
label: "Overflow",
currentValue: settings.display.overflow,
values: ["truncate", "wrap"] as WidgetWrapping[],
description: "Wrap the usage line or truncate with ellipsis (requires bar width ≠ fill and alignment ≠ split).",
},
{
id: "paddingLeft",
label: "Padding Left",
currentValue: String(settings.display.paddingLeft ?? 0),
values: ["0", "1", "2", "3", "4", CUSTOM_OPTION],
description: "Add left padding inside the widget.",
},
{
id: "paddingRight",
label: "Padding Right",
currentValue: String(settings.display.paddingRight ?? 0),
values: ["0", "1", "2", "3", "4", CUSTOM_OPTION],
description: "Add right padding inside the widget.",
},
];
}
export function buildDisplayResetItems(settings: Settings): SettingItem[] {
return [
{
id: "resetTimePosition",
label: "Reset Timer",
currentValue: settings.display.resetTimePosition,
values: ["off", "front", "back", "integrated"],
description: "Where to show the reset timer in each window.",
},
{
id: "resetTimeFormat",
label: "Reset Timer Format",
currentValue: settings.display.resetTimeFormat ?? "relative",
values: ["relative", "datetime"] as ResetTimeFormat[],
description: "Show relative countdown or reset datetime.",
},
{
id: "resetTimeContainment",
label: "Reset Timer Containment",
currentValue: settings.display.resetTimeContainment ?? "()",
values: ["none", "blank", "()", "[]", "<>", CUSTOM_OPTION] as ResetTimerContainment[],
description: "Wrapping characters for the reset timer (custom supported).",
},
];
}
export function resolveUsageColorTargets(targets?: UsageColorTargets): UsageColorTargets {
return {
title: targets?.title ?? true,
timer: targets?.timer ?? true,
bar: targets?.bar ?? true,
usageLabel: targets?.usageLabel ?? true,
status: targets?.status ?? true,
};
}
export function formatUsageColorTargetsSummary(targets?: UsageColorTargets): string {
const resolved = resolveUsageColorTargets(targets);
const enabled = [
resolved.title ? "Title" : null,
resolved.timer ? "Timer" : null,
resolved.bar ? "Bar" : null,
resolved.usageLabel ? "Usage label" : null,
resolved.status ? "Status" : null,
].filter(Boolean) as string[];
if (enabled.length === 0) return "off";
if (enabled.length === 5) return "all";
return enabled.join(", ");
}
export function buildUsageColorTargetItems(settings: Settings): SettingItem[] {
const targets = resolveUsageColorTargets(settings.display.usageColorTargets);
return [
{
id: "usageColorTitle",
label: "Title",
currentValue: targets.title ? "on" : "off",
values: ["on", "off"],
description: "Color the window title by usage.",
},
{
id: "usageColorTimer",
label: "Timer",
currentValue: targets.timer ? "on" : "off",
values: ["on", "off"],
description: "Color the reset timer by usage.",
},
{
id: "usageColorBar",
label: "Bar",
currentValue: targets.bar ? "on" : "off",
values: ["on", "off"],
description: "Color the usage bar by usage.",
},
{
id: "usageColorLabel",
label: "Usage label",
currentValue: targets.usageLabel ? "on" : "off",
values: ["on", "off"],
description: "Color the percentage text by usage.",
},
{
id: "usageColorStatus",
label: "Status",
currentValue: targets.status ? "on" : "off",
values: ["on", "off"],
description: "Color the status indicator by status.",
},
];
}
export function buildDisplayColorItems(settings: Settings): SettingItem[] {
return [
{
id: "baseTextColor",
label: "Base Color",
currentValue: normalizeBaseTextColor(settings.display.baseTextColor),
values: [...BASE_COLOR_OPTIONS] as BaseTextColor[],
description: "Base color for neutral labels and dividers.",
},
{
id: "backgroundColor",
label: "Background Color",
currentValue: normalizeBaseTextColor(settings.display.backgroundColor),
values: [...BASE_COLOR_OPTIONS] as BaseTextColor[],
description: "Background color for the widget line.",
},
{
id: "colorScheme",
label: "Color Indicator Scheme",
currentValue: settings.display.colorScheme,
values: [
"base-warning-error",
"success-base-warning-error",
"monochrome",
] as ColorScheme[],
description: "Choose how usage/status indicators are color-coded.",
},
{
id: "usageColorTargets",
label: "Color Indicator Targets",
currentValue: formatUsageColorTargetsSummary(settings.display.usageColorTargets),
description: "Pick which elements use the indicator colors.",
},
{
id: "errorThreshold",
label: "Error Threshold (%)",
currentValue: String(settings.display.errorThreshold),
values: ["10", "15", "20", "25", "30", "35", "40", CUSTOM_OPTION],
description: "Percent remaining below which usage is red.",
},
{
id: "warningThreshold",
label: "Warning Threshold (%)",
currentValue: String(settings.display.warningThreshold),
values: ["30", "40", "50", "60", "70", CUSTOM_OPTION],
description: "Percent remaining below which usage is yellow.",
},
{
id: "successThreshold",
label: "Success Threshold (%)",
currentValue: String(settings.display.successThreshold),
values: ["60", "70", "75", "80", "90", CUSTOM_OPTION],
description: "Percent remaining above which usage is green.",
},
];
}
export function buildDisplayBarItems(settings: Settings): SettingItem[] {
const items: SettingItem[] = [
{
id: "barType",
label: "Bar Type",
currentValue: settings.display.barType,
values: [
"horizontal-bar",
"horizontal-single",
"vertical",
"braille",
"shade",
] as BarType[],
description: "Choose the bar glyph style for usage.",
},
];
if (settings.display.barType === "horizontal-bar") {
items.push({
id: "barCharacter",
label: "H. Bar Character",
currentValue: settings.display.barCharacter,
values: ["light", "heavy", "double", "block", CUSTOM_OPTION],
description: "Custom bar character(s), set 1 or 2 (fill/empty)",
});
}
items.push(
{
id: "barWidth",
label: "Bar Width",
currentValue: String(settings.display.barWidth),
values: ["1", "4", "6", "8", "10", "12", "fill", CUSTOM_OPTION],
description: "Set the bar width or fill available space.",
},
{
id: "containBar",
label: "Contain Bar",
currentValue: settings.display.containBar ? "on" : "off",
values: ["on", "off"],
description: "Wrap the bar with ▕ and ▏ caps.",
},
);
if (settings.display.barType === "braille") {
items.push(
{
id: "brailleFillEmpty",
label: "Braille Empty Fill",
currentValue: settings.display.brailleFillEmpty ? "on" : "off",
values: ["on", "off"],
description: "Fill empty braille cells with dim blocks.",
},
{
id: "brailleFullBlocks",
label: "Braille Full Blocks",
currentValue: settings.display.brailleFullBlocks ? "on" : "off",
values: ["on", "off"],
description: "Use full 8-dot braille blocks for filled segments.",
},
);
}
items.push({
id: "barStyle",
label: "Bar Style",
currentValue: settings.display.barStyle,
values: ["bar", "percentage", "both"] as BarStyle[],
description: "Show bar, percentage, or both.",
});
return items;
}
export function buildDisplayProviderItems(settings: Settings): SettingItem[] {
return [
{
id: "showProviderName",
label: "Show Provider Name",
currentValue: settings.display.showProviderName ? "on" : "off",
values: ["on", "off"],
description: "Toggle the provider name prefix.",
},
{
id: "providerLabel",
label: "Provider Label",
currentValue: settings.display.providerLabel,
values: ["none", "plan", "subscription", "sub", CUSTOM_OPTION] as (ProviderLabel | typeof CUSTOM_OPTION)[],
description: "Suffix appended after the provider name.",
},
{
id: "providerLabelColon",
label: "Provider Label Colon",
currentValue: settings.display.providerLabelColon ? "on" : "off",
values: ["on", "off"],
description: "Show a colon after the provider label.",
},
{
id: "providerLabelBold",
label: "Show in Bold",
currentValue: settings.display.providerLabelBold ? "on" : "off",
values: ["on", "off"],
description: "Bold the provider name and colon.",
},
{
id: "showUsageLabels",
label: "Show Usage Labels",
currentValue: settings.display.showUsageLabels ? "on" : "off",
values: ["on", "off"],
description: "Show “used/rem.” labels after percentages.",
},
{
id: "showWindowTitle",
label: "Show Title",
currentValue: settings.display.showWindowTitle ? "on" : "off",
values: ["on", "off"],
description: "Show window titles like 5h, Week, etc.",
},
{
id: "boldWindowTitle",
label: "Bold Title",
currentValue: settings.display.boldWindowTitle ? "on" : "off",
values: ["on", "off"],
description: "Bold window titles like 5h, Week, etc.",
},
];
}
const STATUS_ICON_PACK_PREVIEW = {
minimal: "minimal (✓ ⚠ × ?)",
emoji: "emoji (✅ ⚠️ 🔴 ❓)",
faces: "faces (😎 😳 😵 🤔)",
} as const;
const STATUS_ICON_FACES_PRESET = "😎😳😵🤔";
const STATUS_ICON_CUSTOM_FALLBACK = ["✓", "⚠", "×", "?"];
const STATUS_ICON_CUSTOM_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
function resolveCustomStatusIcons(value?: string): [string, string, string, string] {
if (!value) return STATUS_ICON_CUSTOM_FALLBACK as [string, string, string, string];
const segments = Array.from(STATUS_ICON_CUSTOM_SEGMENTER.segment(value), (entry) => entry.segment)
.map((segment) => segment.trim())
.filter(Boolean);
if (segments.length < 3) return STATUS_ICON_CUSTOM_FALLBACK as [string, string, string, string];
if (segments.length === 3) {
return [segments[0], segments[1], segments[2], STATUS_ICON_CUSTOM_FALLBACK[3]] as [string, string, string, string];
}
return [segments[0], segments[1], segments[2], segments[3]] as [string, string, string, string];
}
function formatCustomStatusIcons(value?: string): string {
return resolveCustomStatusIcons(value).join(" ");
}
function formatStatusIconPack(pack: Exclude<StatusIconPack, "custom">): string {
return STATUS_ICON_PACK_PREVIEW[pack] ?? pack;
}
function parseStatusIconPack(value: string): StatusIconPack {
if (value.startsWith("minimal")) return "minimal";
if (value.startsWith("emoji")) return "emoji";
return "emoji";
}
export function buildDisplayStatusItems(settings: Settings): SettingItem[] {
const rawMode = settings.display.statusIndicatorMode ?? "icon";
const mode: StatusIndicatorMode = rawMode === "text" || rawMode === "icon+text" || rawMode === "icon"
? rawMode
: "icon";
const items: SettingItem[] = [
{
id: "statusIndicatorMode",
label: "Status Mode",
currentValue: mode,
values: ["icon", "text", "icon+text"] as StatusIndicatorMode[],
description: "Use icons, text, or both for status indicators.",
},
];
if (mode === "icon" || mode === "icon+text") {
const pack = settings.display.statusIconPack ?? "emoji";
const customIcons = settings.display.statusIconCustom;
items.push({
id: "statusIconPack",
label: "Status Icon Pack",
currentValue: pack === "custom" ? formatCustomStatusIcons(customIcons) : formatStatusIconPack(pack),
values: [
formatStatusIconPack("minimal"),
formatStatusIconPack("emoji"),
STATUS_ICON_PACK_PREVIEW.faces,
CUSTOM_OPTION,
],
description: "Pick the icon set used for status indicators. Choose custom to edit icons (OK/warn/error/unknown).",
});
}
items.push(
{
id: "statusDismissOk",
label: "Dismiss Operational Status",
currentValue: settings.display.statusDismissOk ? "on" : "off",
values: ["on", "off"],
description: "Hide status indicators when there are no incidents.",
}
);
return items;
}
export function buildDisplayDividerItems(settings: Settings): SettingItem[] {
return [
{
id: "dividerCharacter",
label: "Divider Character",
currentValue: settings.display.dividerCharacter,
values: ["none", "blank", "|", "│", "┃", "┆", "┇", "║", "•", "●", "○", "◇", CUSTOM_OPTION] as DividerCharacter[],
description: "Choose the divider glyph between windows.",
},
{
id: "dividerColor",
label: "Divider Color",
currentValue: normalizeDividerColor(settings.display.dividerColor ?? "borderMuted"),
values: [...DIVIDER_COLOR_OPTIONS] as DividerColor[],
description: "Color used for divider glyphs and lines.",
},
{
id: "statusProviderDivider",
label: "Status/Provider Divider",
currentValue: settings.display.statusProviderDivider ? "on" : "off",
values: ["on", "off"],
description: "Add a divider between status and provider label.",
},
{
id: "dividerBlanks",
label: "Blanks Before/After Divider",
currentValue: String(settings.display.dividerBlanks),
values: ["0", "1", "2", "3", "fill", CUSTOM_OPTION],
description: "Padding around the divider character.",
},
{
id: "showProviderDivider",
label: "Show Provider Divider",
currentValue: settings.display.showProviderDivider ? "on" : "off",
values: ["on", "off"],
description: "Show the divider after the provider label.",
},
{
id: "showTopDivider",
label: "Show Top Divider",
currentValue: settings.display.showTopDivider ? "on" : "off",
values: ["on", "off"],
description: "Show a divider line above the widget.",
},
{
id: "showBottomDivider",
label: "Show Bottom Divider",
currentValue: settings.display.showBottomDivider ? "on" : "off",
values: ["on", "off"],
description: "Show a divider line below the widget.",
},
{
id: "dividerFooterJoin",
label: "Connect Dividers",
currentValue: settings.display.dividerFooterJoin ? "on" : "off",
values: ["on", "off"],
description: "Draw reverse-T connectors for top/bottom dividers.",
},
];
}
function clampNumber(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
function parseClampedNumber(value: string, min: number, max: number): number | null {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed)) return null;
return clampNumber(parsed, min, max);
}
export function applyDisplayChange(settings: Settings, id: string, value: string): Settings {
switch (id) {
case "alignment":
settings.display.alignment = value as DisplayAlignment;
break;
case "barType":
settings.display.barType = value as BarType;
break;
case "barStyle":
settings.display.barStyle = value as BarStyle;
break;
case "barWidth": {
if (value === "fill") {
settings.display.barWidth = "fill" as BarWidth;
break;
}
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.barWidth = parsed;
}
break;
}
case "containBar":
settings.display.containBar = value === "on";
break;
case "barCharacter":
settings.display.barCharacter = value as BarCharacter;
break;
case "brailleFillEmpty":
settings.display.brailleFillEmpty = value === "on";
break;
case "brailleFullBlocks":
settings.display.brailleFullBlocks = value === "on";
break;
case "colorScheme":
settings.display.colorScheme = value as ColorScheme;
break;
case "usageColorTitle":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
title: value === "on",
};
break;
case "usageColorTimer":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
timer: value === "on",
};
break;
case "usageColorBar":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
bar: value === "on",
};
break;
case "usageColorLabel":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
usageLabel: value === "on",
};
break;
case "usageColorStatus":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
status: value === "on",
};
break;
case "usageColorTargets":
settings.display.usageColorTargets = resolveUsageColorTargets(settings.display.usageColorTargets);
break;
case "resetTimePosition":
settings.display.resetTimePosition = value as "off" | "front" | "back" | "integrated";
break;
case "resetTimeFormat":
settings.display.resetTimeFormat = value as ResetTimeFormat;
break;
case "resetTimeContainment":
if (value === CUSTOM_OPTION) {
break;
}
settings.display.resetTimeContainment = value as ResetTimerContainment;
break;
case "statusIndicatorMode":
settings.display.statusIndicatorMode = value as StatusIndicatorMode;
break;
case "statusIconPack":
if (value === CUSTOM_OPTION) {
settings.display.statusIconPack = "custom";
break;
}
if (value.startsWith("minimal") || value.startsWith("emoji")) {
settings.display.statusIconPack = parseStatusIconPack(value);
break;
}
if (value.startsWith("faces")) {
settings.display.statusIconCustom = STATUS_ICON_FACES_PRESET;
settings.display.statusIconPack = "custom";
break;
}
settings.display.statusIconCustom = value;
settings.display.statusIconPack = "custom";
break;
case "statusIconCustom":
settings.display.statusIconCustom = value;
settings.display.statusIconPack = "custom";
break;
case "statusProviderDivider":
settings.display.statusProviderDivider = value === "on";
break;
case "statusDismissOk":
settings.display.statusDismissOk = value === "on";
break;
case "showProviderName":
settings.display.showProviderName = value === "on";
break;
case "providerLabel":
settings.display.providerLabel = value as ProviderLabel;
break;
case "providerLabelColon":
settings.display.providerLabelColon = value === "on";
break;
case "providerLabelBold":
settings.display.providerLabelBold = value === "on";
break;
case "baseTextColor":
settings.display.baseTextColor = normalizeBaseTextColor(value);
break;
case "backgroundColor":
settings.display.backgroundColor = normalizeBaseTextColor(value);
break;
case "showUsageLabels":
settings.display.showUsageLabels = value === "on";
break;
case "showWindowTitle":
settings.display.showWindowTitle = value === "on";
break;
case "boldWindowTitle":
settings.display.boldWindowTitle = value === "on";
break;
case "showContextBar":
settings.display.showContextBar = value === "on";
break;
case "paddingLeft": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.paddingLeft = parsed;
}
break;
}
case "paddingRight": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.paddingRight = parsed;
}
break;
}
case "dividerCharacter":
settings.display.dividerCharacter = value as DividerCharacter;
break;
case "dividerColor":
settings.display.dividerColor = normalizeDividerColor(value);
break;
case "dividerBlanks": {
if (value === "fill") {
settings.display.dividerBlanks = "fill" as DividerBlanks;
break;
}
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.dividerBlanks = parsed;
}
break;
}
case "showProviderDivider":
settings.display.showProviderDivider = value === "on";
break;
case "dividerFooterJoin":
settings.display.dividerFooterJoin = value === "on";
break;
case "showTopDivider":
settings.display.showTopDivider = value === "on";
break;
case "showBottomDivider":
settings.display.showBottomDivider = value === "on";
break;
case "overflow":
settings.display.overflow = value as WidgetWrapping;
break;
case "widgetWrapping":
settings.display.overflow = value as WidgetWrapping;
break;
case "errorThreshold": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.errorThreshold = parsed;
}
break;
}
case "warningThreshold": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.warningThreshold = parsed;
}
break;
}
case "successThreshold": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.successThreshold = parsed;
}
break;
}
}
return settings;
}
@@ -1,183 +0,0 @@
/**
* Settings menu item builders.
*/
import type { SelectItem } from "@mariozechner/pi-tui";
import type { CoreProviderSettingsMap } from "../../shared.js";
import type { Settings } from "../settings-types.js";
import type { ProviderName } from "../types.js";
import { PROVIDERS, PROVIDER_DISPLAY_NAMES } from "../providers/metadata.js";
export type TooltipSelectItem = SelectItem & { tooltip?: string };
export function buildMainMenuItems(settings: Settings, pinnedProvider?: ProviderName | null): TooltipSelectItem[] {
const pinnedLabel = pinnedProvider ? PROVIDER_DISPLAY_NAMES[pinnedProvider] : "auto (current provider)";
const kb = settings.keybindings;
const kbDesc = `cycle: ${kb.cycleProvider}, reset: ${kb.toggleResetFormat}`;
return [
{
value: "display-theme",
label: "Themes",
description: "save, manage, share",
tooltip: "Save, load, and share display themes.",
},
{
value: "display",
label: "Adv. Display Settings",
description: "layout, bars, colors",
tooltip: "Adjust layout, colors, bar styling, status indicators, and dividers.",
},
{
value: "providers",
label: "Provider Settings",
description: "provider specific settings",
tooltip: "Configure provider display toggles and window visibility.",
},
{
value: "pin-provider",
label: "Provider Shown",
description: pinnedLabel,
tooltip: "Select which provider is shown in the widget.",
},
{
value: "keybindings",
label: "Keybindings",
description: kbDesc,
tooltip: "Configure keyboard shortcuts. Changes take effect after pi restart.",
},
{
value: "open-core-settings",
label: "Additional settings",
description: "in /sub-core:settings",
tooltip: "Open /sub-core:settings for refresh behavior and provider enablement.",
},
];
}
export function buildProviderListItems(settings: Settings, coreProviders?: CoreProviderSettingsMap): TooltipSelectItem[] {
const orderedProviders = settings.providerOrder.length > 0 ? settings.providerOrder : PROVIDERS;
const items: TooltipSelectItem[] = orderedProviders.map((provider) => {
const ps = settings.providers[provider];
const core = coreProviders?.[provider];
const enabledValue = core
? core.enabled === "auto"
? "auto"
: core.enabled === true || core.enabled === "on"
? "on"
: "off"
: "auto";
const status = ps.showStatus ? "status on" : "status off";
return {
value: `provider-${provider}`,
label: PROVIDER_DISPLAY_NAMES[provider],
description: `enabled ${enabledValue}, ${status}`,
tooltip: `Configure ${PROVIDER_DISPLAY_NAMES[provider]} display settings.`,
};
});
items.push({
value: "reset-providers",
label: "Reset Provider Defaults",
description: "restore provider settings",
tooltip: "Restore provider display settings to their defaults.",
});
return items;
}
export function buildDisplayMenuItems(): TooltipSelectItem[] {
return [
{
value: "display-layout",
label: "Layout & Structure",
description: "alignment, wrapping, padding",
tooltip: "Control alignment, wrapping, and padding.",
},
{
value: "display-bar",
label: "Bars",
description: "style, width, character",
tooltip: "Customize bar type, width, and bar styling.",
},
{
value: "display-provider",
label: "Labels & Text",
description: "labels, titles, usage text",
tooltip: "Adjust provider label visibility and text styling.",
},
{
value: "display-reset",
label: "Reset Timer",
description: "position, format, wrapping",
tooltip: "Control reset timer placement and formatting.",
},
{
value: "display-status",
label: "Status",
description: "mode, icons, text",
tooltip: "Configure status mode and icon packs.",
},
{
value: "display-divider",
label: "Dividers",
description: "character, blanks, status divider, lines",
tooltip: "Change divider character, spacing, status separator, and divider lines.",
},
{
value: "display-color",
label: "Colors",
description: "base, scheme, thresholds",
tooltip: "Tune base colors, color scheme, and thresholds.",
},
];
}
export function buildDisplayThemeMenuItems(): TooltipSelectItem[] {
return [
{
value: "display-theme-save",
label: "Save Theme",
description: "store current theme",
tooltip: "Save the current display theme with a custom name.",
},
{
value: "display-theme-load",
label: "Load & Manage themes",
description: "load, share, rename and delete themes",
tooltip: "Load, share, delete, rename, and restore saved themes.",
},
{
value: "display-theme-share",
label: "Share Theme",
description: "share current theme",
tooltip: "Post a share string for the current theme.",
},
{
value: "display-theme-import",
label: "Import theme",
description: "from share string",
tooltip: "Import a shared theme string.",
},
{
value: "display-theme-random",
label: "Random theme",
description: "generate a new theme",
tooltip: "Generate a random display theme as inspiration or a starting point.",
},
{
value: "display-theme-restore",
label: "Restore previous state",
description: "restore your last theme",
tooltip: "Restore your previous display theme.",
},
];
}
export function buildProviderSettingsItems(settings: Settings): TooltipSelectItem[] {
return buildProviderListItems(settings);
}
export function getProviderFromCategory(category: string): ProviderName | null {
const match = category.match(/^provider-(\w+)$/);
return match ? (match[1] as ProviderName) : null;
}
@@ -1,349 +0,0 @@
import type { Settings } from "../settings-types.js";
import type { TooltipSelectItem } from "./menu.js";
type DisplaySettings = Settings["display"];
type BarType = DisplaySettings["barType"];
type BarStyle = DisplaySettings["barStyle"];
type BarCharacter = DisplaySettings["barCharacter"];
type BarWidth = DisplaySettings["barWidth"];
type DividerCharacter = DisplaySettings["dividerCharacter"];
type DividerBlanks = DisplaySettings["dividerBlanks"];
type DisplayAlignment = DisplaySettings["alignment"];
type OverflowMode = DisplaySettings["overflow"];
type BaseTextColor = DisplaySettings["baseTextColor"];
type DividerColor = DisplaySettings["dividerColor"];
type ResetTimeFormat = DisplaySettings["resetTimeFormat"];
type ResetTimerContainment = DisplaySettings["resetTimeContainment"];
type StatusIndicatorMode = DisplaySettings["statusIndicatorMode"];
type StatusIconPack = DisplaySettings["statusIconPack"];
type ProviderLabel = DisplaySettings["providerLabel"];
const RANDOM_BAR_TYPES: BarType[] = ["horizontal-bar", "horizontal-single", "vertical", "braille", "shade"];
const RANDOM_BAR_STYLES: BarStyle[] = ["bar", "percentage", "both"];
const RANDOM_BAR_WIDTHS: BarWidth[] = [1, 4, 6, 8, 10, 12, "fill"];
const RANDOM_BAR_CHARACTERS: BarCharacter[] = [
"light",
"heavy",
"double",
"block",
"▮▯",
"■□",
"●○",
"▲△",
"◆◇",
"🚀_",
];
const RANDOM_ALIGNMENTS: DisplayAlignment[] = ["left", "center", "right", "split"];
const RANDOM_OVERFLOW: OverflowMode[] = ["truncate", "wrap"];
const RANDOM_RESET_POSITIONS: DisplaySettings["resetTimePosition"][] = ["off", "front", "back", "integrated"];
const RANDOM_RESET_FORMATS: ResetTimeFormat[] = ["relative", "datetime"];
const RANDOM_RESET_CONTAINMENTS: ResetTimerContainment[] = ["none", "blank", "()", "[]", "<>"];
const RANDOM_STATUS_MODES: StatusIndicatorMode[] = ["icon", "text", "icon+text"];
const RANDOM_STATUS_PACKS: StatusIconPack[] = ["minimal", "emoji"];
const RANDOM_PROVIDER_LABELS: ProviderLabel[] = ["plan", "subscription", "sub", "none"];
const RANDOM_DIVIDER_CHARACTERS: DividerCharacter[] = ["none", "blank", "|", "│", "┃", "┆", "┇", "║", "•", "●", "○", "◇"];
const RANDOM_DIVIDER_BLANKS: DividerBlanks[] = [0, 1, 2, 3];
const RANDOM_COLOR_SCHEMES: DisplaySettings["colorScheme"][] = [
"base-warning-error",
"success-base-warning-error",
"monochrome",
];
const RANDOM_BASE_TEXT_COLORS: BaseTextColor[] = ["dim", "muted", "text", "primary", "success", "warning", "error", "border", "borderMuted"];
const RANDOM_BACKGROUND_COLORS: BaseTextColor[] = [
"text",
"selectedBg",
"userMessageBg",
"customMessageBg",
"toolPendingBg",
"toolSuccessBg",
"toolErrorBg",
];
const RANDOM_DIVIDER_COLORS: DividerColor[] = [
"primary",
"text",
"muted",
"dim",
"success",
"warning",
"error",
"border",
"borderMuted",
"borderAccent",
];
const RANDOM_PADDING: number[] = [0, 1, 2, 3, 4];
function pickRandom<T>(items: readonly T[]): T {
return items[Math.floor(Math.random() * items.length)] ?? items[0]!;
}
function randomBool(probability = 0.5): boolean {
return Math.random() < probability;
}
const THEME_ID_LENGTH = 24;
const THEME_ID_FALLBACK = "theme";
function buildThemeId(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, THEME_ID_LENGTH) || THEME_ID_FALLBACK;
}
export interface DisplayThemeTarget {
id?: string;
name: string;
display: Settings["display"];
deletable: boolean;
}
export function buildDisplayThemeItems(
settings: Settings,
): TooltipSelectItem[] {
const items: TooltipSelectItem[] = [];
items.push({
value: "user",
label: "Restore backup",
description: "restore your last theme",
tooltip: "Restore your previous display theme.",
});
items.push({
value: "default",
label: "Default",
description: "restore default settings",
tooltip: "Reset display settings to defaults.",
});
items.push({
value: "minimal",
label: "Default Minimal",
description: "compact display",
tooltip: "Apply the default minimal theme.",
});
for (const theme of settings.displayThemes) {
const description = theme.source === "imported" ? "manually imported theme" : "manually saved theme";
items.push({
value: `theme:${theme.id}`,
label: theme.name,
description,
tooltip: `Manage ${theme.name}.`,
});
}
return items;
}
export function resolveDisplayThemeTarget(
value: string,
settings: Settings,
defaults: Settings,
fallbackUser: Settings["display"] | null,
): DisplayThemeTarget | null {
if (value === "user") {
const display = settings.displayUserTheme ?? fallbackUser ?? settings.display;
return { name: "Restore backup", display, deletable: false };
}
if (value === "default") {
return { name: "Default", display: { ...defaults.display }, deletable: false };
}
if (value === "minimal") {
return {
name: "Default Minimal",
display: {
...defaults.display,
alignment: "split",
barStyle: "percentage",
barType: "horizontal-bar",
barWidth: 1,
barCharacter: "heavy",
containBar: true,
brailleFillEmpty: false,
brailleFullBlocks: false,
colorScheme: "base-warning-error",
usageColorTargets: {
title: true,
timer: true,
bar: true,
usageLabel: true,
status: true,
},
resetTimePosition: "off",
resetTimeFormat: "relative",
resetTimeContainment: "blank",
statusIndicatorMode: "icon",
statusIconPack: "minimal",
statusProviderDivider: false,
statusDismissOk: true,
showProviderName: false,
providerLabel: "none",
providerLabelColon: false,
providerLabelBold: true,
baseTextColor: "muted",
backgroundColor: "text",
showWindowTitle: false,
boldWindowTitle: true,
showUsageLabels: false,
dividerCharacter: "none",
dividerColor: "dim",
dividerBlanks: 1,
showProviderDivider: true,
dividerFooterJoin: true,
showTopDivider: false,
showBottomDivider: false,
paddingLeft: 1,
paddingRight: 1,
widgetPlacement: "belowEditor",
errorThreshold: 25,
warningThreshold: 50,
overflow: "truncate",
successThreshold: 75,
},
deletable: false,
};
}
if (value.startsWith("theme:")) {
const id = value.replace("theme:", "");
const theme = settings.displayThemes.find((entry) => entry.id === id);
if (!theme) return null;
return { id: theme.id, name: theme.name, display: theme.display, deletable: true };
}
return null;
}
export function buildRandomDisplay(base: DisplaySettings): DisplaySettings {
const display: DisplaySettings = { ...base };
display.alignment = pickRandom(RANDOM_ALIGNMENTS);
display.overflow = pickRandom(RANDOM_OVERFLOW);
const padding = pickRandom(RANDOM_PADDING);
display.paddingLeft = padding;
display.paddingRight = padding;
display.barStyle = pickRandom(RANDOM_BAR_STYLES);
display.barType = pickRandom(RANDOM_BAR_TYPES);
display.barWidth = pickRandom(RANDOM_BAR_WIDTHS);
display.barCharacter = pickRandom(RANDOM_BAR_CHARACTERS);
display.containBar = randomBool();
display.brailleFillEmpty = randomBool();
display.brailleFullBlocks = randomBool();
display.colorScheme = pickRandom(RANDOM_COLOR_SCHEMES);
const usageColorTargets = {
title: randomBool(),
timer: randomBool(),
bar: randomBool(),
usageLabel: randomBool(),
status: randomBool(),
};
if (!usageColorTargets.title && !usageColorTargets.timer && !usageColorTargets.bar && !usageColorTargets.usageLabel && !usageColorTargets.status) {
usageColorTargets.bar = true;
}
display.usageColorTargets = usageColorTargets;
display.resetTimePosition = pickRandom(RANDOM_RESET_POSITIONS);
display.resetTimeFormat = pickRandom(RANDOM_RESET_FORMATS);
display.resetTimeContainment = pickRandom(RANDOM_RESET_CONTAINMENTS);
display.statusIndicatorMode = pickRandom(RANDOM_STATUS_MODES);
display.statusIconPack = pickRandom(RANDOM_STATUS_PACKS);
display.statusProviderDivider = randomBool();
display.statusDismissOk = randomBool();
display.showProviderName = randomBool();
display.providerLabel = pickRandom(RANDOM_PROVIDER_LABELS);
display.providerLabelColon = display.providerLabel !== "none" && randomBool();
display.providerLabelBold = randomBool();
display.baseTextColor = pickRandom(RANDOM_BASE_TEXT_COLORS);
display.backgroundColor = pickRandom(RANDOM_BACKGROUND_COLORS);
display.boldWindowTitle = randomBool();
display.showUsageLabels = randomBool();
display.dividerCharacter = pickRandom(RANDOM_DIVIDER_CHARACTERS);
display.dividerColor = pickRandom(RANDOM_DIVIDER_COLORS);
display.dividerBlanks = pickRandom(RANDOM_DIVIDER_BLANKS);
display.showProviderDivider = randomBool();
display.dividerFooterJoin = randomBool();
display.showTopDivider = randomBool();
display.showBottomDivider = randomBool();
if (display.dividerCharacter === "none") {
display.showProviderDivider = false;
display.dividerFooterJoin = false;
display.showTopDivider = false;
display.showBottomDivider = false;
}
if (display.providerLabel === "none") {
display.providerLabelColon = false;
}
return display;
}
export function buildThemeActionItems(target: DisplayThemeTarget): TooltipSelectItem[] {
const items: TooltipSelectItem[] = [
{
value: "load",
label: "Load",
description: "apply this theme",
tooltip: "Apply the selected theme.",
},
{
value: "share",
label: "Share",
description: "post share string",
tooltip: "Post a shareable theme string to chat.",
},
];
if (target.deletable) {
items.push({
value: "rename",
label: "Rename",
description: "rename saved theme",
tooltip: "Rename this saved theme.",
});
items.push({
value: "delete",
label: "Delete",
description: "remove saved theme",
tooltip: "Remove this theme from saved themes.",
});
}
return items;
}
export function upsertDisplayTheme(
settings: Settings,
name: string,
display: Settings["display"],
source?: "saved" | "imported",
): Settings {
const trimmed = name.trim() || "Theme";
const id = buildThemeId(trimmed);
const snapshot = { ...display };
const existing = settings.displayThemes.find((theme) => theme.id === id);
const resolvedSource = source ?? existing?.source ?? "saved";
if (existing) {
existing.name = trimmed;
existing.display = snapshot;
existing.source = resolvedSource;
} else {
settings.displayThemes.push({ id, name: trimmed, display: snapshot, source: resolvedSource });
}
return settings;
}
export function renameDisplayTheme(settings: Settings, id: string, name: string): Settings {
const trimmed = name.trim() || "Theme";
const nextId = buildThemeId(trimmed);
const existing = settings.displayThemes.find((theme) => theme.id === id);
if (!existing) return settings;
if (nextId === id) {
existing.name = trimmed;
return settings;
}
const collision = settings.displayThemes.find((theme) => theme.id === nextId);
if (collision) {
collision.name = trimmed;
collision.display = existing.display;
collision.source = existing.source;
settings.displayThemes = settings.displayThemes.filter((theme) => theme.id !== id);
return settings;
}
existing.id = nextId;
existing.name = trimmed;
return settings;
}
export function saveDisplayTheme(settings: Settings, name: string): Settings {
return upsertDisplayTheme(settings, name, settings.display, "saved");
}
File diff suppressed because it is too large Load Diff
@@ -1,75 +0,0 @@
/**
* Display theme share helpers.
*/
import type { Settings } from "./settings-types.js";
import { mergeSettings } from "./settings-types.js";
const SHARE_SEPARATOR = ":";
const DISPLAY_SHARE_VERSION = 1;
export interface DisplaySharePayload {
v: number;
display: Settings["display"];
}
export interface DecodedDisplayShare {
name: string;
display: Settings["display"];
version: number;
isNewerVersion: boolean;
hasName: boolean;
}
function encodeDisplaySharePayload(display: Settings["display"]): string {
const payload: DisplaySharePayload = { v: DISPLAY_SHARE_VERSION, display };
return Buffer.from(JSON.stringify(payload)).toString("base64url");
}
export function buildDisplayShareString(name: string, display: Settings["display"]): string {
const encoded = encodeDisplaySharePayload(display);
const trimmedName = name.trim() || "custom";
return `${trimmedName}${SHARE_SEPARATOR}${encoded}`;
}
export function buildDisplayShareStringWithoutName(display: Settings["display"]): string {
return encodeDisplaySharePayload(display);
}
export function decodeDisplayShareString(input: string): DecodedDisplayShare | null {
const trimmed = input.trim();
if (!trimmed) return null;
let name = "custom";
let hasName = false;
let payload = trimmed;
const separatorIndex = trimmed.indexOf(SHARE_SEPARATOR);
if (separatorIndex >= 0) {
const candidateName = trimmed.slice(0, separatorIndex).trim();
payload = trimmed.slice(separatorIndex + 1).trim();
if (candidateName) {
name = candidateName;
hasName = true;
}
}
if (!payload) return null;
try {
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
const parsed = JSON.parse(decoded) as unknown;
if (!parsed || typeof parsed !== "object") return null;
const displayCandidate = (parsed as DisplaySharePayload).display ?? parsed;
if (!displayCandidate || typeof displayCandidate !== "object" || Array.isArray(displayCandidate)) {
return null;
}
const merged = mergeSettings({ display: displayCandidate } as Partial<Settings>).display;
const version = typeof (parsed as DisplaySharePayload).v === "number" ? (parsed as DisplaySharePayload).v : 0;
return {
name,
display: merged,
version,
isNewerVersion: version > DISPLAY_SHARE_VERSION,
hasName,
};
} catch {
return null;
}
}
@@ -1,229 +0,0 @@
/**
* Shared types and metadata for sub-* extensions.
*/
export const PROVIDERS = ["anthropic", "copilot", "gemini", "antigravity", "codex", "kiro", "zai", "opencode-go"] as const;
export type ProviderName = (typeof PROVIDERS)[number];
export type StatusIndicator = "none" | "minor" | "major" | "critical" | "maintenance" | "unknown";
export interface ProviderStatus {
indicator: StatusIndicator;
description?: string;
}
export interface RateWindow {
label: string;
usedPercent: number;
resetDescription?: string;
resetAt?: string;
}
export interface UsageSnapshot {
provider: ProviderName;
displayName: string;
windows: RateWindow[];
extraUsageEnabled?: boolean;
fiveHourUsage?: number;
lastSuccessAt?: number;
error?: UsageError;
status?: ProviderStatus;
requestsSummary?: string;
requestsRemaining?: number;
requestsEntitlement?: number;
}
export type UsageErrorCode =
| "NO_CREDENTIALS"
| "NO_CLI"
| "NOT_LOGGED_IN"
| "FETCH_FAILED"
| "HTTP_ERROR"
| "API_ERROR"
| "TIMEOUT"
| "UNKNOWN";
export interface UsageError {
code: UsageErrorCode;
message: string;
httpStatus?: number;
}
export interface ProviderUsageEntry {
provider: ProviderName;
usage?: UsageSnapshot;
}
export type ProviderEnabledSetting = "auto" | "on" | "off" | boolean;
export interface CoreProviderSettings {
enabled: ProviderEnabledSetting;
displayName?: string;
fetchStatus: boolean;
extraUsageCurrencySymbol?: string;
extraUsageDecimalSeparator?: "." | ",";
}
export interface CoreProviderSettingsMap {
anthropic: CoreProviderSettings;
copilot: CoreProviderSettings;
gemini: CoreProviderSettings;
antigravity: CoreProviderSettings;
codex: CoreProviderSettings;
kiro: CoreProviderSettings;
zai: CoreProviderSettings;
"opencode-go": CoreProviderSettings;
}
export interface BehaviorSettings {
refreshInterval: number;
minRefreshInterval: number;
refreshOnTurnStart: boolean;
refreshOnToolResult: boolean;
}
export const DEFAULT_BEHAVIOR_SETTINGS: BehaviorSettings = {
refreshInterval: 60,
minRefreshInterval: 10,
refreshOnTurnStart: false,
refreshOnToolResult: false,
};
export function getDefaultCoreProviderSettings(): CoreProviderSettingsMap {
const defaults = {} as CoreProviderSettingsMap;
for (const provider of PROVIDERS) {
defaults[provider] = {
enabled: "auto" as ProviderEnabledSetting,
fetchStatus: Boolean(PROVIDER_METADATA[provider]?.status),
};
}
return defaults;
}
export function getDefaultCoreSettings(): CoreSettings {
return {
providers: getDefaultCoreProviderSettings(),
behavior: { ...DEFAULT_BEHAVIOR_SETTINGS },
statusRefresh: { ...DEFAULT_BEHAVIOR_SETTINGS },
providerOrder: [...PROVIDERS],
defaultProvider: null,
};
}
export interface CoreSettings {
providers: CoreProviderSettingsMap;
behavior: BehaviorSettings;
statusRefresh: BehaviorSettings;
providerOrder: ProviderName[];
defaultProvider: ProviderName | null;
}
export type SubCoreState = {
provider?: ProviderName;
usage?: UsageSnapshot;
};
export type SubCoreAllState = {
provider?: ProviderName;
entries: ProviderUsageEntry[];
};
export type SubCoreEvents =
| { type: "sub-core:ready"; state: SubCoreState }
| { type: "sub-core:update-current"; state: SubCoreState }
| { type: "sub-core:update-all"; state: SubCoreAllState };
export interface StatusPageComponentMatch {
id?: string;
name?: string;
}
export type ProviderStatusConfig =
| { type: "statuspage"; url: string; component?: StatusPageComponentMatch }
| { type: "google-workspace" };
export interface ProviderDetectionConfig {
providerTokens: string[];
modelTokens: string[];
}
export interface ProviderMetadata {
displayName: string;
detection?: ProviderDetectionConfig;
status?: ProviderStatusConfig;
}
export const PROVIDER_METADATA: Record<ProviderName, ProviderMetadata> = {
anthropic: {
displayName: "Anthropic (Claude)",
status: { type: "statuspage", url: "https://status.anthropic.com/api/v2/status.json" },
detection: { providerTokens: ["anthropic"], modelTokens: ["claude"] },
},
copilot: {
displayName: "GitHub Copilot",
status: { type: "statuspage", url: "https://www.githubstatus.com/api/v2/status.json" },
detection: { providerTokens: ["copilot", "github"], modelTokens: [] },
},
gemini: {
displayName: "Google Gemini",
status: { type: "google-workspace" },
detection: { providerTokens: ["google", "gemini"], modelTokens: ["gemini"] },
},
antigravity: {
displayName: "Antigravity",
status: { type: "google-workspace" },
detection: { providerTokens: ["antigravity"], modelTokens: ["antigravity"] },
},
codex: {
displayName: "OpenAI Codex",
status: {
type: "statuspage",
url: "https://status.openai.com/api/v2/status.json",
component: {
id: "01JVCV8YSWZFRSM1G5CVP253SK",
name: "Codex",
},
},
detection: { providerTokens: ["openai", "codex"], modelTokens: ["gpt", "o1", "o3"] },
},
kiro: {
displayName: "AWS Kiro",
detection: { providerTokens: ["kiro", "aws"], modelTokens: [] },
},
zai: {
displayName: "z.ai",
detection: { providerTokens: ["zai", "z.ai", "xai"], modelTokens: [] },
},
"opencode-go": {
displayName: "OpenCode (MiniMax)",
detection: { providerTokens: ["opencode-go", "opencode", "minimax"], modelTokens: ["kimi", "minimax", "m2.5"] },
},
};
export const PROVIDER_DISPLAY_NAMES = Object.fromEntries(
PROVIDERS.map((provider) => [provider, PROVIDER_METADATA[provider].displayName])
) as Record<ProviderName, string>;
export const MODEL_MULTIPLIERS: Record<string, number> = {
"Claude Haiku 4.5": 0.33,
"Claude Opus 4.1": 10,
"Claude Opus 4.5": 3,
"Claude Sonnet 4": 1,
"Claude Sonnet 4.5": 1,
"Gemini 2.5 Pro": 1,
"Gemini 3 Flash": 0.33,
"Gemini 3 Pro": 1,
"GPT-4.1": 0,
"GPT-4o": 0,
"GPT-5": 1,
"GPT-5 mini": 0,
"GPT-5-Codex": 1,
"GPT-5.1": 1,
"GPT-5.1-Codex": 1,
"GPT-5.1-Codex-Mini": 0.33,
"GPT-5.1-Codex-Max": 1,
"GPT-5.2": 1,
"Grok Code Fast 1": 0.25,
"Raptor mini": 0,
};
@@ -1,103 +0,0 @@
/**
* Status indicator helpers.
*/
import type { ProviderStatus } from "./types.js";
import type { StatusIconPack } from "./settings-types.js";
const STATUS_ICON_PACKS: Record<Exclude<StatusIconPack, "custom">, Record<ProviderStatus["indicator"], string>> = {
minimal: {
none: "✓",
minor: "⚠",
major: "⚠",
critical: "×",
maintenance: "~",
unknown: "?",
},
emoji: {
none: "✅",
minor: "⚠️",
major: "🟠",
critical: "🔴",
maintenance: "🔧",
unknown: "❓",
},
};
const DEFAULT_CUSTOM_ICONS = ["✓", "⚠", "×", "?"];
const CUSTOM_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
function parseCustomIcons(value?: string): [string, string, string, string] {
if (!value) return DEFAULT_CUSTOM_ICONS as [string, string, string, string];
const segments = Array.from(CUSTOM_SEGMENTER.segment(value), (entry) => entry.segment)
.map((segment) => segment.trim())
.filter(Boolean);
if (segments.length < 3) return DEFAULT_CUSTOM_ICONS as [string, string, string, string];
if (segments.length === 3) {
return [segments[0], segments[1], segments[2], DEFAULT_CUSTOM_ICONS[3]] as [string, string, string, string];
}
return [segments[0], segments[1], segments[2], segments[3]] as [string, string, string, string];
}
function buildCustomPack(custom?: string): Record<ProviderStatus["indicator"], string> {
const [ok, warn, error, unknown] = parseCustomIcons(custom);
return {
none: ok,
minor: warn,
major: error,
critical: error,
maintenance: warn,
unknown,
};
}
export function getStatusIcon(
status: ProviderStatus | undefined,
pack: StatusIconPack,
custom?: string,
): string {
if (!status) return "";
if (pack === "custom") {
return buildCustomPack(custom)[status.indicator] ?? "";
}
return STATUS_ICON_PACKS[pack][status.indicator] ?? "";
}
export function getStatusLabel(
status: ProviderStatus | undefined,
useAbbreviated = false,
): string {
if (!status) return "";
if (useAbbreviated) {
switch (status.indicator) {
case "none":
return "Status OK";
case "minor":
return "Status Degr.";
case "major":
case "critical":
return "Status Crit.";
case "maintenance":
return "Status Maint.";
case "unknown":
default:
return "Status Unk.";
}
}
if (status.description) return status.description;
switch (status.indicator) {
case "none":
return "Operational";
case "minor":
return "Degraded";
case "major":
return "Outage";
case "critical":
return "Outage";
case "maintenance":
return "Maintenance";
case "unknown":
default:
return "Status Unknown";
}
}
@@ -1,61 +0,0 @@
/**
* Storage abstraction for settings persistence.
*/
import * as fs from "node:fs";
import * as path from "node:path";
export interface StorageAdapter {
readFile(path: string): string | undefined;
writeFile(path: string, contents: string): void;
writeFileExclusive(path: string, contents: string): boolean;
exists(path: string): boolean;
removeFile(path: string): void;
ensureDir(path: string): void;
}
export function createFsStorage(): StorageAdapter {
return {
readFile(filePath: string): string | undefined {
try {
return fs.readFileSync(filePath, "utf-8");
} catch {
return undefined;
}
},
writeFile(filePath: string, contents: string): void {
fs.writeFileSync(filePath, contents, "utf-8");
},
writeFileExclusive(filePath: string, contents: string): boolean {
try {
fs.writeFileSync(filePath, contents, { flag: "wx" });
return true;
} catch {
return false;
}
},
exists(filePath: string): boolean {
return fs.existsSync(filePath);
},
removeFile(filePath: string): void {
try {
fs.unlinkSync(filePath);
} catch {
// Ignore remove errors
}
},
ensureDir(dirPath: string): void {
fs.mkdirSync(path.resolve(dirPath), { recursive: true });
},
};
}
let activeStorage: StorageAdapter = createFsStorage();
export function getStorage(): StorageAdapter {
return activeStorage;
}
export function setStorage(storage: StorageAdapter): void {
activeStorage = storage;
}
@@ -1,25 +0,0 @@
/**
* Core types for the sub-bar extension
*/
export type {
ProviderName,
StatusIndicator,
ProviderStatus,
RateWindow,
UsageSnapshot,
UsageError,
UsageErrorCode,
ProviderUsageEntry,
SubCoreState,
SubCoreAllState,
SubCoreEvents,
} from "./shared.js";
export { PROVIDERS } from "./shared.js";
export type ModelInfo = {
provider?: string;
id?: string;
scopedModelPatterns?: string[];
};
@@ -1,304 +0,0 @@
import type { Component, SettingItem, SettingsListTheme } from "@mariozechner/pi-tui";
import {
Input,
fuzzyFilter,
getEditorKeybindings,
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from "@mariozechner/pi-tui";
export interface SettingsListOptions {
enableSearch?: boolean;
}
export const CUSTOM_OPTION = "__custom__";
export const CUSTOM_LABEL = "custom";
export type { SettingItem, SettingsListTheme };
export class SettingsList implements Component {
private items: SettingItem[];
private filteredItems: SettingItem[];
private theme: SettingsListTheme;
private selectedIndex = 0;
private maxVisible: number;
private onChange: (id: string, newValue: string) => void;
private onCancel: () => void;
private searchInput?: Input;
private searchEnabled: boolean;
private submenuComponent: Component | null = null;
private submenuItemIndex: number | null = null;
constructor(
items: SettingItem[],
maxVisible: number,
theme: SettingsListTheme,
onChange: (id: string, newValue: string) => void,
onCancel: () => void,
options: SettingsListOptions = {},
) {
this.items = items;
this.filteredItems = items;
this.maxVisible = maxVisible;
this.theme = theme;
this.onChange = onChange;
this.onCancel = onCancel;
this.searchEnabled = options.enableSearch ?? false;
if (this.searchEnabled) {
this.searchInput = new Input();
}
}
/** Update an item's currentValue */
updateValue(id: string, newValue: string): void {
const item = this.items.find((i) => i.id === id);
if (item) {
item.currentValue = newValue;
}
}
getSelectedId(): string | null {
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
const item = displayItems[this.selectedIndex];
return item?.id ?? null;
}
setSelectedId(id: string): void {
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
const index = displayItems.findIndex((item) => item.id === id);
if (index >= 0) {
this.selectedIndex = index;
}
}
invalidate(): void {
this.submenuComponent?.invalidate?.();
}
render(width: number): string[] {
// If submenu is active, render it instead
if (this.submenuComponent) {
return this.submenuComponent.render(width);
}
return this.renderMainList(width);
}
private renderMainList(width: number): string[] {
const lines: string[] = [];
if (this.searchEnabled && this.searchInput) {
lines.push(...this.searchInput.render(width));
lines.push("");
}
if (this.items.length === 0) {
lines.push(this.theme.hint(" No settings available"));
if (this.searchEnabled) {
this.addHintLine(lines);
}
return lines;
}
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
if (displayItems.length === 0) {
lines.push(this.theme.hint(" No matching settings"));
this.addHintLine(lines);
return lines;
}
// Calculate visible range with scrolling
const startIndex = Math.max(
0,
Math.min(
this.selectedIndex - Math.floor(this.maxVisible / 2),
displayItems.length - this.maxVisible,
),
);
const endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);
// Calculate max label width for alignment
const maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));
// Render visible items
for (let i = startIndex; i < endIndex; i++) {
const item = displayItems[i];
if (!item) continue;
const isSelected = i === this.selectedIndex;
const prefix = isSelected ? this.theme.cursor : " ";
const prefixWidth = visibleWidth(prefix);
// Pad label to align values
const labelPadded = item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
const labelText = this.theme.label(labelPadded, isSelected);
// Calculate space for value
const separator = " ";
const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);
const valueMaxWidth = Math.max(1, width - usedWidth - 2);
const optionLines = isSelected && item.values && item.values.length > 0
? wrapTextWithAnsi(this.formatOptionsInline(item, item.values), valueMaxWidth)
: null;
const valueText = optionLines
? optionLines[0] ?? ""
: this.theme.value(truncateToWidth(item.currentValue, valueMaxWidth, ""), isSelected);
const line = prefix + labelText + separator + valueText;
lines.push(truncateToWidth(line, width, ""));
if (optionLines && optionLines.length > 1) {
const indent = " ".repeat(prefixWidth + maxLabelWidth + visibleWidth(separator));
for (const continuation of optionLines.slice(1)) {
lines.push(truncateToWidth(indent + continuation, width, ""));
}
}
}
// Add scroll indicator if needed
if (startIndex > 0 || endIndex < displayItems.length) {
const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;
lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, "")));
}
// Add description for selected item
const selectedItem = displayItems[this.selectedIndex];
if (selectedItem?.description) {
lines.push("");
const wrapWidth = Math.max(1, width - 4);
const wrappedDesc = wrapTextWithAnsi(selectedItem.description, wrapWidth);
for (const line of wrappedDesc) {
const prefixed = ` ${line}`;
lines.push(this.theme.description(truncateToWidth(prefixed, width, "")));
}
}
// Add hint
this.addHintLine(lines);
return lines;
}
handleInput(data: string): void {
// If submenu is active, delegate all input to it
// The submenu's onCancel (triggered by escape) will call done() which closes it
if (this.submenuComponent) {
this.submenuComponent.handleInput?.(data);
return;
}
const kb = getEditorKeybindings();
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
if (kb.matches(data, "selectUp")) {
if (displayItems.length === 0) return;
this.selectedIndex = this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;
} else if (kb.matches(data, "selectDown")) {
if (displayItems.length === 0) return;
this.selectedIndex = this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;
} else if (kb.matches(data, "cursorLeft")) {
this.stepValue(-1);
} else if (kb.matches(data, "cursorRight")) {
this.stepValue(1);
} else if (kb.matches(data, "selectConfirm") || data === " ") {
this.activateItem();
} else if (kb.matches(data, "selectCancel")) {
this.onCancel();
} else if (this.searchEnabled && this.searchInput) {
const sanitized = data.replace(/ /g, "");
if (!sanitized) {
return;
}
this.searchInput.handleInput(sanitized);
this.applyFilter(this.searchInput.getValue());
}
}
private stepValue(direction: -1 | 1): void {
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
const item = displayItems[this.selectedIndex];
if (!item || !item.values || item.values.length === 0) return;
const values = item.values;
let currentIndex = values.indexOf(item.currentValue);
if (currentIndex === -1) {
currentIndex = direction > 0 ? 0 : values.length - 1;
}
const nextIndex = (currentIndex + direction + values.length) % values.length;
const newValue = values[nextIndex];
if (newValue === CUSTOM_OPTION) {
item.currentValue = newValue;
this.onChange(item.id, newValue);
return;
}
item.currentValue = newValue;
this.onChange(item.id, newValue);
}
private activateItem(): void {
const item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];
if (!item) return;
const hasCustom = Boolean(item.values && item.values.includes(CUSTOM_OPTION));
const currentIsCustom = hasCustom && item.values && !item.values.includes(item.currentValue);
if (item.submenu && hasCustom) {
if (currentIsCustom || item.currentValue === CUSTOM_OPTION) {
this.openSubmenu(item);
}
return;
}
if (item.submenu) {
this.openSubmenu(item);
}
}
private closeSubmenu(): void {
this.submenuComponent = null;
// Restore selection to the item that opened the submenu
if (this.submenuItemIndex !== null) {
this.selectedIndex = this.submenuItemIndex;
this.submenuItemIndex = null;
}
}
private applyFilter(query: string): void {
this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
this.selectedIndex = 0;
}
private formatOptionsInline(item: SettingItem, values: string[]): string {
const separator = this.theme.description(" • ");
const hasCustom = values.includes(CUSTOM_OPTION);
const currentIsCustom = hasCustom && !values.includes(item.currentValue);
return values
.map((value) => {
const label = value === CUSTOM_OPTION
? (currentIsCustom ? `${CUSTOM_LABEL} (${item.currentValue})` : CUSTOM_LABEL)
: value;
const selected = value === item.currentValue || (currentIsCustom && value === CUSTOM_OPTION);
return this.theme.value(label, selected);
})
.join(separator);
}
private openSubmenu(item: SettingItem): void {
if (!item.submenu) return;
this.submenuItemIndex = this.selectedIndex;
this.submenuComponent = item.submenu(item.currentValue, (selectedValue) => {
if (selectedValue !== undefined) {
item.currentValue = selectedValue;
this.onChange(item.id, selectedValue);
}
this.closeSubmenu();
});
}
private addHintLine(lines: string[]): void {
lines.push("");
lines.push(
this.theme.hint(
this.searchEnabled
? " Type to search · ←/→ change · Enter/Space edit custom · Esc to cancel"
: " ←/→ change · Enter/Space edit custom · Esc to cancel",
),
);
}
}
@@ -1,5 +0,0 @@
/**
* Usage data types shared across modules.
*/
export type { ProviderUsageEntry } from "../../shared.js";
@@ -1,42 +0,0 @@
/**
* Utility functions for the sub-bar display layer.
*/
import { MODEL_MULTIPLIERS } from "./shared.js";
export function normalizeTokens(value: string): string[] {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
.trim()
.split(" ")
.filter(Boolean);
}
const MODEL_MULTIPLIER_TOKENS = Object.entries(MODEL_MULTIPLIERS).map(([label, multiplier]) => ({
label,
multiplier,
tokens: normalizeTokens(label),
}));
/**
* Get the request multiplier for a model ID
* Uses fuzzy matching against known model names
*/
export function getModelMultiplier(modelId: string | undefined): number | undefined {
if (!modelId) return undefined;
const modelTokens = normalizeTokens(modelId);
if (modelTokens.length === 0) return undefined;
let bestMatch: { multiplier: number; tokenCount: number } | undefined;
for (const entry of MODEL_MULTIPLIER_TOKENS) {
const isMatch = entry.tokens.every((token) => modelTokens.includes(token));
if (!isMatch) continue;
const tokenCount = entry.tokens.length;
if (!bestMatch || tokenCount > bestMatch.tokenCount) {
bestMatch = { multiplier: entry.multiplier, tokenCount };
}
}
return bestMatch?.multiplier;
}
@@ -1,535 +0,0 @@
/**
* sub-core - Shared usage data core for sub-* extensions.
*/
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import * as fs from "node:fs";
import type { Dependencies, ProviderName, SubCoreState, UsageSnapshot } from "./src/types.js";
import { getDefaultSettings, type Settings } from "./src/settings-types.js";
import type { ProviderUsageEntry } from "./src/usage/types.js";
import { createDefaultDependencies } from "./src/dependencies.js";
import { createUsageController, type UsageUpdate } from "./src/usage/controller.js";
import { fetchUsageEntries, getCachedUsageEntries } from "./src/usage/fetch.js";
import { onCacheSnapshot, onCacheUpdate, watchCacheUpdates, type Cache } from "./src/cache.js";
import { isExpectedMissingData } from "./src/errors.js";
import { getStorage } from "./src/storage.js";
import { clearSettingsCache, loadSettings, saveSettings, SETTINGS_PATH } from "./src/settings.js";
import { showSettingsUI } from "./src/settings-ui.js";
type SubCoreRequest =
| {
type?: "current";
includeSettings?: boolean;
reply: (payload: { state: SubCoreState; settings?: Settings }) => void;
}
| {
type: "entries";
force?: boolean;
reply: (payload: { entries: ProviderUsageEntry[] }) => void;
};
type SubCoreAction = {
type: "refresh" | "cycleProvider";
force?: boolean;
};
const TOOL_NAMES = {
usage: ["sub_get_usage", "get_current_usage"],
allUsage: ["sub_get_all_usage", "get_all_usage"],
} as const;
type ToolName = (typeof TOOL_NAMES)[keyof typeof TOOL_NAMES][number];
type SubCoreGlobalState = { active: boolean };
const subCoreGlobal = globalThis as typeof globalThis & { __piSubCore?: SubCoreGlobalState };
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
const result = { ...target } as T;
for (const key of Object.keys(source) as (keyof T)[]) {
const sourceValue = source[key];
const targetValue = result[key];
if (
sourceValue !== undefined &&
typeof sourceValue === "object" &&
sourceValue !== null &&
!Array.isArray(sourceValue) &&
typeof targetValue === "object" &&
targetValue !== null &&
!Array.isArray(targetValue)
) {
result[key] = deepMerge(targetValue as object, sourceValue as object) as T[keyof T];
} else if (sourceValue !== undefined) {
result[key] = sourceValue as T[keyof T];
}
}
return result;
}
function stripUsageProvider(usage?: UsageSnapshot): Omit<UsageSnapshot, "provider"> | undefined {
if (!usage) return undefined;
const { provider: _provider, ...rest } = usage;
return rest;
}
/**
* Create the extension
*/
export default function createExtension(pi: ExtensionAPI, deps: Dependencies = createDefaultDependencies()): void {
if (subCoreGlobal.__piSubCore?.active) {
return;
}
subCoreGlobal.__piSubCore = { active: true };
let usageRefreshInterval: ReturnType<typeof setInterval> | undefined;
let statusRefreshInterval: ReturnType<typeof setInterval> | undefined;
let lastContext: ExtensionContext | undefined;
let lastUsageRefreshAt = 0;
let lastStatusRefreshAt = 0;
let settings: Settings = getDefaultSettings();
let settingsLoaded = false;
let toolsRegistered = false;
let lastState: SubCoreState = {};
let settingsSnapshot = "";
let settingsMtimeMs = 0;
let settingsDebounce: NodeJS.Timeout | undefined;
let settingsWatcher: fs.FSWatcher | undefined;
let settingsPoll: NodeJS.Timeout | undefined;
let settingsWatchStarted = false;
const controller = createUsageController(deps);
const controllerState = {
currentProvider: undefined as ProviderName | undefined,
cachedUsage: undefined as UsageSnapshot | undefined,
providerCycleIndex: 0,
};
let lastAllSnapshot = "";
let lastCurrentSnapshot = "";
const emitCurrentUpdate = (provider?: ProviderName, usage?: UsageSnapshot): void => {
lastState = { provider, usage };
const payload = JSON.stringify(lastState);
if (payload === lastCurrentSnapshot) return;
lastCurrentSnapshot = payload;
pi.events.emit("sub-core:update-current", { state: lastState });
};
const unsubscribeCacheSnapshot = onCacheSnapshot((cache: Cache) => {
const ttlMs = settings.behavior.refreshInterval * 1000;
const now = Date.now();
const entries: ProviderUsageEntry[] = [];
for (const provider of settings.providerOrder) {
const entry = cache[provider];
if (!entry || !entry.usage) continue;
if (now - entry.fetchedAt >= ttlMs) continue;
const usage = { ...entry.usage, status: entry.status };
if (usage.error && isExpectedMissingData(usage.error)) continue;
entries.push({ provider, usage });
}
const payload = JSON.stringify({ provider: controllerState.currentProvider, entries });
if (payload === lastAllSnapshot) return;
lastAllSnapshot = payload;
pi.events.emit("sub-core:update-all", {
state: { provider: controllerState.currentProvider, entries },
});
});
const unsubscribeCache = onCacheUpdate((provider, entry) => {
if (!controllerState.currentProvider || provider !== controllerState.currentProvider) return;
const usage = entry?.usage ? { ...entry.usage, status: entry.status } : undefined;
controllerState.cachedUsage = usage;
emitCurrentUpdate(controllerState.currentProvider, usage);
});
let stopCacheWatch: (() => void) | undefined;
let cacheWatchStarted = false;
const startCacheWatch = (): void => {
if (cacheWatchStarted) return;
cacheWatchStarted = true;
stopCacheWatch = watchCacheUpdates();
};
function emitUpdate(update: UsageUpdate): void {
emitCurrentUpdate(update.provider, update.usage);
}
async function refresh(
ctx: ExtensionContext,
options?: { force?: boolean; allowStaleCache?: boolean; skipFetch?: boolean }
) {
lastContext = ctx;
ensureSettingsLoaded();
try {
await controller.refresh(ctx, settings, controllerState, emitUpdate, options);
} finally {
if (!options?.skipFetch) {
lastUsageRefreshAt = Date.now();
}
}
}
async function refreshStatus(
ctx: ExtensionContext,
options?: { force?: boolean; allowStaleCache?: boolean; skipFetch?: boolean }
) {
lastContext = ctx;
ensureSettingsLoaded();
try {
await controller.refreshStatus(ctx, settings, controllerState, emitUpdate, options);
} finally {
if (!options?.skipFetch) {
lastStatusRefreshAt = Date.now();
}
}
}
async function cycleProvider(ctx: ExtensionContext): Promise<void> {
ensureSettingsLoaded();
await controller.cycleProvider(ctx, settings, controllerState, emitUpdate);
}
function setupRefreshInterval(): void {
if (usageRefreshInterval) {
clearInterval(usageRefreshInterval);
usageRefreshInterval = undefined;
}
if (statusRefreshInterval) {
clearInterval(statusRefreshInterval);
statusRefreshInterval = undefined;
}
const usageIntervalMs = settings.behavior.refreshInterval * 1000;
if (usageIntervalMs > 0) {
const usageTickMs = Math.min(usageIntervalMs, 10000);
usageRefreshInterval = setInterval(() => {
if (!lastContext) return;
const elapsed = lastUsageRefreshAt ? Date.now() - lastUsageRefreshAt : usageIntervalMs + 1;
if (elapsed >= usageIntervalMs) {
void refresh(lastContext);
}
}, usageTickMs);
usageRefreshInterval.unref?.();
}
const statusIntervalMs = settings.statusRefresh.refreshInterval * 1000;
if (statusIntervalMs > 0) {
const statusTickMs = Math.min(statusIntervalMs, 10000);
statusRefreshInterval = setInterval(() => {
if (!lastContext) return;
const elapsed = lastStatusRefreshAt ? Date.now() - lastStatusRefreshAt : statusIntervalMs + 1;
if (elapsed >= statusIntervalMs) {
void refreshStatus(lastContext);
}
}, statusTickMs);
statusRefreshInterval.unref?.();
}
}
function applySettingsPatch(patch: Partial<Settings>): void {
ensureSettingsLoaded();
settings = deepMerge(settings, patch);
saveSettings(settings);
setupRefreshInterval();
pi.events.emit("sub-core:settings:updated", { settings });
}
function readSettingsFile(): string | undefined {
try {
return fs.readFileSync(SETTINGS_PATH, "utf-8");
} catch {
return undefined;
}
}
function applySettingsFromDisk(): void {
clearSettingsCache();
settings = loadSettings();
registerToolsFromSettings(settings);
setupRefreshInterval();
pi.events.emit("sub-core:settings:updated", { settings });
if (lastContext) {
void refresh(lastContext, { allowStaleCache: true, skipFetch: true });
void refreshStatus(lastContext, { allowStaleCache: true, skipFetch: true });
}
}
function refreshSettingsSnapshot(): void {
const content = readSettingsFile();
if (!content || content === settingsSnapshot) return;
try {
JSON.parse(content);
} catch {
return;
}
settingsSnapshot = content;
applySettingsFromDisk();
}
function checkSettingsFile(): void {
try {
const stat = fs.statSync(SETTINGS_PATH, { throwIfNoEntry: false });
if (!stat || !stat.mtimeMs) return;
if (stat.mtimeMs === settingsMtimeMs) return;
settingsMtimeMs = stat.mtimeMs;
refreshSettingsSnapshot();
} catch {
// Ignore missing files
}
}
function scheduleSettingsRefresh(): void {
if (settingsDebounce) clearTimeout(settingsDebounce);
settingsDebounce = setTimeout(() => checkSettingsFile(), 200);
}
function startSettingsWatch(): void {
if (settingsWatchStarted) return;
settingsWatchStarted = true;
if (!settingsSnapshot) {
const content = readSettingsFile();
if (content) {
settingsSnapshot = content;
try {
const stat = fs.statSync(SETTINGS_PATH, { throwIfNoEntry: false });
if (stat?.mtimeMs) settingsMtimeMs = stat.mtimeMs;
} catch {
// Ignore
}
}
}
try {
settingsWatcher = fs.watch(SETTINGS_PATH, scheduleSettingsRefresh);
settingsWatcher.unref?.();
} catch {
settingsWatcher = undefined;
}
settingsPoll = setInterval(() => checkSettingsFile(), 2000);
settingsPoll.unref?.();
}
async function getEntries(force?: boolean): Promise<ProviderUsageEntry[]> {
ensureSettingsLoaded();
const enabledProviders = controller.getEnabledProviders(settings);
if (enabledProviders.length === 0) return [];
if (force) {
return fetchUsageEntries(deps, settings, enabledProviders, { force: true });
}
return getCachedUsageEntries(enabledProviders, settings);
}
const registerUsageTool = (name: ToolName): void => {
pi.registerTool({
name,
label: "Sub Usage",
description: "Refresh and return the latest subscription usage snapshot.",
parameters: Type.Object({
force: Type.Optional(Type.Boolean({ description: "Force refresh" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { force } = params as { force?: boolean };
await refresh(ctx, { force: force ?? true });
const payload = { provider: lastState.provider, usage: stripUsageProvider(lastState.usage) };
return {
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
details: payload,
};
},
});
};
const registerAllUsageTool = (name: ToolName): void => {
pi.registerTool({
name,
label: "Sub All Usage",
description: "Refresh and return usage snapshots for all enabled providers.",
parameters: Type.Object({
force: Type.Optional(Type.Boolean({ description: "Force refresh" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const { force } = params as { force?: boolean };
const entries = await getEntries(force ?? true);
const payload = entries.map((entry) => ({
provider: entry.provider,
usage: stripUsageProvider(entry.usage),
}));
return {
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
details: { entries: payload },
};
},
});
};
function registerToolsFromSettings(nextSettings: Settings): void {
if (toolsRegistered) return;
const usageToolEnabled = nextSettings.tools?.usageTool ?? false;
const allUsageToolEnabled = nextSettings.tools?.allUsageTool ?? false;
if (usageToolEnabled) {
for (const name of TOOL_NAMES.usage) {
registerUsageTool(name);
}
}
if (allUsageToolEnabled) {
for (const name of TOOL_NAMES.allUsage) {
registerAllUsageTool(name);
}
}
toolsRegistered = true;
}
function ensureSettingsLoaded(): void {
if (settingsLoaded) return;
settings = loadSettings();
settingsLoaded = true;
registerToolsFromSettings(settings);
setupRefreshInterval();
const watchTimer = setTimeout(() => {
startCacheWatch();
startSettingsWatch();
}, 0);
watchTimer.unref?.();
}
pi.registerCommand("sub-core:settings", {
description: "Open sub-core settings",
handler: async (_args, ctx) => {
ensureSettingsLoaded();
const handleSettingsChange = async (updatedSettings: Settings) => {
applySettingsPatch(updatedSettings);
if (lastContext) {
await refresh(lastContext);
}
};
const newSettings = await showSettingsUI(ctx, handleSettingsChange);
settings = newSettings;
applySettingsPatch(newSettings);
if (lastContext) {
await refresh(lastContext);
}
},
});
pi.events.on("sub-core:request", async (payload) => {
ensureSettingsLoaded();
const request = payload as SubCoreRequest;
if (request.type === "entries") {
const entries = await getEntries(request.force);
if (lastContext && settings.statusRefresh.refreshInterval > 0) {
await refreshStatus(lastContext, { force: request.force });
}
request.reply({ entries });
return;
}
request.reply({
state: lastState,
settings: request.includeSettings ? settings : undefined,
});
});
pi.events.on("sub-core:settings:patch", (payload) => {
const patch = (payload as { patch?: Partial<Settings> }).patch;
if (!patch) return;
applySettingsPatch(patch);
if (lastContext) {
void refresh(lastContext);
}
});
pi.events.on("sub-core:action", (payload) => {
const action = payload as SubCoreAction;
if (!lastContext) return;
switch (action.type) {
case "refresh":
void refresh(lastContext, { force: action.force });
break;
case "cycleProvider":
void cycleProvider(lastContext);
break;
}
});
pi.on("session_start", async (_event, ctx) => {
lastContext = ctx;
ensureSettingsLoaded();
void refresh(ctx, { allowStaleCache: true, skipFetch: true });
void refreshStatus(ctx, { allowStaleCache: true, skipFetch: true });
pi.events.emit("sub-core:ready", { state: lastState, settings });
});
pi.on("turn_start", async (_event, ctx) => {
if (settings.behavior.refreshOnTurnStart) {
await refresh(ctx);
}
if (settings.statusRefresh.refreshOnTurnStart) {
await refreshStatus(ctx);
}
});
pi.on("tool_result", async (_event, ctx) => {
if (settings.behavior.refreshOnToolResult) {
await refresh(ctx, { force: true });
}
if (settings.statusRefresh.refreshOnToolResult) {
await refreshStatus(ctx, { force: true });
}
});
pi.on("turn_end", async (_event, ctx) => {
await refresh(ctx, { force: true });
});
pi.on("session_switch", async (_event, ctx) => {
controllerState.currentProvider = undefined;
controllerState.cachedUsage = undefined;
await refresh(ctx);
await refreshStatus(ctx);
});
pi.on("session_branch" as unknown as "session_start", async (_event: unknown, ctx: ExtensionContext) => {
controllerState.currentProvider = undefined;
controllerState.cachedUsage = undefined;
await refresh(ctx);
await refreshStatus(ctx);
});
pi.on("model_select" as unknown as "session_start", async (_event: unknown, ctx: ExtensionContext) => {
controllerState.currentProvider = undefined;
controllerState.cachedUsage = undefined;
void refresh(ctx, { force: true, allowStaleCache: true });
void refreshStatus(ctx, { force: true, allowStaleCache: true });
});
pi.on("session_shutdown", async () => {
if (usageRefreshInterval) {
clearInterval(usageRefreshInterval);
usageRefreshInterval = undefined;
}
if (statusRefreshInterval) {
clearInterval(statusRefreshInterval);
statusRefreshInterval = undefined;
}
if (settingsDebounce) {
clearTimeout(settingsDebounce);
settingsDebounce = undefined;
}
if (settingsPoll) {
clearInterval(settingsPoll);
settingsPoll = undefined;
}
settingsWatcher?.close();
settingsWatcher = undefined;
settingsWatchStarted = false;
settingsSnapshot = "";
settingsMtimeMs = 0;
unsubscribeCache();
unsubscribeCacheSnapshot();
stopCacheWatch?.();
stopCacheWatch = undefined;
cacheWatchStarted = false;
lastContext = undefined;
subCoreGlobal.__piSubCore = undefined;
});
}
@@ -1,35 +0,0 @@
{
"name": "@marckrenn/pi-sub-core",
"version": "1.3.0",
"description": "Shared usage data core for pi extensions",
"keywords": [
"pi-package"
],
"type": "module",
"license": "MIT",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"pi": {
"extensions": [
"./index.ts"
]
},
"scripts": {
"check": "tsc --noEmit",
"check:watch": "tsc --noEmit --watch",
"test": "tsx test/all.test.ts"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.19.2",
"typescript": "^5.8.0"
},
"dependencies": {
"@marckrenn/pi-sub-shared": "^1.3.0"
},
"peerDependencies": {
"@mariozechner/pi-coding-agent": "*"
}
}
@@ -1,489 +0,0 @@
/**
* Cache management for sub-bar
* Shared cache across all pi instances to avoid redundant API calls
*/
import * as path from "node:path";
import * as fs from "node:fs";
import type { ProviderName, ProviderStatus, UsageSnapshot } from "./types.js";
import { isExpectedMissingData } from "./errors.js";
import { getStorage } from "./storage.js";
import {
getCachePath,
getCacheLockPath,
getLegacyAgentCacheLockPath,
getLegacyAgentCachePath,
getLegacyCacheLockPath,
getLegacyCachePath,
} from "./paths.js";
import { tryAcquireFileLock, releaseFileLock, waitForLockRelease } from "./storage/lock.js";
/**
* Cache entry for a provider
*/
export interface CacheEntry {
fetchedAt: number;
statusFetchedAt?: number;
usage?: UsageSnapshot;
status?: ProviderStatus;
}
/**
* Cache structure
*/
export interface Cache {
[provider: string]: CacheEntry;
}
export type CacheUpdateListener = (provider: ProviderName, entry?: CacheEntry) => void;
export type CacheSnapshotListener = (cache: Cache) => void;
const cacheUpdateListeners = new Set<CacheUpdateListener>();
const cacheSnapshotListeners = new Set<CacheSnapshotListener>();
let lastCacheSnapshot: Cache | null = null;
let lastCacheContent = "";
let lastCacheMtimeMs = 0;
let legacyCacheMigrated = false;
function updateCacheSnapshot(cache: Cache, content: string, mtimeMs: number): void {
lastCacheSnapshot = cache;
lastCacheContent = content;
lastCacheMtimeMs = mtimeMs;
}
function resetCacheSnapshot(): void {
lastCacheSnapshot = {};
lastCacheContent = "";
lastCacheMtimeMs = 0;
}
function migrateLegacyCache(): void {
if (legacyCacheMigrated) return;
legacyCacheMigrated = true;
const storage = getStorage();
try {
const legacyCachePaths = [LEGACY_AGENT_CACHE_PATH, LEGACY_CACHE_PATH];
if (!storage.exists(CACHE_PATH)) {
const legacyPath = legacyCachePaths.find((path) => storage.exists(path));
if (legacyPath) {
const content = storage.readFile(legacyPath);
if (content) {
ensureCacheDir();
storage.writeFile(CACHE_PATH, content);
}
}
}
for (const legacyPath of legacyCachePaths) {
if (storage.exists(legacyPath)) {
storage.removeFile(legacyPath);
}
}
for (const legacyLockPath of [LEGACY_AGENT_LOCK_PATH, LEGACY_LOCK_PATH]) {
if (storage.exists(legacyLockPath)) {
storage.removeFile(legacyLockPath);
}
}
} catch (error) {
console.error("Failed to migrate cache:", error);
}
}
export function onCacheUpdate(listener: CacheUpdateListener): () => void {
cacheUpdateListeners.add(listener);
return () => {
cacheUpdateListeners.delete(listener);
};
}
export function onCacheSnapshot(listener: CacheSnapshotListener): () => void {
cacheSnapshotListeners.add(listener);
return () => {
cacheSnapshotListeners.delete(listener);
};
}
function emitCacheUpdate(provider: ProviderName, entry?: CacheEntry): void {
for (const listener of cacheUpdateListeners) {
try {
listener(provider, entry);
} catch (error) {
console.error("Failed to notify cache update:", error);
}
}
}
function emitCacheSnapshot(cache: Cache): void {
for (const listener of cacheSnapshotListeners) {
try {
listener(cache);
} catch (error) {
console.error("Failed to notify cache snapshot:", error);
}
}
}
/**
* Cache file path
*/
export const CACHE_PATH = getCachePath();
const LEGACY_CACHE_PATH = getLegacyCachePath();
const LEGACY_AGENT_CACHE_PATH = getLegacyAgentCachePath();
/**
* Lock file path
*/
const LOCK_PATH = getCacheLockPath();
const LEGACY_LOCK_PATH = getLegacyCacheLockPath();
const LEGACY_AGENT_LOCK_PATH = getLegacyAgentCacheLockPath();
/**
* Lock timeout in milliseconds
*/
const LOCK_TIMEOUT_MS = 5000;
/**
* Ensure cache directory exists
*/
function ensureCacheDir(): void {
const storage = getStorage();
const dir = path.dirname(CACHE_PATH);
storage.ensureDir(dir);
}
/**
* Read cache from disk
*/
export function readCache(): Cache {
migrateLegacyCache();
const storage = getStorage();
try {
const cacheExists = storage.exists(CACHE_PATH);
if (!cacheExists) {
if (lastCacheMtimeMs !== 0 || lastCacheContent) {
resetCacheSnapshot();
}
return lastCacheSnapshot ?? {};
}
const stat = fs.statSync(CACHE_PATH, { throwIfNoEntry: false });
if (stat && stat.mtimeMs === lastCacheMtimeMs && lastCacheSnapshot) {
return lastCacheSnapshot;
}
const content = storage.readFile(CACHE_PATH);
if (!content) {
updateCacheSnapshot({}, "", stat?.mtimeMs ?? 0);
return {};
}
if (!stat && content === lastCacheContent && lastCacheSnapshot) {
return lastCacheSnapshot;
}
try {
const parsed = JSON.parse(content) as Cache;
updateCacheSnapshot(parsed, content, stat?.mtimeMs ?? Date.now());
return parsed;
} catch (error) {
const lastBrace = content.lastIndexOf("}");
if (lastBrace > 0) {
const trimmed = content.slice(0, lastBrace + 1);
try {
const parsed = JSON.parse(trimmed) as Cache;
if (stat) {
writeCache(parsed);
} else {
updateCacheSnapshot(parsed, trimmed, Date.now());
}
return parsed;
} catch {
// fall through to log below
}
}
console.error("Failed to read cache:", error);
}
} catch (error) {
console.error("Failed to read cache:", error);
}
return {};
}
/**
* Write cache to disk
*/
function writeCache(cache: Cache): void {
migrateLegacyCache();
const storage = getStorage();
try {
ensureCacheDir();
const content = JSON.stringify(cache, null, 2);
const cacheExists = storage.exists(CACHE_PATH);
if (cacheExists && content === lastCacheContent) {
const stat = fs.statSync(CACHE_PATH, { throwIfNoEntry: false });
updateCacheSnapshot(cache, content, stat?.mtimeMs ?? lastCacheMtimeMs);
return;
}
const tempPath = `${CACHE_PATH}.${process.pid}.tmp`;
fs.writeFileSync(tempPath, content, "utf-8");
fs.renameSync(tempPath, CACHE_PATH);
const stat = fs.statSync(CACHE_PATH, { throwIfNoEntry: false });
updateCacheSnapshot(cache, content, stat?.mtimeMs ?? Date.now());
} catch (error) {
console.error("Failed to write cache:", error);
}
}
export interface CacheWatchOptions {
debounceMs?: number;
pollIntervalMs?: number;
lockRetryMs?: number;
}
export function watchCacheUpdates(options?: CacheWatchOptions): () => void {
migrateLegacyCache();
const debounceMs = options?.debounceMs ?? 250;
const pollIntervalMs = options?.pollIntervalMs ?? 5000;
const lockRetryMs = options?.lockRetryMs ?? 1000;
let debounceTimer: NodeJS.Timeout | undefined;
let pollTimer: NodeJS.Timeout | undefined;
let lockRetryPending = false;
let lastSnapshot = "";
let lastMtimeMs = 0;
let stopped = false;
const scheduleLockRetry = () => {
if (lockRetryPending || stopped) return;
lockRetryPending = true;
void waitForLockRelease(LOCK_PATH, lockRetryMs).then((released) => {
lockRetryPending = false;
if (released) {
emitFromCache();
}
});
};
const emitFromCache = () => {
try {
if (fs.existsSync(LOCK_PATH)) {
scheduleLockRetry();
return;
}
const stat = fs.statSync(CACHE_PATH, { throwIfNoEntry: false });
if (!stat || !stat.mtimeMs) return;
if (stat.mtimeMs === lastMtimeMs) return;
lastMtimeMs = stat.mtimeMs;
const content = fs.readFileSync(CACHE_PATH, "utf-8");
if (content === lastSnapshot) return;
lastSnapshot = content;
const cache = JSON.parse(content) as Cache;
updateCacheSnapshot(cache, content, stat.mtimeMs);
emitCacheSnapshot(cache);
for (const [provider, entry] of Object.entries(cache)) {
emitCacheUpdate(provider as ProviderName, entry);
}
} catch {
// Ignore parse or read errors (likely mid-write)
}
};
const scheduleEmit = () => {
if (stopped) return;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => emitFromCache(), debounceMs);
};
let watcher: fs.FSWatcher | undefined;
try {
watcher = fs.watch(CACHE_PATH, scheduleEmit);
watcher.unref?.();
} catch {
watcher = undefined;
}
pollTimer = setInterval(() => emitFromCache(), pollIntervalMs);
pollTimer.unref?.();
return () => {
stopped = true;
if (debounceTimer) clearTimeout(debounceTimer);
if (pollTimer) clearInterval(pollTimer);
watcher?.close();
};
}
/**
* Wait for lock to be released and re-check cache
* Returns the cache entry if it became fresh while waiting
*/
async function waitForLockAndRecheck(
provider: ProviderName,
ttlMs: number,
maxWaitMs: number = 3000
): Promise<CacheEntry | null> {
const released = await waitForLockRelease(LOCK_PATH, maxWaitMs);
if (!released) {
return null;
}
const cache = readCache();
const entry = cache[provider];
if (entry && entry.usage?.error && !isExpectedMissingData(entry.usage.error)) {
return null;
}
if (entry && Date.now() - entry.fetchedAt < ttlMs) {
return entry;
}
return null;
}
/**
* Get cached data for a provider if fresh, or null if stale/missing
*/
export async function getCachedData(
provider: ProviderName,
ttlMs: number,
cacheSnapshot?: Cache
): Promise<CacheEntry | null> {
const cache = cacheSnapshot ?? readCache();
const entry = cache[provider];
if (!entry) {
return null;
}
if (entry.usage?.error && !isExpectedMissingData(entry.usage.error)) {
return null;
}
const age = Date.now() - entry.fetchedAt;
if (age < ttlMs) {
return entry;
}
return null;
}
/**
* Fetch data with lock coordination
* Returns cached data if fresh, or executes fetchFn if cache is stale
*/
export async function fetchWithCache<T extends { usage?: UsageSnapshot; status?: ProviderStatus; statusFetchedAt?: number }>(
provider: ProviderName,
ttlMs: number,
fetchFn: () => Promise<T>,
options?: { force?: boolean }
): Promise<T> {
const forceRefresh = options?.force === true;
if (!forceRefresh) {
// Check cache first
const cached = await getCachedData(provider, ttlMs);
if (cached) {
return { usage: cached.usage, status: cached.status } as T;
}
}
// Cache is stale or forced refresh, try to acquire lock
const lockAcquired = tryAcquireFileLock(LOCK_PATH, LOCK_TIMEOUT_MS);
if (!lockAcquired) {
// Another process is fetching, wait and re-check cache
const freshEntry = await waitForLockAndRecheck(provider, ttlMs);
if (freshEntry) {
return { usage: freshEntry.usage, status: freshEntry.status } as T;
}
// Timeout or cache still stale, fetch anyway
}
try {
// Fetch fresh data
const result = await fetchFn();
// Only cache if we got valid usage data (not just no-credentials/errors)
const hasCredentialError = result.usage?.error && isExpectedMissingData(result.usage.error);
const hasError = Boolean(result.usage?.error);
const shouldCache = result.usage && !hasCredentialError && !hasError;
const cache = readCache();
if (shouldCache) {
// Update cache with valid data
const fetchedAt = Date.now();
const previous = cache[provider];
const statusFetchedAt = result.statusFetchedAt ?? (result.status ? fetchedAt : previous?.statusFetchedAt);
cache[provider] = {
fetchedAt,
statusFetchedAt,
usage: result.usage,
status: result.status,
};
writeCache(cache);
emitCacheUpdate(provider, cache[provider]);
emitCacheSnapshot(cache);
} else if (hasCredentialError) {
// Remove from cache if no credentials
if (cache[provider]) {
delete cache[provider];
writeCache(cache);
emitCacheUpdate(provider, undefined);
emitCacheSnapshot(cache);
}
}
return result;
} finally {
if (lockAcquired) {
releaseFileLock(LOCK_PATH);
}
}
}
export async function updateCacheStatus(
provider: ProviderName,
status: ProviderStatus,
options?: { statusFetchedAt?: number }
): Promise<void> {
const lockAcquired = tryAcquireFileLock(LOCK_PATH, LOCK_TIMEOUT_MS);
if (!lockAcquired) {
await waitForLockRelease(LOCK_PATH, 3000);
}
try {
const cache = readCache();
const entry = cache[provider];
const statusFetchedAt = options?.statusFetchedAt ?? Date.now();
cache[provider] = {
fetchedAt: entry?.fetchedAt ?? 0,
statusFetchedAt,
usage: entry?.usage,
status,
};
writeCache(cache);
emitCacheUpdate(provider, cache[provider]);
emitCacheSnapshot(cache);
} finally {
if (lockAcquired) {
releaseFileLock(LOCK_PATH);
}
}
}
/**
* Clear cache for a specific provider or all providers
*/
export function clearCache(provider?: ProviderName): void {
const storage = getStorage();
if (provider) {
const cache = readCache();
delete cache[provider];
writeCache(cache);
} else {
try {
if (storage.exists(CACHE_PATH)) {
storage.removeFile(CACHE_PATH);
}
resetCacheSnapshot();
} catch (error) {
console.error("Failed to clear cache:", error);
}
}
}
@@ -1,35 +0,0 @@
/**
* Configuration constants for the sub-bar extension
*/
/**
* Google Workspace status API endpoint
*/
export const GOOGLE_STATUS_URL = "https://www.google.com/appsstatus/dashboard/incidents.json";
/**
* Google product ID for Gemini in the status API
*/
export const GEMINI_PRODUCT_ID = "npdyhgECDJ6tB66MxXyo";
/**
* Model multipliers for Copilot request counting
* Maps model display names to their request multiplier
*/
export { MODEL_MULTIPLIERS } from "../../src/shared.js";
/**
* Timeout for API requests in milliseconds
*/
export const API_TIMEOUT_MS = 5000;
/**
* Timeout for CLI commands in milliseconds
*/
export const CLI_TIMEOUT_MS = 10000;
/**
* Interval for automatic usage refresh in milliseconds
*/
export const REFRESH_INTERVAL_MS = 60_000;
@@ -1,37 +0,0 @@
/**
* Default dependencies using real implementations
*/
import * as fs from "node:fs";
import * as os from "node:os";
import { execFileSync } from "node:child_process";
import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
import type { Dependencies } from "./types.js";
/**
* Create default dependencies using Node.js APIs
*/
export function createDefaultDependencies(): Dependencies {
return {
fetch: globalThis.fetch,
readFile: (path: string) => {
try {
return fs.readFileSync(path, "utf-8");
} catch {
return undefined;
}
},
fileExists: (path: string) => {
try {
return fs.existsSync(path);
} catch {
return false;
}
},
execFileSync: (file: string, args: string[], options?: ExecFileSyncOptionsWithStringEncoding) => {
return execFileSync(file, args, options) as string;
},
homedir: () => os.homedir(),
env: process.env,
};
}
@@ -1,71 +0,0 @@
/**
* Error utilities for the sub-bar extension
*/
import type { UsageError, UsageErrorCode } from "./types.js";
export function createError(code: UsageErrorCode, message: string, httpStatus?: number): UsageError {
return { code, message, httpStatus };
}
export function noCredentials(): UsageError {
return createError("NO_CREDENTIALS", "No credentials found");
}
export function noCli(cliName: string): UsageError {
return createError("NO_CLI", `${cliName} CLI not found`);
}
export function notLoggedIn(): UsageError {
return createError("NOT_LOGGED_IN", "Not logged in");
}
export function fetchFailed(reason?: string): UsageError {
return createError("FETCH_FAILED", reason ?? "Fetch failed");
}
export function httpError(status: number): UsageError {
return createError("HTTP_ERROR", `HTTP ${status}`, status);
}
export function apiError(message: string): UsageError {
return createError("API_ERROR", message);
}
export function timeout(): UsageError {
return createError("TIMEOUT", "Request timed out");
}
/**
* Check if an error should be considered "no data available" vs actual error
* These are expected states when provider isn't configured
*/
export function isExpectedMissingData(error: UsageError): boolean {
const ignoreCodes = new Set<UsageErrorCode>(["NO_CREDENTIALS", "NO_CLI", "NOT_LOGGED_IN"]);
return ignoreCodes.has(error.code);
}
/**
* Format error for display in the usage widget
*/
export function formatErrorForDisplay(error: UsageError): string {
switch (error.code) {
case "NO_CREDENTIALS":
return "No creds";
case "NO_CLI":
return "No CLI";
case "NOT_LOGGED_IN":
return "Not logged in";
case "HTTP_ERROR":
if (error.httpStatus === 401) {
return "token no longer valid please /login again";
}
return `${error.httpStatus}`;
case "FETCH_FAILED":
case "API_ERROR":
case "TIMEOUT":
case "UNKNOWN":
default:
return "Fetch failed";
}
}
@@ -1,55 +0,0 @@
/**
* Shared path helpers for sub-core storage.
*/
import { getAgentDir } from "@mariozechner/pi-coding-agent";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const SETTINGS_FILE_NAME = "pi-sub-core-settings.json";
const CACHE_DIR_NAME = "cache";
const CACHE_NAMESPACE_DIR = "sub-core";
const CACHE_FILE_NAME = "cache.json";
const CACHE_LOCK_FILE_NAME = "cache.lock";
const LEGACY_AGENT_CACHE_FILE_NAME = "pi-sub-core-cache.json";
const LEGACY_AGENT_LOCK_FILE_NAME = "pi-sub-core-cache.lock";
export function getExtensionDir(): string {
return join(dirname(fileURLToPath(import.meta.url)), "..");
}
export function getCacheDir(): string {
return join(getAgentDir(), CACHE_DIR_NAME, CACHE_NAMESPACE_DIR);
}
export function getCachePath(): string {
return join(getCacheDir(), CACHE_FILE_NAME);
}
export function getCacheLockPath(): string {
return join(getCacheDir(), CACHE_LOCK_FILE_NAME);
}
export function getLegacyCachePath(): string {
return join(getExtensionDir(), "cache.json");
}
export function getLegacyCacheLockPath(): string {
return join(getExtensionDir(), "cache.lock");
}
export function getLegacyAgentCachePath(): string {
return join(getAgentDir(), LEGACY_AGENT_CACHE_FILE_NAME);
}
export function getLegacyAgentCacheLockPath(): string {
return join(getAgentDir(), LEGACY_AGENT_LOCK_FILE_NAME);
}
export function getSettingsPath(): string {
return join(getAgentDir(), SETTINGS_FILE_NAME);
}
export function getLegacySettingsPath(): string {
return join(getExtensionDir(), "settings.json");
}
@@ -1,66 +0,0 @@
/**
* Provider interface and registry
*/
import type { Dependencies, ProviderName, ProviderStatus, UsageSnapshot } from "./types.js";
/**
* Interface for a usage provider
*/
export interface UsageProvider {
readonly name: ProviderName;
readonly displayName: string;
/**
* Fetch current usage data for this provider
*/
fetchUsage(deps: Dependencies): Promise<UsageSnapshot>;
/**
* Fetch current status for this provider (optional)
*/
fetchStatus?(deps: Dependencies): Promise<ProviderStatus>;
/**
* Check if credentials are available (optional)
*/
hasCredentials?(deps: Dependencies): boolean;
}
/**
* Base class for providers with common functionality
*/
export abstract class BaseProvider implements UsageProvider {
abstract readonly name: ProviderName;
abstract readonly displayName: string;
abstract fetchUsage(deps: Dependencies): Promise<UsageSnapshot>;
hasCredentials(_deps: Dependencies): boolean {
return true;
}
/**
* Create an empty snapshot with an error
*/
protected emptySnapshot(error?: import("./types.js").UsageError): UsageSnapshot {
return {
provider: this.name,
displayName: this.displayName,
windows: [],
error,
};
}
/**
* Create a snapshot with usage data
*/
protected snapshot(data: Partial<Omit<UsageSnapshot, "provider" | "displayName">>): UsageSnapshot {
return {
provider: this.name,
displayName: this.displayName,
windows: [],
...data,
};
}
}
@@ -1,51 +0,0 @@
/**
* Provider detection helpers.
*/
import type { ProviderName } from "../types.js";
import { PROVIDERS } from "../types.js";
import { PROVIDER_METADATA } from "./metadata.js";
interface ProviderDetectionHint {
provider: ProviderName;
providerTokens: string[];
modelTokens: string[];
}
const PROVIDER_DETECTION_HINTS: ProviderDetectionHint[] = PROVIDERS.map((provider) => {
const detection = PROVIDER_METADATA[provider].detection ?? { providerTokens: [], modelTokens: [] };
return {
provider,
providerTokens: detection.providerTokens,
modelTokens: detection.modelTokens,
};
});
/**
* Detect the provider from model metadata.
*/
export function detectProviderFromModel(
model: { provider?: string; id?: string } | undefined
): ProviderName | undefined {
if (!model) return undefined;
const providerValue = model.provider?.toLowerCase() || "";
const idValue = model.id?.toLowerCase() || "";
if (providerValue.includes("antigravity") || idValue.includes("antigravity")) {
return "antigravity";
}
for (const hint of PROVIDER_DETECTION_HINTS) {
if (hint.providerTokens.some((token) => providerValue.includes(token))) {
return hint.provider;
}
}
for (const hint of PROVIDER_DETECTION_HINTS) {
if (hint.modelTokens.some((token) => idValue.includes(token))) {
return hint.provider;
}
}
return undefined;
}
@@ -1,174 +0,0 @@
/**
* Anthropic/Claude usage provider
*/
import * as path from "node:path";
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
import { formatReset, createTimeoutController } from "../../utils.js";
import { API_TIMEOUT_MS } from "../../config.js";
import { getSettings } from "../../settings.js";
/**
* Load Claude API token from various sources
*/
function loadClaudeToken(deps: Dependencies): string | undefined {
// Explicit override via env var (useful in CI / menu bar apps)
const envToken = deps.env.ANTHROPIC_OAUTH_TOKEN?.trim();
if (envToken) return envToken;
// Try pi auth.json next
const piAuthPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
try {
if (deps.fileExists(piAuthPath)) {
const data = JSON.parse(deps.readFile(piAuthPath) ?? "{}");
if (data.anthropic?.access) return data.anthropic.access;
}
} catch {
// Ignore parse errors
}
// Try macOS Keychain (Claude Code credentials)
try {
const keychainData = deps.execFileSync(
"security",
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
).trim();
if (keychainData) {
const parsed = JSON.parse(keychainData);
const scopes = parsed.claudeAiOauth?.scopes || [];
if (scopes.includes("user:profile") && parsed.claudeAiOauth?.accessToken) {
return parsed.claudeAiOauth.accessToken;
}
}
} catch {
// Keychain access failed
}
return undefined;
}
type ExtraUsageFormat = {
symbol: string;
decimalSeparator: "." | ",";
};
function getExtraUsageFormat(): ExtraUsageFormat {
const settings = getSettings();
const providerSettings = settings.providers.anthropic;
return {
symbol: providerSettings.extraUsageCurrencySymbol?.trim() ?? "",
decimalSeparator: providerSettings.extraUsageDecimalSeparator === "," ? "," : ".",
};
}
function formatExtraUsageCredits(credits: number, format: ExtraUsageFormat): string {
const amount = (credits / 100).toFixed(2);
const formatted = format.decimalSeparator === "," ? amount.replace(".", ",") : amount;
return format.symbol ? `${format.symbol}${formatted}` : formatted;
}
export class AnthropicProvider extends BaseProvider {
readonly name = "anthropic" as const;
readonly displayName = "Claude Plan";
hasCredentials(deps: Dependencies): boolean {
return Boolean(loadClaudeToken(deps));
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const token = loadClaudeToken(deps);
if (!token) {
return this.emptySnapshot(noCredentials());
}
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const res = await deps.fetch("https://api.anthropic.com/api/oauth/usage", {
headers: {
Authorization: `Bearer ${token}`,
"anthropic-beta": "oauth-2025-04-20",
},
signal: controller.signal,
});
clear();
if (!res.ok) {
return this.emptySnapshot(httpError(res.status));
}
const data = (await res.json()) as {
five_hour?: { utilization?: number; resets_at?: string };
seven_day?: { utilization?: number; resets_at?: string };
extra_usage?: {
is_enabled?: boolean;
used_credits?: number;
monthly_limit?: number;
utilization?: number;
};
};
const windows: RateWindow[] = [];
if (data.five_hour?.utilization !== undefined) {
const resetAt = data.five_hour.resets_at ? new Date(data.five_hour.resets_at) : undefined;
windows.push({
label: "5h",
usedPercent: data.five_hour.utilization,
resetDescription: resetAt ? formatReset(resetAt) : undefined,
resetAt: resetAt?.toISOString(),
});
}
if (data.seven_day?.utilization !== undefined) {
const resetAt = data.seven_day.resets_at ? new Date(data.seven_day.resets_at) : undefined;
windows.push({
label: "Week",
usedPercent: data.seven_day.utilization,
resetDescription: resetAt ? formatReset(resetAt) : undefined,
resetAt: resetAt?.toISOString(),
});
}
// Extra usage
const extraUsageEnabled = data.extra_usage?.is_enabled === true;
const fiveHourUsage = data.five_hour?.utilization ?? 0;
if (extraUsageEnabled) {
const extra = data.extra_usage!;
const usedCredits = extra.used_credits || 0;
const monthlyLimit = extra.monthly_limit;
const utilization = extra.utilization || 0;
const format = getExtraUsageFormat();
// "active" when 5h >= 99%, otherwise "on"
const extraStatus = fiveHourUsage >= 99 ? "active" : "on";
let label: string;
if (monthlyLimit && monthlyLimit > 0) {
label = `Extra [${extraStatus}] ${formatExtraUsageCredits(usedCredits, format)}/${formatExtraUsageCredits(monthlyLimit, format)}`;
} else {
label = `Extra [${extraStatus}] ${formatExtraUsageCredits(usedCredits, format)}`;
}
windows.push({
label,
usedPercent: utilization,
resetDescription: extraStatus === "active" ? "__ACTIVE__" : undefined,
});
}
return this.snapshot({
windows,
extraUsageEnabled,
fiveHourUsage,
});
} catch {
clear();
return this.emptySnapshot(fetchFailed());
}
}
}
@@ -1,226 +0,0 @@
/**
* Google Antigravity usage provider
*/
import * as path from "node:path";
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
import { createTimeoutController, formatReset } from "../../utils.js";
import { API_TIMEOUT_MS } from "../../config.js";
const ANTIGRAVITY_ENDPOINTS = [
"https://daily-cloudcode-pa.sandbox.googleapis.com",
"https://cloudcode-pa.googleapis.com",
] as const;
const ANTIGRAVITY_HEADERS = {
"User-Agent": "antigravity/1.11.5 darwin/arm64",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"Client-Metadata": JSON.stringify({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
}),
};
const ANTIGRAVITY_HIDDEN_MODELS = new Set(["tab_flash_lite_preview"]);
interface AntigravityAuth {
access?: string;
accessToken?: string;
token?: string;
key?: string;
projectId?: string;
project?: string;
}
interface CloudCodeQuotaResponse {
models?: Record<string, {
displayName?: string;
model?: string;
isInternal?: boolean;
quotaInfo?: {
remainingFraction?: number;
limit?: string;
resetTime?: string;
};
}>;
}
interface ParsedModelQuota {
name: string;
remainingFraction: number;
resetAt?: Date;
}
/**
* Load Antigravity access token from auth.json
*/
function loadAntigravityAuth(deps: Dependencies): AntigravityAuth | undefined {
// Explicit override via env var
const envProjectId = (deps.env.GOOGLE_ANTIGRAVITY_PROJECT_ID || deps.env.GOOGLE_ANTIGRAVITY_PROJECT)?.trim();
const envToken = (deps.env.GOOGLE_ANTIGRAVITY_OAUTH_TOKEN || deps.env.ANTIGRAVITY_OAUTH_TOKEN)?.trim();
if (envToken) {
return { token: envToken, projectId: envProjectId || undefined };
}
// Also support passing pi-ai style JSON api key: { token, projectId }
const envApiKey = (deps.env.GOOGLE_ANTIGRAVITY_API_KEY || deps.env.ANTIGRAVITY_API_KEY)?.trim();
if (envApiKey) {
try {
const parsed = JSON.parse(envApiKey) as { token?: string; projectId?: string };
if (parsed?.token) {
return { token: parsed.token, projectId: parsed.projectId || envProjectId || undefined };
}
} catch {
// not JSON
}
return { token: envApiKey, projectId: envProjectId || undefined };
}
const piAuthPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
try {
if (deps.fileExists(piAuthPath)) {
const data = JSON.parse(deps.readFile(piAuthPath) ?? "{}");
const entry = data["google-antigravity"];
if (!entry) return undefined;
if (typeof entry === "string") {
return { token: entry };
}
return {
access: entry.access,
accessToken: entry.accessToken,
token: entry.token,
key: entry.key,
projectId: entry.projectId ?? entry.project,
};
}
} catch {
// Ignore parse errors
}
return undefined;
}
function resolveAntigravityToken(auth: AntigravityAuth | undefined): string | undefined {
return auth?.access ?? auth?.accessToken ?? auth?.token ?? auth?.key;
}
function parseResetTime(value?: string): Date | undefined {
if (!value) return undefined;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return undefined;
return date;
}
function toUsedPercent(remainingFraction: number): number {
const fraction = Number.isFinite(remainingFraction) ? remainingFraction : 1;
const used = (1 - fraction) * 100;
return Math.max(0, Math.min(100, used));
}
async function fetchAntigravityQuota(
deps: Dependencies,
endpoint: string,
token: string,
projectId?: string
): Promise<{ data?: CloudCodeQuotaResponse; status?: number }> {
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const payload = projectId ? { project: projectId } : {};
const res = await deps.fetch(`${endpoint}/v1internal:fetchAvailableModels`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...ANTIGRAVITY_HEADERS,
},
body: JSON.stringify(payload),
signal: controller.signal,
});
clear();
if (!res.ok) return { status: res.status };
const data = (await res.json()) as CloudCodeQuotaResponse;
return { data };
} catch {
clear();
return {};
}
}
export class AntigravityProvider extends BaseProvider {
readonly name = "antigravity" as const;
readonly displayName = "Antigravity";
hasCredentials(deps: Dependencies): boolean {
return Boolean(resolveAntigravityToken(loadAntigravityAuth(deps)));
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const auth = loadAntigravityAuth(deps);
const token = resolveAntigravityToken(auth);
if (!token) {
return this.emptySnapshot(noCredentials());
}
let data: CloudCodeQuotaResponse | undefined;
let lastStatus: number | undefined;
for (const endpoint of ANTIGRAVITY_ENDPOINTS) {
const result = await fetchAntigravityQuota(deps, endpoint, token, auth?.projectId);
if (result.data) {
data = result.data;
break;
}
if (result.status) {
lastStatus = result.status;
}
}
if (!data) {
return lastStatus ? this.emptySnapshot(httpError(lastStatus)) : this.emptySnapshot(fetchFailed());
}
const modelByName = new Map<string, ParsedModelQuota>();
for (const [modelId, model] of Object.entries(data.models ?? {})) {
if (model.isInternal) continue;
if (modelId && ANTIGRAVITY_HIDDEN_MODELS.has(modelId.toLowerCase())) continue;
const name = model.displayName ?? modelId ?? model.model ?? "unknown";
if (!name) continue;
if (ANTIGRAVITY_HIDDEN_MODELS.has(name.toLowerCase())) continue;
const remainingFraction = model.quotaInfo?.remainingFraction ?? 1;
const resetAt = parseResetTime(model.quotaInfo?.resetTime);
const existing = modelByName.get(name);
if (!existing) {
modelByName.set(name, { name, remainingFraction, resetAt });
continue;
}
let next = existing;
if (remainingFraction < existing.remainingFraction) {
next = { name, remainingFraction, resetAt };
} else if (remainingFraction === existing.remainingFraction && resetAt) {
if (!existing.resetAt || resetAt.getTime() < existing.resetAt.getTime()) {
next = { ...existing, resetAt };
}
} else if (!existing.resetAt && resetAt) {
next = { ...existing, resetAt };
}
if (next !== existing) {
modelByName.set(name, next);
}
}
const parsedModels = Array.from(modelByName.values()).sort((a, b) => a.name.localeCompare(b.name));
const buildWindow = (label: string, remainingFraction: number, resetAt?: Date): RateWindow => ({
label,
usedPercent: toUsedPercent(remainingFraction),
resetDescription: resetAt ? formatReset(resetAt) : undefined,
resetAt: resetAt?.toISOString(),
});
const windows = parsedModels.map((model) => buildWindow(model.name, model.remainingFraction, model.resetAt));
return this.snapshot({ windows });
}
}
@@ -1,186 +0,0 @@
/**
* OpenAI Codex usage provider
*/
import * as path from "node:path";
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
import { formatReset, createTimeoutController } from "../../utils.js";
import { API_TIMEOUT_MS } from "../../config.js";
interface CodexRateWindow {
reset_at?: number;
limit_window_seconds?: number;
used_percent?: number;
}
interface CodexRateLimit {
primary_window?: CodexRateWindow;
secondary_window?: CodexRateWindow;
}
interface CodexAdditionalRateLimit {
limit_name?: string;
metered_feature?: string;
rate_limit?: CodexRateLimit;
}
/**
* Load Codex credentials from auth.json
* First tries pi's auth.json, then falls back to legacy codex location
*/
function loadCodexCredentials(deps: Dependencies): { accessToken?: string; accountId?: string } {
// Explicit override via env var
const envAccessToken = (
deps.env.OPENAI_CODEX_OAUTH_TOKEN ||
deps.env.OPENAI_CODEX_ACCESS_TOKEN ||
deps.env.CODEX_OAUTH_TOKEN ||
deps.env.CODEX_ACCESS_TOKEN
)?.trim();
const envAccountId = (deps.env.OPENAI_CODEX_ACCOUNT_ID || deps.env.CHATGPT_ACCOUNT_ID)?.trim();
if (envAccessToken) {
return { accessToken: envAccessToken, accountId: envAccountId || undefined };
}
// Try pi's auth.json first
const piAuthPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
try {
if (deps.fileExists(piAuthPath)) {
const data = JSON.parse(deps.readFile(piAuthPath) ?? "{}");
if (data["openai-codex"]?.access) {
return {
accessToken: data["openai-codex"].access,
accountId: data["openai-codex"].accountId,
};
}
}
} catch {
// Ignore parse errors, try legacy location
}
// Fall back to legacy codex location
const codexHome = deps.env.CODEX_HOME || path.join(deps.homedir(), ".codex");
const authPath = path.join(codexHome, "auth.json");
try {
if (deps.fileExists(authPath)) {
const data = JSON.parse(deps.readFile(authPath) ?? "{}");
if (data.OPENAI_API_KEY) {
return { accessToken: data.OPENAI_API_KEY };
} else if (data.tokens?.access_token) {
return {
accessToken: data.tokens.access_token,
accountId: data.tokens.account_id,
};
}
}
} catch {
// Ignore parse errors
}
return {};
}
function getWindowLabel(windowSeconds?: number, fallbackWindowSeconds?: number): string {
const safeWindowSeconds =
typeof windowSeconds === "number" && windowSeconds > 0
? windowSeconds
: typeof fallbackWindowSeconds === "number" && fallbackWindowSeconds > 0
? fallbackWindowSeconds
: 0;
if (!safeWindowSeconds) {
return "0h";
}
const windowHours = Math.round(safeWindowSeconds / 3600);
if (windowHours >= 144) return "Week";
if (windowHours >= 24) return "Day";
return `${windowHours}h`;
}
function pushWindow(
windows: RateWindow[],
prefix: string | undefined,
window: CodexRateWindow | undefined,
fallbackWindowSeconds?: number
): void {
if (!window) return;
const resetDate = window.reset_at ? new Date(window.reset_at * 1000) : undefined;
const label = getWindowLabel(window.limit_window_seconds, fallbackWindowSeconds);
const windowLabel = prefix ? `${prefix} ${label}` : label;
windows.push({
label: windowLabel,
usedPercent: window.used_percent || 0,
resetDescription: resetDate ? formatReset(resetDate) : undefined,
resetAt: resetDate?.toISOString(),
});
}
function addRateWindows(windows: RateWindow[], rateLimit: CodexRateLimit | undefined, prefix?: string): void {
pushWindow(windows, prefix, rateLimit?.primary_window, 10800);
pushWindow(windows, prefix, rateLimit?.secondary_window, 86400);
}
export class CodexProvider extends BaseProvider {
readonly name = "codex" as const;
readonly displayName = "Codex Plan";
hasCredentials(deps: Dependencies): boolean {
return Boolean(loadCodexCredentials(deps).accessToken);
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const { accessToken, accountId } = loadCodexCredentials(deps);
if (!accessToken) {
return this.emptySnapshot(noCredentials());
}
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
};
if (accountId) {
headers["ChatGPT-Account-Id"] = accountId;
}
const res = await deps.fetch("https://chatgpt.com/backend-api/wham/usage", {
headers,
signal: controller.signal,
});
clear();
if (!res.ok) {
return this.emptySnapshot(httpError(res.status));
}
const data = (await res.json()) as {
rate_limit?: CodexRateLimit;
additional_rate_limits?: CodexAdditionalRateLimit[];
};
const windows: RateWindow[] = [];
addRateWindows(windows, data.rate_limit);
if (Array.isArray(data.additional_rate_limits)) {
for (const entry of data.additional_rate_limits) {
if (!entry || typeof entry !== "object") continue;
const prefix =
typeof entry.limit_name === "string" && entry.limit_name.trim().length > 0
? entry.limit_name.trim()
: typeof entry.metered_feature === "string" && entry.metered_feature.trim().length > 0
? entry.metered_feature.trim()
: "Additional";
addRateWindows(windows, entry.rate_limit, prefix);
}
}
return this.snapshot({ windows });
} catch {
clear();
return this.emptySnapshot(fetchFailed());
}
}
}
@@ -1,176 +0,0 @@
/**
* GitHub Copilot usage provider
*/
import * as path from "node:path";
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
import { formatReset, createTimeoutController } from "../../utils.js";
import { API_TIMEOUT_MS } from "../../config.js";
/**
* Copilot token entries stored by legacy GitHub Copilot CLI
*/
type CopilotHostEntry = {
oauth_token?: string;
user_token?: string;
github_token?: string;
token?: string;
};
const COPILOT_TOKEN_KEYS: Array<keyof CopilotHostEntry> = [
"oauth_token",
"user_token",
"github_token",
"token",
];
function getTokenFromHostEntry(entry: CopilotHostEntry | undefined): string | undefined {
if (!entry) return undefined;
for (const key of COPILOT_TOKEN_KEYS) {
const value = entry[key];
if (typeof value === "string" && value.length > 0) {
return value;
}
}
return undefined;
}
function loadLegacyCopilotToken(deps: Dependencies): string | undefined {
const configHome = deps.env.XDG_CONFIG_HOME || path.join(deps.homedir(), ".config");
const legacyPaths = [
path.join(configHome, "github-copilot", "hosts.json"),
path.join(deps.homedir(), ".github-copilot", "hosts.json"),
];
for (const hostsPath of legacyPaths) {
try {
if (!deps.fileExists(hostsPath)) continue;
const data = JSON.parse(deps.readFile(hostsPath) ?? "{}");
if (!data || typeof data !== "object") continue;
const normalizedHosts: Record<string, CopilotHostEntry> = {};
for (const [host, entry] of Object.entries(data as Record<string, CopilotHostEntry>)) {
normalizedHosts[host.toLowerCase()] = entry;
}
const preferredToken =
getTokenFromHostEntry(normalizedHosts["github.com"]) ||
getTokenFromHostEntry(normalizedHosts["api.github.com"]);
if (preferredToken) return preferredToken;
for (const entry of Object.values(normalizedHosts)) {
const token = getTokenFromHostEntry(entry);
if (token) return token;
}
} catch {
// Ignore parse errors
}
}
return undefined;
}
/**
* Load Copilot token from pi auth.json first, then fallback to legacy locations.
*/
function loadCopilotToken(deps: Dependencies): string | undefined {
// Explicit override via env var
const envToken = (deps.env.COPILOT_GITHUB_TOKEN || deps.env.GH_TOKEN || deps.env.GITHUB_TOKEN || deps.env.COPILOT_TOKEN)?.trim();
if (envToken) return envToken;
const authPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
try {
if (deps.fileExists(authPath)) {
const data = JSON.parse(deps.readFile(authPath) ?? "{}");
// Prefer refresh token (GitHub access token) for GitHub API endpoints.
const piToken = data["github-copilot"]?.refresh || data["github-copilot"]?.access;
if (typeof piToken === "string" && piToken.length > 0) return piToken;
}
} catch {
// Ignore parse errors
}
return loadLegacyCopilotToken(deps);
}
export class CopilotProvider extends BaseProvider {
readonly name = "copilot" as const;
readonly displayName = "Copilot Plan";
hasCredentials(deps: Dependencies): boolean {
return Boolean(loadCopilotToken(deps));
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const token = loadCopilotToken(deps);
if (!token) {
return this.emptySnapshot(noCredentials());
}
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const res = await deps.fetch("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 ${token}`,
},
signal: controller.signal,
});
clear();
if (!res.ok) {
return this.emptySnapshot(httpError(res.status));
}
const data = (await res.json()) as {
quota_reset_date_utc?: string;
quota_snapshots?: {
premium_interactions?: {
percent_remaining?: number;
remaining?: number;
entitlement?: number;
};
};
};
const windows: RateWindow[] = [];
const resetDate = data.quota_reset_date_utc ? new Date(data.quota_reset_date_utc) : undefined;
const resetDesc = resetDate ? formatReset(resetDate) : undefined;
let requestsRemaining: number | undefined;
let requestsEntitlement: number | undefined;
if (data.quota_snapshots?.premium_interactions) {
const pi = data.quota_snapshots.premium_interactions;
const monthUsedPercent = Math.max(0, 100 - (pi.percent_remaining || 0));
windows.push({
label: "Month",
usedPercent: monthUsedPercent,
resetDescription: resetDesc,
resetAt: resetDate?.toISOString(),
});
const remaining = pi.remaining ?? 0;
const entitlement = pi.entitlement ?? 0;
requestsRemaining = remaining;
requestsEntitlement = entitlement;
}
return this.snapshot({
windows,
requestsRemaining,
requestsEntitlement,
});
} catch {
clear();
return this.emptySnapshot(fetchFailed());
}
}
}
@@ -1,130 +0,0 @@
/**
* Google Gemini usage provider
*/
import * as path from "node:path";
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
import { createTimeoutController } from "../../utils.js";
import { API_TIMEOUT_MS } from "../../config.js";
/**
* Load Gemini access token from various sources
*/
function loadGeminiToken(deps: Dependencies): string | undefined {
// Explicit override via env var
const envToken = (
deps.env.GOOGLE_GEMINI_CLI_OAUTH_TOKEN ||
deps.env.GOOGLE_GEMINI_CLI_ACCESS_TOKEN ||
deps.env.GEMINI_OAUTH_TOKEN ||
deps.env.GOOGLE_GEMINI_OAUTH_TOKEN
)?.trim();
if (envToken) return envToken;
// Try pi auth.json first
const piAuthPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
try {
if (deps.fileExists(piAuthPath)) {
const data = JSON.parse(deps.readFile(piAuthPath) ?? "{}");
if (data["google-gemini-cli"]?.access) return data["google-gemini-cli"].access;
}
} catch {
// Ignore parse errors
}
// Try ~/.gemini/oauth_creds.json
const credPath = path.join(deps.homedir(), ".gemini", "oauth_creds.json");
try {
if (deps.fileExists(credPath)) {
const data = JSON.parse(deps.readFile(credPath) ?? "{}");
if (data.access_token) return data.access_token;
}
} catch {
// Ignore parse errors
}
return undefined;
}
export class GeminiProvider extends BaseProvider {
readonly name = "gemini" as const;
readonly displayName = "Gemini Plan";
hasCredentials(deps: Dependencies): boolean {
return Boolean(loadGeminiToken(deps));
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const token = loadGeminiToken(deps);
if (!token) {
return this.emptySnapshot(noCredentials());
}
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const res = await deps.fetch("https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: "{}",
signal: controller.signal,
});
clear();
if (!res.ok) {
return this.emptySnapshot(httpError(res.status));
}
const data = (await res.json()) as {
buckets?: Array<{
modelId?: string;
remainingFraction?: number;
}>;
};
// Aggregate quotas by model type
const quotas: Record<string, number> = {};
for (const bucket of data.buckets || []) {
const model = bucket.modelId || "unknown";
const frac = bucket.remainingFraction ?? 1;
if (!quotas[model] || frac < quotas[model]) {
quotas[model] = frac;
}
}
const windows: RateWindow[] = [];
let proMin = 1;
let flashMin = 1;
let hasProModel = false;
let hasFlashModel = false;
for (const [model, frac] of Object.entries(quotas)) {
if (model.toLowerCase().includes("pro")) {
hasProModel = true;
if (frac < proMin) proMin = frac;
}
if (model.toLowerCase().includes("flash")) {
hasFlashModel = true;
if (frac < flashMin) flashMin = frac;
}
}
if (hasProModel) {
windows.push({ label: "Pro", usedPercent: (1 - proMin) * 100 });
}
if (hasFlashModel) {
windows.push({ label: "Flash", usedPercent: (1 - flashMin) * 100 });
}
return this.snapshot({ windows });
} catch {
clear();
return this.emptySnapshot(fetchFailed());
}
}
}
@@ -1,92 +0,0 @@
/**
* AWS Kiro usage provider
*/
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCli, notLoggedIn, fetchFailed } from "../../errors.js";
import { formatReset, stripAnsi, whichSync } from "../../utils.js";
import { CLI_TIMEOUT_MS } from "../../config.js";
export class KiroProvider extends BaseProvider {
readonly name = "kiro" as const;
readonly displayName = "Kiro Plan";
hasCredentials(deps: Dependencies): boolean {
return Boolean(whichSync("kiro-cli", deps));
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const kiroBinary = whichSync("kiro-cli", deps);
if (!kiroBinary) {
return this.emptySnapshot(noCli("kiro-cli"));
}
try {
// Check if logged in
try {
deps.execFileSync(kiroBinary, ["whoami"], {
encoding: "utf-8",
timeout: API_TIMEOUT_MS,
stdio: ["ignore", "pipe", "pipe"],
});
} catch {
return this.emptySnapshot(notLoggedIn());
}
// Get usage
const output = deps.execFileSync(kiroBinary, ["chat", "--no-interactive", "/usage"], {
encoding: "utf-8",
timeout: CLI_TIMEOUT_MS,
env: { ...deps.env, TERM: "xterm-256color" },
stdio: ["ignore", "pipe", "pipe"],
});
const stripped = stripAnsi(output);
const windows: RateWindow[] = [];
// Parse credits percentage from "████...█ X%"
let creditsPercent = 0;
const percentMatch = stripped.match(/█+\s*(\d+)%/);
if (percentMatch) {
creditsPercent = parseInt(percentMatch[1], 10);
}
// Parse credits used/total from "(X.XX of Y covered in plan)"
const creditsMatch = stripped.match(/\((\d+\.?\d*)\s+of\s+(\d+)\s+covered/);
if (creditsMatch && !percentMatch) {
const creditsUsed = parseFloat(creditsMatch[1]);
const creditsTotal = parseFloat(creditsMatch[2]);
if (creditsTotal > 0) {
creditsPercent = (creditsUsed / creditsTotal) * 100;
}
}
// Parse reset date from "resets on 01/01"
let resetsAt: Date | undefined;
const resetMatch = stripped.match(/resets on (\d{2}\/\d{2})/);
if (resetMatch) {
const [month, day] = resetMatch[1].split("/").map(Number);
const now = new Date();
const year = now.getFullYear();
resetsAt = new Date(year, month - 1, day);
if (resetsAt < now) resetsAt.setFullYear(year + 1);
}
windows.push({
label: "Credits",
usedPercent: creditsPercent,
resetDescription: resetsAt ? formatReset(resetsAt) : undefined,
resetAt: resetsAt?.toISOString(),
});
return this.snapshot({ windows });
} catch {
return this.emptySnapshot(fetchFailed());
}
}
// Kiro doesn't have a public status page
}
const API_TIMEOUT_MS = 5000;
@@ -1,191 +0,0 @@
/**
* OpenCode Go usage provider
*
* OpenCode Go is a subscription plan ($10/mo) from the OpenCode team
* that provides access to open coding models (GLM-5, Kimi K2.5, MiniMax M2.5)
* through opencode.ai/zen/go/v1 endpoints.
*
* Credentials are discovered from:
* 1. Config file (~/.config/opencode/opencode-go-usage.json)
* 2. Environment variables (OPENCODE_GO_WORKSPACE_ID, OPENCODE_GO_AUTH_COOKIE)
* 3. Environment variable (OPENCODE_API_KEY) - for API key auth
*
* Usage limits: rolling 5h ($12), weekly ($30), monthly ($60) — tracked in dollar value.
*
* Usage is fetched by scraping the HTML dashboard at opencode.ai/workspace/{id}/go
* using the auth cookie from the browser.
*/
import * as path from "node:path";
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCredentials, fetchFailed, apiError } from "../../errors.js";
import { formatReset, createTimeoutController } from "../../utils.js";
import { API_TIMEOUT_MS } from "../../config.js";
interface OpenCodeGoConfig {
workspaceId?: string;
authCookie?: string;
}
/**
* Load config from file or environment
*/
function loadOpenCodeGoConfig(deps: Dependencies): OpenCodeGoConfig | undefined {
// 1. Config file (~/.config/opencode/opencode-go-usage.json)
const configPath = path.join(deps.homedir(), ".config", "opencode", "opencode-go-usage.json");
try {
if (deps.fileExists(configPath)) {
const content = deps.readFile(configPath);
if (content) {
const parsed = JSON.parse(content) as OpenCodeGoConfig;
if (parsed.workspaceId && parsed.authCookie) {
return parsed;
}
}
}
} catch {
// Ignore parse errors
}
// 2. Environment variables
const workspaceId = deps.env.OPENCODE_GO_WORKSPACE_ID?.trim();
const authCookie = deps.env.OPENCODE_GO_AUTH_COOKIE?.trim();
if (workspaceId && authCookie) {
return { workspaceId, authCookie };
}
return undefined;
}
interface UsageData {
usagePercent: number;
resetInSec: number;
}
/**
* Fetch usage by scraping the HTML dashboard
*/
async function fetchOpenCodeGoUsage(
workspaceId: string,
authCookie: string,
fetch: typeof globalThis.fetch,
signal?: AbortSignal
): Promise<{ rolling?: UsageData; weekly?: UsageData; monthly?: UsageData }> {
const url = `https://opencode.ai/workspace/${encodeURIComponent(workspaceId)}/go`;
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Cookie: `auth=${authCookie}`,
},
signal,
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
throw new Error("Authentication failed. Please refresh your auth cookie.");
}
throw new Error(`HTTP ${response.status}: Request failed`);
}
const html = await response.text();
const usage: { rolling?: UsageData; weekly?: UsageData; monthly?: UsageData } = {};
const patterns = {
rolling: /rollingUsage:\$R\[\d+\]=(\{[^}]+\})/,
weekly: /weeklyUsage:\$R\[\d+\]=(\{[^}]+\})/,
monthly: /monthlyUsage:\$R\[\d+\]=(\{[^}]+\})/,
};
for (const [key, pattern] of Object.entries(patterns)) {
const match = html.match(pattern);
if (match) {
try {
// Fix the JavaScript object syntax to valid JSON
const jsonStr = match[1].replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g, '$1"$2"$3');
usage[key as keyof typeof usage] = JSON.parse(jsonStr);
} catch {
// Ignore parse errors
}
}
}
return usage;
}
export class OpenCodeGoProvider extends BaseProvider {
readonly name = "opencode-go" as const;
readonly displayName = "OpenCode Go";
hasCredentials(deps: Dependencies): boolean {
return Boolean(loadOpenCodeGoConfig(deps));
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const config = loadOpenCodeGoConfig(deps);
if (!config || !config.workspaceId || !config.authCookie) {
return this.emptySnapshot(noCredentials());
}
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const usage = await fetchOpenCodeGoUsage(
config.workspaceId,
config.authCookie,
deps.fetch,
controller.signal
);
clear();
const windows: RateWindow[] = [];
if (usage.rolling) {
const resetDate = usage.rolling.resetInSec > 0
? new Date(Date.now() + usage.rolling.resetInSec * 1000)
: undefined;
windows.push({
label: "5h",
usedPercent: usage.rolling.usagePercent,
resetDescription: resetDate ? formatReset(resetDate) : undefined,
resetAt: resetDate?.toISOString(),
});
}
if (usage.weekly) {
const resetDate = usage.weekly.resetInSec > 0
? new Date(Date.now() + usage.weekly.resetInSec * 1000)
: undefined;
windows.push({
label: "Week",
usedPercent: usage.weekly.usagePercent,
resetDescription: resetDate ? formatReset(resetDate) : undefined,
resetAt: resetDate?.toISOString(),
});
}
if (usage.monthly) {
const resetDate = usage.monthly.resetInSec > 0
? new Date(Date.now() + usage.monthly.resetInSec * 1000)
: undefined;
windows.push({
label: "Month",
usedPercent: usage.monthly.usagePercent,
resetDescription: resetDate ? formatReset(resetDate) : undefined,
resetAt: resetDate?.toISOString(),
});
}
return this.snapshot({ windows });
} catch (err) {
clear();
const error = err instanceof Error ? err.message : "Unknown error";
if (error.includes("Authentication failed")) {
return this.emptySnapshot(apiError(error));
}
return this.emptySnapshot(fetchFailed(error));
}
}
}
@@ -1,120 +0,0 @@
/**
* z.ai usage provider
*/
import * as path from "node:path";
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
import { BaseProvider } from "../../provider.js";
import { noCredentials, fetchFailed, httpError, apiError } from "../../errors.js";
import { formatReset, createTimeoutController } from "../../utils.js";
import { API_TIMEOUT_MS } from "../../config.js";
/**
* Load z.ai API key from environment or auth.json
*/
function loadZaiApiKey(deps: Dependencies): string | undefined {
// Try environment variable first
if (deps.env.ZAI_API_KEY) {
return deps.env.ZAI_API_KEY;
}
if (deps.env.Z_AI_API_KEY) {
return deps.env.Z_AI_API_KEY;
}
// Try pi auth.json
const authPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
try {
if (deps.fileExists(authPath)) {
const auth = JSON.parse(deps.readFile(authPath) ?? "{}");
return auth["z-ai"]?.access || auth["z-ai"]?.key || auth["zai"]?.access || auth["zai"]?.key;
}
} catch {
// Ignore parse errors
}
return undefined;
}
export class ZaiProvider extends BaseProvider {
readonly name = "zai" as const;
readonly displayName = "z.ai Plan";
hasCredentials(deps: Dependencies): boolean {
return Boolean(loadZaiApiKey(deps));
}
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
const apiKey = loadZaiApiKey(deps);
if (!apiKey) {
return this.emptySnapshot(noCredentials());
}
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const res = await deps.fetch("https://api.z.ai/api/monitor/usage/quota/limit", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
signal: controller.signal,
});
clear();
if (!res.ok) {
return this.emptySnapshot(httpError(res.status));
}
const data = (await res.json()) as {
success?: boolean;
code?: number;
msg?: string;
data?: {
limits?: Array<{
type?: string;
unit?: number;
number?: number;
percentage?: number;
nextResetTime?: string;
}>;
};
};
if (!data.success || data.code !== 200) {
return this.emptySnapshot(apiError(data.msg || "API error"));
}
const windows: RateWindow[] = [];
const limits = data.data?.limits || [];
for (const limit of limits) {
const percent = limit.percentage || 0;
const nextReset = limit.nextResetTime ? new Date(limit.nextResetTime) : undefined;
if (limit.type === "TOKENS_LIMIT") {
windows.push({
label: "Tokens",
usedPercent: percent,
resetDescription: nextReset ? formatReset(nextReset) : undefined,
resetAt: nextReset?.toISOString(),
});
} else if (limit.type === "TIME_LIMIT") {
windows.push({
label: "Monthly",
usedPercent: percent,
resetDescription: nextReset ? formatReset(nextReset) : undefined,
resetAt: nextReset?.toISOString(),
});
}
}
return this.snapshot({ windows });
} catch {
clear();
return this.emptySnapshot(fetchFailed());
}
}
// z.ai doesn't have a public status page
}
@@ -1,5 +0,0 @@
/**
* Provider registry exports.
*/
export * from "./registry.js";
@@ -1,16 +0,0 @@
/**
* Provider metadata shared across the core.
*/
export {
PROVIDERS,
PROVIDER_METADATA,
PROVIDER_DISPLAY_NAMES,
} from "../../../src/shared.js";
export type {
ProviderName,
ProviderMetadata,
ProviderStatusConfig,
ProviderDetectionConfig,
} from "../../../src/shared.js";
@@ -1,57 +0,0 @@
/**
* Provider registry - exports all providers
*/
export { AnthropicProvider } from "./impl/anthropic.js";
export { CopilotProvider } from "./impl/copilot.js";
export { GeminiProvider } from "./impl/gemini.js";
export { AntigravityProvider } from "./impl/antigravity.js";
export { CodexProvider } from "./impl/codex.js";
export { KiroProvider } from "./impl/kiro.js";
export { ZaiProvider } from "./impl/zai.js";
export { OpenCodeGoProvider } from "./impl/opencode-go.js";
import type { Dependencies, ProviderName } from "../types.js";
import type { UsageProvider } from "../provider.js";
import { PROVIDERS } from "./metadata.js";
import { AnthropicProvider } from "./impl/anthropic.js";
import { CopilotProvider } from "./impl/copilot.js";
import { GeminiProvider } from "./impl/gemini.js";
import { AntigravityProvider } from "./impl/antigravity.js";
import { CodexProvider } from "./impl/codex.js";
import { KiroProvider } from "./impl/kiro.js";
import { ZaiProvider } from "./impl/zai.js";
import { OpenCodeGoProvider } from "./impl/opencode-go.js";
const PROVIDER_FACTORIES: Record<ProviderName, () => UsageProvider> = {
anthropic: () => new AnthropicProvider(),
copilot: () => new CopilotProvider(),
gemini: () => new GeminiProvider(),
antigravity: () => new AntigravityProvider(),
codex: () => new CodexProvider(),
kiro: () => new KiroProvider(),
zai: () => new ZaiProvider(),
"opencode-go": () => new OpenCodeGoProvider(),
};
/**
* Create a provider instance by name
*/
export function createProvider(name: ProviderName): UsageProvider {
return PROVIDER_FACTORIES[name]();
}
/**
* Get all provider instances
*/
export function getAllProviders(): UsageProvider[] {
return PROVIDERS.map((name) => PROVIDER_FACTORIES[name]());
}
export function hasProviderCredentials(name: ProviderName, deps: Dependencies): boolean {
const provider = createProvider(name);
if (provider.hasCredentials) {
return provider.hasCredentials(deps);
}
return true;
}
@@ -1,109 +0,0 @@
/**
* Provider-specific settings helpers.
*/
import type { SettingItem } from "@mariozechner/pi-tui";
import type { ProviderName } from "../types.js";
import type { Settings, CoreProviderSettings } from "../settings-types.js";
import { CUSTOM_OPTION } from "../ui/settings-list.js";
function buildBaseProviderItems(ps: CoreProviderSettings): SettingItem[] {
const enabledValue = ps.enabled === "auto" ? "auto" : ps.enabled === true || ps.enabled === "on" ? "on" : "off";
return [
{
id: "enabled",
label: "Enabled",
currentValue: enabledValue,
values: ["auto", "on", "off"],
description: "Auto enables if credentials are detected.",
},
{
id: "fetchStatus",
label: "Fetch Status",
currentValue: ps.fetchStatus ? "on" : "off",
values: ["on", "off"],
description: "Fetch status page indicator for this provider.",
},
];
}
function resolveEnabledValue(value: string): CoreProviderSettings["enabled"] {
if (value === "auto") return "auto";
return value === "on";
}
function applyBaseProviderSetting(ps: CoreProviderSettings, id: string, value: string): boolean {
switch (id) {
case "enabled":
ps.enabled = resolveEnabledValue(value);
return true;
case "fetchStatus":
ps.fetchStatus = value === "on";
return true;
default:
return false;
}
}
/**
* Build settings items for a specific provider.
*/
export function buildProviderSettingsItems(settings: Settings, provider: ProviderName): SettingItem[] {
const ps = settings.providers[provider];
const items = buildBaseProviderItems(ps);
if (provider === "anthropic") {
const currencySymbol = ps.extraUsageCurrencySymbol?.trim();
items.push(
{
id: "extraUsageCurrencySymbol",
label: "Extra Usage Currency Symbol",
currentValue: currencySymbol ? currencySymbol : "none",
values: ["none", CUSTOM_OPTION],
description: "Prefix symbol for Extra usage amounts.",
},
{
id: "extraUsageDecimalSeparator",
label: "Extra Usage Decimal Separator",
currentValue: ps.extraUsageDecimalSeparator === "," ? "," : ".",
values: [".", ","],
description: "Decimal separator for Extra usage amounts.",
},
);
}
return items;
}
/**
* Apply a provider settings change in-place.
*/
export function applyProviderSettingsChange(
settings: Settings,
provider: ProviderName,
id: string,
value: string
): Settings {
const ps = settings.providers[provider];
if (applyBaseProviderSetting(ps, id, value)) {
return settings;
}
switch (id) {
case "extraUsageCurrencySymbol":
if (value === CUSTOM_OPTION) {
return settings;
}
if (value === "none") {
delete ps.extraUsageCurrencySymbol;
return settings;
}
ps.extraUsageCurrencySymbol = value;
return settings;
case "extraUsageDecimalSeparator":
ps.extraUsageDecimalSeparator = value === "," ? "," : ".";
return settings;
default:
return settings;
}
}
@@ -1,25 +0,0 @@
/**
* Provider status handling helpers.
*/
import type { Dependencies, ProviderName, ProviderStatus } from "../types.js";
import { fetchProviderStatus } from "../status.js";
import { PROVIDER_METADATA } from "./metadata.js";
export function providerHasStatus(
provider: ProviderName,
providerInstance?: { fetchStatus?: (deps: Dependencies) => Promise<ProviderStatus> }
): boolean {
return Boolean(providerInstance?.fetchStatus) || Boolean(PROVIDER_METADATA[provider]?.status);
}
export async function fetchProviderStatusWithFallback(
provider: ProviderName,
providerInstance: { fetchStatus?: (deps: Dependencies) => Promise<ProviderStatus> },
deps: Dependencies
): Promise<ProviderStatus> {
if (providerInstance.fetchStatus) {
return providerInstance.fetchStatus(deps);
}
return fetchProviderStatus(provider, deps);
}
@@ -1,95 +0,0 @@
/**
* Settings types and defaults for sub-core
*/
import type {
CoreSettings,
CoreProviderSettingsMap,
CoreProviderSettings,
BehaviorSettings,
ProviderName,
ProviderEnabledSetting,
} from "../../src/shared.js";
import { PROVIDERS, getDefaultCoreSettings } from "../../src/shared.js";
export type {
CoreProviderSettings,
CoreProviderSettingsMap,
BehaviorSettings,
CoreSettings,
ProviderEnabledSetting,
} from "../../src/shared.js";
/**
* Tool registration settings
*/
export interface ToolSettings {
usageTool: boolean;
allUsageTool: boolean;
}
/**
* All settings
*/
export interface Settings extends CoreSettings {
/** Version for migration */
version: number;
/** Tool registration settings */
tools: ToolSettings;
}
/**
* Current settings version
*/
export const SETTINGS_VERSION = 3;
/**
* Default settings
*/
export function getDefaultSettings(): Settings {
const coreDefaults = getDefaultCoreSettings();
return {
version: SETTINGS_VERSION,
tools: {
usageTool: false,
allUsageTool: false,
},
providers: coreDefaults.providers,
behavior: coreDefaults.behavior,
statusRefresh: coreDefaults.statusRefresh,
providerOrder: coreDefaults.providerOrder,
defaultProvider: coreDefaults.defaultProvider,
};
}
/**
* Deep merge two objects
*/
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
const result = { ...target } as T;
for (const key of Object.keys(source) as (keyof T)[]) {
const sourceValue = source[key];
const targetValue = result[key];
if (
sourceValue !== undefined &&
typeof sourceValue === "object" &&
sourceValue !== null &&
!Array.isArray(sourceValue) &&
typeof targetValue === "object" &&
targetValue !== null &&
!Array.isArray(targetValue)
) {
result[key] = deepMerge(targetValue as object, sourceValue as object) as T[keyof T];
} else if (sourceValue !== undefined) {
result[key] = sourceValue as T[keyof T];
}
}
return result;
}
/**
* Merge settings with defaults (no legacy migrations).
*/
export function mergeSettings(loaded: Partial<Settings>): Settings {
return deepMerge(getDefaultSettings(), loaded);
}
@@ -1 +0,0 @@
export { showSettingsUI } from "./settings/ui.js";
@@ -1,137 +0,0 @@
/**
* Settings persistence for sub-core
*/
import * as path from "node:path";
import type { Settings } from "./settings-types.js";
import { getDefaultSettings, mergeSettings, SETTINGS_VERSION } from "./settings-types.js";
import { getStorage } from "./storage.js";
import { getLegacySettingsPath, getSettingsPath } from "./paths.js";
import { clearCache } from "./cache.js";
/**
* Settings file path
*/
export const SETTINGS_PATH = getSettingsPath();
const LEGACY_SETTINGS_PATH = getLegacySettingsPath();
/**
* In-memory settings cache
*/
let cachedSettings: Settings | undefined;
type LoadedSettings = {
settings: Settings;
loadedVersion: number;
};
/**
* Ensure the settings directory exists
*/
function ensureSettingsDir(): void {
const storage = getStorage();
const dir = path.dirname(SETTINGS_PATH);
storage.ensureDir(dir);
}
function loadSettingsFromDisk(settingsPath: string): LoadedSettings | null {
const storage = getStorage();
if (!storage.exists(settingsPath)) return null;
const content = storage.readFile(settingsPath);
if (!content) return null;
const loaded = JSON.parse(content) as Partial<Settings>;
const loadedVersion = typeof loaded.version === "number" ? loaded.version : 0;
const merged = mergeSettings(loaded);
return { settings: merged, loadedVersion };
}
function applyVersionMigration(settings: Settings, loadedVersion: number): { settings: Settings; needsSave: boolean } {
if (loadedVersion < SETTINGS_VERSION) {
clearCache();
return { settings: { ...settings, version: SETTINGS_VERSION }, needsSave: true };
}
return { settings, needsSave: false };
}
function tryLoadSettings(settingsPath: string): LoadedSettings | null {
try {
return loadSettingsFromDisk(settingsPath);
} catch (error) {
console.error(`Failed to load settings from ${settingsPath}:`, error);
return null;
}
}
/**
* Load settings from disk
*/
export function loadSettings(): Settings {
if (cachedSettings) {
return cachedSettings;
}
const diskSettings = tryLoadSettings(SETTINGS_PATH);
if (diskSettings) {
const { settings: next, needsSave } = applyVersionMigration(diskSettings.settings, diskSettings.loadedVersion);
if (needsSave) {
saveSettings(next);
}
cachedSettings = next;
return cachedSettings;
}
const legacySettings = tryLoadSettings(LEGACY_SETTINGS_PATH);
if (legacySettings) {
const { settings: next } = applyVersionMigration(legacySettings.settings, legacySettings.loadedVersion);
const saved = saveSettings(next);
if (saved) {
getStorage().removeFile(LEGACY_SETTINGS_PATH);
}
cachedSettings = next;
return cachedSettings;
}
// Return defaults if file doesn't exist or failed to load
cachedSettings = getDefaultSettings();
return cachedSettings;
}
/**
* Save settings to disk
*/
export function saveSettings(settings: Settings): boolean {
const storage = getStorage();
try {
ensureSettingsDir();
const content = JSON.stringify(settings, null, 2);
storage.writeFile(SETTINGS_PATH, content);
cachedSettings = settings;
return true;
} catch (error) {
console.error(`Failed to save settings to ${SETTINGS_PATH}:`, error);
return false;
}
}
/**
* Reset settings to defaults
*/
export function resetSettings(): Settings {
const defaults = getDefaultSettings();
saveSettings(defaults);
return defaults;
}
/**
* Get current settings (cached)
*/
export function getSettings(): Settings {
return loadSettings();
}
/**
* Clear the settings cache (force reload on next access)
*/
export function clearSettingsCache(): void {
cachedSettings = undefined;
}
@@ -1,58 +0,0 @@
/**
* Behavior settings UI helpers.
*/
import type { SettingItem } from "@mariozechner/pi-tui";
import type { BehaviorSettings } from "../settings-types.js";
import { CUSTOM_OPTION } from "../ui/settings-list.js";
export function buildRefreshItems(settings: BehaviorSettings): SettingItem[] {
return [
{
id: "refreshInterval",
label: "Auto-refresh Interval",
currentValue: settings.refreshInterval === 0 ? "off" : `${settings.refreshInterval}s`,
values: ["off", "15s", "30s", "60s", "120s", "300s", CUSTOM_OPTION],
description: "How often to refresh automatically.",
},
{
id: "minRefreshInterval",
label: "Minimum Refresh Interval",
currentValue: settings.minRefreshInterval === 0 ? "off" : `${settings.minRefreshInterval}s`,
values: ["off", "5s", "10s", "15s", "30s", "60s", "120s", CUSTOM_OPTION],
description: "Cap refreshes even when triggered each turn.",
},
{
id: "refreshOnTurnStart",
label: "Refresh on Turn Start",
currentValue: settings.refreshOnTurnStart ? "on" : "off",
values: ["on", "off"],
description: "Refresh when a new turn starts.",
},
{
id: "refreshOnToolResult",
label: "Refresh on Tool Result",
currentValue: settings.refreshOnToolResult ? "on" : "off",
values: ["on", "off"],
description: "Refresh after tool executions.",
},
];
}
export function applyRefreshChange(settings: BehaviorSettings, id: string, value: string): BehaviorSettings {
switch (id) {
case "refreshInterval":
settings.refreshInterval = value === "off" ? 0 : parseInt(value, 10);
break;
case "minRefreshInterval":
settings.minRefreshInterval = value === "off" ? 0 : parseInt(value, 10);
break;
case "refreshOnTurnStart":
settings.refreshOnTurnStart = value === "on";
break;
case "refreshOnToolResult":
settings.refreshOnToolResult = value === "on";
break;
}
return settings;
}
@@ -1,83 +0,0 @@
/**
* Settings menu item builders.
*/
import type { SelectItem } from "@mariozechner/pi-tui";
import type { Settings } from "../settings-types.js";
import type { ProviderName } from "../types.js";
import { PROVIDER_DISPLAY_NAMES } from "../providers/metadata.js";
export type TooltipSelectItem = SelectItem & { tooltip?: string };
export function buildMainMenuItems(settings: Settings): TooltipSelectItem[] {
const enabledCount = Object.values(settings.providers).filter((p) => p.enabled !== "off" && p.enabled !== false).length;
const totalCount = Object.keys(settings.providers).length;
const toolEnabledCount = [settings.tools.usageTool, settings.tools.allUsageTool].filter(Boolean).length;
const toolTotalCount = 2;
return [
{
value: "providers",
label: "Provider Settings",
description: `${enabledCount}/${totalCount} enabled`,
tooltip: "Enable providers, toggle status fetch, and adjust provider settings.",
},
{
value: "behavior",
label: "Usage Refresh Settings",
description: `refresh ${settings.behavior.refreshInterval}s`,
tooltip: "Control usage refresh interval and triggers.",
},
{
value: "status-refresh",
label: "Status Refresh Settings",
description: `refresh ${settings.statusRefresh.refreshInterval}s`,
tooltip: "Control status refresh interval and triggers.",
},
{
value: "tools",
label: "Tool Settings",
description: `${toolEnabledCount}/${toolTotalCount} enabled`,
tooltip: "Enable sub-core tools (requires /reload to take effect).",
},
{
value: "provider-order",
label: "Provider Order",
description: settings.providerOrder.slice(0, 3).join(", ") + "...",
tooltip: "Reorder providers for cycling and auto-selection.",
},
{
value: "reset",
label: "Reset to Defaults",
description: "restore all settings",
tooltip: "Restore all sub-core settings to defaults.",
},
];
}
export function buildProviderListItems(settings: Settings): TooltipSelectItem[] {
return settings.providerOrder.map((provider) => {
const ps = settings.providers[provider];
const enabledValue = ps.enabled === "auto" ? "auto" : ps.enabled === true || ps.enabled === "on" ? "on" : "off";
const statusIcon = ps.fetchStatus ? ", status fetch on" : "";
return {
value: `provider-${provider}`,
label: PROVIDER_DISPLAY_NAMES[provider],
description: `enabled ${enabledValue}${statusIcon}`,
tooltip: `Enable ${PROVIDER_DISPLAY_NAMES[provider]} and configure status fetching.`,
};
});
}
export function buildProviderOrderItems(settings: Settings): TooltipSelectItem[] {
const activeProviders = settings.providerOrder.filter((provider) => {
const enabled = settings.providers[provider].enabled;
return enabled !== "off" && enabled !== false;
});
return activeProviders.map((provider, index) => ({
value: provider,
label: `${index + 1}. ${PROVIDER_DISPLAY_NAMES[provider]}`,
tooltip: "Reorder enabled providers (Space to toggle move mode).",
}));
}
@@ -1,38 +0,0 @@
/**
* Tool settings UI helpers.
*/
import type { SettingItem } from "@mariozechner/pi-tui";
import type { Settings, ToolSettings } from "../settings-types.js";
export function buildToolItems(settings: ToolSettings): SettingItem[] {
return [
{
id: "usageTool",
label: "Usage Tool",
currentValue: settings.usageTool ? "on" : "off",
values: ["on", "off"],
description: "Expose sub_get_usage/get_current_usage (requires /reload).",
},
{
id: "allUsageTool",
label: "All Usage Tool",
currentValue: settings.allUsageTool ? "on" : "off",
values: ["on", "off"],
description: "Expose sub_get_all_usage/get_all_usage (requires /reload).",
},
];
}
export function applyToolChange(settings: Settings, id: string, value: string): Settings {
const enabled = value === "on";
switch (id) {
case "usageTool":
settings.tools.usageTool = enabled;
break;
case "allUsageTool":
settings.tools.allUsageTool = enabled;
break;
}
return settings;
}
@@ -1,450 +0,0 @@
/**
* Settings UI for sub-core
*/
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
import { Container, Input, type SelectItem, SelectList, Spacer, Text } from "@mariozechner/pi-tui";
import { SettingsList, type SettingItem, CUSTOM_OPTION } from "../ui/settings-list.js";
import type { ProviderName } from "../types.js";
import type { Settings } from "../settings-types.js";
import { getDefaultSettings } from "../settings-types.js";
import { getSettings, saveSettings, resetSettings } from "../settings.js";
import { PROVIDER_DISPLAY_NAMES } from "../providers/metadata.js";
import { buildProviderSettingsItems, applyProviderSettingsChange } from "../providers/settings.js";
import { buildRefreshItems, applyRefreshChange } from "./behavior.js";
import { buildToolItems, applyToolChange } from "./tools.js";
import { buildMainMenuItems, buildProviderListItems, buildProviderOrderItems, type TooltipSelectItem } from "./menu.js";
/**
* Settings category
*/
type ProviderCategory = `provider-${ProviderName}`;
type SettingsCategory =
| "main"
| "providers"
| ProviderCategory
| "behavior"
| "status-refresh"
| "tools"
| "provider-order";
/**
* Extract provider name from category
*/
function getProviderFromCategory(category: SettingsCategory): ProviderName | null {
const match = category.match(/^provider-(\w+)$/);
if (match && match[1] !== "order") {
return match[1] as ProviderName;
}
return null;
}
/**
* Show the settings UI
*/
export async function showSettingsUI(
ctx: ExtensionContext,
onSettingsChange?: (settings: Settings) => void | Promise<void>
): Promise<Settings> {
let settings = getSettings();
let currentCategory: SettingsCategory = "main";
let providerOrderSelectedIndex = 0;
let providerOrderReordering = false;
let suppressProviderOrderChange = false;
return new Promise((resolve) => {
ctx.ui.custom<Settings>((tui, theme, _kb, done) => {
let container = new Container();
let activeList: SelectList | SettingsList | null = null;
const clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));
const buildInputSubmenu = (
label: string,
parseValue: (value: string) => string | null,
formatInitial?: (value: string) => string,
) => {
return (currentValue: string, done: (selectedValue?: string) => void) => {
const input = new Input();
input.focused = true;
input.setValue(formatInitial ? formatInitial("") : "");
input.onSubmit = (value) => {
const parsed = parseValue(value);
if (!parsed) return;
done(parsed);
};
input.onEscape = () => {
done();
};
const inputContainer = new Container();
inputContainer.addChild(new Text(theme.fg("muted", label), 1, 0));
inputContainer.addChild(new Spacer(1));
inputContainer.addChild(input);
return {
render: (width: number) => inputContainer.render(width),
invalidate: () => inputContainer.invalidate(),
handleInput: (data: string) => input.handleInput(data),
};
};
};
const parseRefreshInterval = (raw: string): string | null => {
const trimmed = raw.trim().toLowerCase();
if (!trimmed) {
ctx.ui.notify("Enter a value", "warning");
return null;
}
if (trimmed === "off") return "off";
const cleaned = trimmed.replace(/s$/, "");
const parsed = Number.parseInt(cleaned, 10);
if (Number.isNaN(parsed)) {
ctx.ui.notify("Enter seconds", "warning");
return null;
}
const clamped = parsed <= 0 ? 0 : clamp(parsed, 5, 3600);
return clamped === 0 ? "off" : `${clamped}s`;
};
const parseMinRefreshInterval = (raw: string): string | null => {
const trimmed = raw.trim().toLowerCase();
if (!trimmed) {
ctx.ui.notify("Enter a value", "warning");
return null;
}
if (trimmed === "off") return "off";
const cleaned = trimmed.replace(/s$/, "");
const parsed = Number.parseInt(cleaned, 10);
if (Number.isNaN(parsed)) {
ctx.ui.notify("Enter seconds", "warning");
return null;
}
const clamped = parsed <= 0 ? 0 : clamp(parsed, 5, 3600);
return clamped === 0 ? "off" : `${clamped}s`;
};
const parseCurrencySymbol = (raw: string): string | null => {
const trimmed = raw.trim();
if (!trimmed) {
ctx.ui.notify("Enter a symbol or 'none'", "warning");
return null;
}
if (trimmed.toLowerCase() === "none") return "none";
return trimmed;
};
function rebuild(): void {
container = new Container();
let tooltipText: Text | null = null;
const attachTooltip = (items: TooltipSelectItem[], selectList: SelectList): void => {
if (!items.some((item) => item.tooltip)) return;
const tooltipComponent = new Text("", 1, 0);
const setTooltip = (item?: TooltipSelectItem | null) => {
const tooltip = item?.tooltip?.trim();
tooltipComponent.setText(tooltip ? theme.fg("dim", tooltip) : "");
};
setTooltip(selectList.getSelectedItem() as TooltipSelectItem | null);
const existingHandler = selectList.onSelectionChange;
selectList.onSelectionChange = (item) => {
if (existingHandler) existingHandler(item);
setTooltip(item as TooltipSelectItem);
tui.requestRender();
};
tooltipText = tooltipComponent;
};
// Top border
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
// Title
const titles: Record<string, string> = {
main: "sub-core Settings",
providers: "Provider Settings",
behavior: "Usage Refresh Settings",
"status-refresh": "Status Refresh Settings",
tools: "Tool Settings",
"provider-order": "Provider Order",
};
const providerCategory = getProviderFromCategory(currentCategory);
const title = providerCategory
? `${PROVIDER_DISPLAY_NAMES[providerCategory]} Settings`
: titles[currentCategory] ?? "sub-core Settings";
container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
container.addChild(new Spacer(1));
if (currentCategory === "main") {
const items = buildMainMenuItems(settings);
const selectList = new SelectList(items, Math.min(items.length, 10), {
selectedPrefix: (t: string) => theme.fg("accent", t),
selectedText: (t: string) => theme.fg("accent", t),
description: (t: string) => theme.fg("muted", t),
scrollInfo: (t: string) => theme.fg("dim", t),
noMatch: (t: string) => theme.fg("warning", t),
});
attachTooltip(items, selectList);
selectList.onSelect = (item) => {
if (item.value === "reset") {
settings = resetSettings();
if (onSettingsChange) void onSettingsChange(settings);
ctx.ui.notify("Settings reset to defaults", "info");
rebuild();
tui.requestRender();
} else {
currentCategory = item.value as SettingsCategory;
rebuild();
tui.requestRender();
}
};
selectList.onCancel = () => {
saveSettings(settings);
done(settings);
};
activeList = selectList;
container.addChild(selectList);
} else if (currentCategory === "providers") {
const items = buildProviderListItems(settings);
const selectList = new SelectList(items, Math.min(items.length, 10), {
selectedPrefix: (t: string) => theme.fg("accent", t),
selectedText: (t: string) => theme.fg("accent", t),
description: (t: string) => theme.fg("muted", t),
scrollInfo: (t: string) => theme.fg("dim", t),
noMatch: (t: string) => theme.fg("warning", t),
});
attachTooltip(items, selectList);
selectList.onSelect = (item) => {
currentCategory = item.value as SettingsCategory;
rebuild();
tui.requestRender();
};
selectList.onCancel = () => {
currentCategory = "main";
rebuild();
tui.requestRender();
};
activeList = selectList;
container.addChild(selectList);
} else if (currentCategory === "provider-order") {
const items = buildProviderOrderItems(settings);
const isReordering = providerOrderReordering;
const selectList = new SelectList(items, Math.min(items.length, 10), {
selectedPrefix: (t: string) => isReordering ? theme.fg("warning", t) : theme.fg("accent", t),
selectedText: (t: string) => isReordering ? theme.fg("warning", t) : theme.fg("accent", t),
description: (t: string) => theme.fg("muted", t),
scrollInfo: (t: string) => theme.fg("dim", t),
noMatch: (t: string) => theme.fg("warning", t),
});
if (items.length > 0) {
suppressProviderOrderChange = true;
providerOrderSelectedIndex = Math.min(providerOrderSelectedIndex, items.length - 1);
selectList.setSelectedIndex(providerOrderSelectedIndex);
suppressProviderOrderChange = false;
}
selectList.onSelectionChange = (item) => {
if (suppressProviderOrderChange) return;
const newIndex = items.findIndex((listItem) => listItem.value === item.value);
if (newIndex === -1) return;
if (!providerOrderReordering) {
providerOrderSelectedIndex = newIndex;
return;
}
const activeProviders = settings.providerOrder.filter((provider) => {
const enabled = settings.providers[provider].enabled;
return enabled !== "off" && enabled !== false;
});
const oldIndex = providerOrderSelectedIndex;
if (newIndex === oldIndex) return;
if (oldIndex < 0 || oldIndex >= activeProviders.length) return;
const provider = activeProviders[oldIndex];
const updatedActive = [...activeProviders];
updatedActive.splice(oldIndex, 1);
updatedActive.splice(newIndex, 0, provider);
let activeIndex = 0;
settings.providerOrder = settings.providerOrder.map((existing) => {
const enabled = settings.providers[existing].enabled;
if (enabled === "off" || enabled === false) return existing;
const next = updatedActive[activeIndex];
activeIndex += 1;
return next;
});
providerOrderSelectedIndex = newIndex;
saveSettings(settings);
if (onSettingsChange) void onSettingsChange(settings);
rebuild();
tui.requestRender();
};
attachTooltip(items, selectList);
selectList.onSelect = () => {
if (items.length === 0) return;
providerOrderReordering = !providerOrderReordering;
rebuild();
tui.requestRender();
};
selectList.onCancel = () => {
if (providerOrderReordering) {
providerOrderReordering = false;
rebuild();
tui.requestRender();
return;
}
currentCategory = "main";
rebuild();
tui.requestRender();
};
activeList = selectList;
container.addChild(selectList);
} else {
let items: SettingItem[];
let handleChange: (id: string, value: string) => void;
let backCategory: SettingsCategory = "main";
const provider = getProviderFromCategory(currentCategory);
if (provider) {
items = buildProviderSettingsItems(settings, provider);
const customHandlers: Record<string, ReturnType<typeof buildInputSubmenu>> = {};
if (provider === "anthropic") {
customHandlers.extraUsageCurrencySymbol = buildInputSubmenu(
"Extra Usage Currency Symbol",
parseCurrencySymbol,
undefined,
);
}
for (const item of items) {
if (item.values?.includes(CUSTOM_OPTION) && customHandlers[item.id]) {
item.submenu = customHandlers[item.id];
}
}
handleChange = (id, value) => {
settings = applyProviderSettingsChange(settings, provider, id, value);
saveSettings(settings);
if (onSettingsChange) void onSettingsChange(settings);
};
backCategory = "providers";
} else if (currentCategory === "tools") {
items = buildToolItems(settings.tools);
handleChange = (id, value) => {
settings = applyToolChange(settings, id, value);
saveSettings(settings);
if (onSettingsChange) void onSettingsChange(settings);
};
backCategory = "main";
} else {
const refreshTarget = currentCategory === "status-refresh" ? settings.statusRefresh : settings.behavior;
items = buildRefreshItems(refreshTarget);
const customHandlers: Record<string, ReturnType<typeof buildInputSubmenu>> = {
refreshInterval: buildInputSubmenu("Auto-refresh Interval (seconds)", parseRefreshInterval),
minRefreshInterval: buildInputSubmenu("Minimum Refresh Interval (seconds)", parseMinRefreshInterval),
};
for (const item of items) {
if (item.values?.includes(CUSTOM_OPTION) && customHandlers[item.id]) {
item.submenu = customHandlers[item.id];
}
}
handleChange = (id, value) => {
applyRefreshChange(refreshTarget, id, value);
saveSettings(settings);
if (onSettingsChange) void onSettingsChange(settings);
};
backCategory = "main";
}
const settingsHintText = "↓ navigate • ←/→ change • Enter/Space edit custom • Esc to cancel";
const customTheme = {
...getSettingsListTheme(),
hint: (text: string) => {
if (text.includes("Enter/Space")) {
return theme.fg("dim", settingsHintText);
}
return theme.fg("dim", text);
},
};
const settingsList = new SettingsList(
items,
Math.min(items.length + 2, 15),
customTheme,
handleChange,
() => {
currentCategory = backCategory;
rebuild();
tui.requestRender();
}
);
activeList = settingsList;
container.addChild(settingsList);
}
const usesSettingsList =
currentCategory === "behavior" ||
currentCategory === "status-refresh" ||
currentCategory === "tools" ||
getProviderFromCategory(currentCategory) !== null;
if (!usesSettingsList) {
let helpText: string;
if (currentCategory === "main" || currentCategory === "providers") {
helpText = "↑↓ navigate • Enter/Space select • Esc back";
} else if (currentCategory === "provider-order") {
helpText = providerOrderReordering
? "↑↓ move provider • Esc back"
: "↑↓ navigate • Enter/Space select • Esc back";
} else {
helpText = "↑↓ navigate • Enter/Space to change • Esc to cancel";
}
if (tooltipText) {
container.addChild(new Spacer(1));
container.addChild(tooltipText);
}
container.addChild(new Spacer(1));
container.addChild(new Text(theme.fg("dim", helpText), 1, 0));
}
// Bottom border
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
}
rebuild();
return {
render(width: number) {
return container.render(width);
},
invalidate() {
container.invalidate();
},
handleInput(data: string) {
if (data === " ") {
if (currentCategory === "provider-order") {
providerOrderReordering = !providerOrderReordering;
rebuild();
tui.requestRender();
return;
}
if (activeList && "handleInput" in activeList && activeList.handleInput) {
activeList.handleInput("\r");
}
tui.requestRender();
return;
}
if (activeList && "handleInput" in activeList && activeList.handleInput) {
activeList.handleInput(data);
}
tui.requestRender();
},
};
}).then(resolve);
});
}
@@ -1,245 +0,0 @@
/**
* Status polling for providers
*/
import type { Dependencies, ProviderName, ProviderStatus, StatusIndicator } from "./types.js";
import type { ProviderStatusConfig } from "./providers/metadata.js";
import { GOOGLE_STATUS_URL, GEMINI_PRODUCT_ID, API_TIMEOUT_MS } from "./config.js";
import { PROVIDER_METADATA } from "./providers/metadata.js";
import { createTimeoutController } from "./utils.js";
type StatusPageStatusConfig = Extract<ProviderStatusConfig, { type: "statuspage" }>;
interface StatusPageSummary {
status?: {
indicator?: string;
description?: string;
};
components?: Array<{
id?: string;
name?: string;
status?: string;
}>;
}
interface StatusPageStatus {
indicator?: string;
description?: string;
}
interface StatusPageResponse {
status?: StatusPageStatus;
}
function toSummaryUrl(url: string): string {
if (url.endsWith("/summary.json")) return url;
if (url.endsWith("/status.json")) {
return `${url.slice(0, -"/status.json".length)}/summary.json`;
}
if (!url.endsWith("/")) return `${url}/summary.json`;
return `${url}summary.json`;
}
function isComponentMatch(component: { id?: string; name?: string }, config?: StatusPageStatusConfig): boolean {
if (!config?.component) return false;
if (config.component.id && component.id) {
return component.id === config.component.id;
}
if (config.component.name && component.name) {
return component.name.trim().toLowerCase() === config.component.name.trim().toLowerCase();
}
return false;
}
function mapStatusIndicator(indicator?: string): StatusIndicator {
switch (indicator) {
case "none":
return "none";
case "minor":
return "minor";
case "major":
return "major";
case "critical":
return "critical";
case "maintenance":
return "maintenance";
default:
return "unknown";
}
}
function mapComponentStatus(indicator?: string): StatusIndicator {
switch ((indicator || "").toLowerCase()) {
case "operational":
return "none";
case "under_maintenance":
return "maintenance";
case "degraded_performance":
return "minor";
case "partial_outage":
return "major";
case "major_outage":
return "critical";
default:
return "unknown";
}
}
function formatComponentLabel(rawStatus?: string): string {
switch ((rawStatus || "").toLowerCase()) {
case "operational":
return "Operational";
case "under_maintenance":
return "Under maintenance";
case "degraded_performance":
return "Degraded performance";
case "partial_outage":
return "Partial outage";
case "major_outage":
return "Major outage";
default:
return rawStatus ? rawStatus.replace(/_/g, " ") : "Unknown";
}
}
/**
* Fetch status from a standard statuspage.io API
*/
async function fetchStatuspageStatus(
url: string,
deps: Dependencies,
config?: StatusPageStatusConfig
): Promise<ProviderStatus> {
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const fetchUrl = config?.component ? toSummaryUrl(url) : url;
const res = await deps.fetch(fetchUrl, { signal: controller.signal });
clear();
if (!res.ok) {
return { indicator: "unknown" };
}
if (!config?.component) {
const data = (await res.json()) as StatusPageResponse;
const indicator = mapStatusIndicator(data.status?.indicator);
return { indicator, description: data.status?.description };
}
const data = (await res.json()) as StatusPageSummary;
const summaryIndicator = mapStatusIndicator(data.status?.indicator);
const component = (data.components ?? []).find((entry) => isComponentMatch(entry, config));
if (component) {
const componentIndicator = mapComponentStatus(component.status);
const componentDescription =
componentIndicator === "none"
? undefined
: `${component.name ?? "Component"}: ${formatComponentLabel(component.status)}`;
return {
indicator: componentIndicator,
description: componentDescription,
};
}
return {
indicator: summaryIndicator,
description: data.status?.description,
};
} catch {
clear();
return { indicator: "unknown" };
}
}
/**
* Fetch Gemini status from Google Workspace status API
*/
async function fetchGeminiStatus(deps: Dependencies): Promise<ProviderStatus> {
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
try {
const res = await deps.fetch(GOOGLE_STATUS_URL, { signal: controller.signal });
clear();
if (!res.ok) return { indicator: "unknown" };
const incidents = (await res.json()) as Array<{
end?: string;
currently_affected_products?: Array<{ id: string }>;
affected_products?: Array<{ id: string }>;
most_recent_update?: { status?: string };
status_impact?: string;
external_desc?: string;
}>;
const activeIncidents = incidents.filter((inc) => {
if (inc.end) return false;
const affected = inc.currently_affected_products || inc.affected_products || [];
return affected.some((p) => p.id === GEMINI_PRODUCT_ID);
});
if (activeIncidents.length === 0) {
return { indicator: "none" };
}
let worstIndicator: StatusIndicator = "minor";
let description: string | undefined;
for (const inc of activeIncidents) {
const status = inc.most_recent_update?.status || inc.status_impact;
if (status === "SERVICE_OUTAGE") {
worstIndicator = "critical";
description = inc.external_desc;
} else if (status === "SERVICE_DISRUPTION" && worstIndicator !== "critical") {
worstIndicator = "major";
description = inc.external_desc;
}
}
return { indicator: worstIndicator, description };
} catch {
clear();
return { indicator: "unknown" };
}
}
/**
* Fetch status for a provider
*/
export async function fetchProviderStatus(provider: ProviderName, deps: Dependencies): Promise<ProviderStatus> {
const statusConfig = PROVIDER_METADATA[provider]?.status;
if (!statusConfig) {
return { indicator: "none" };
}
if (statusConfig.type === "google-workspace") {
return fetchGeminiStatus(deps);
}
return fetchStatuspageStatus(statusConfig.url, deps, statusConfig);
}
/**
* Get emoji for a status indicator
*/
export function getStatusEmoji(status?: ProviderStatus): string {
if (!status) return "";
switch (status.indicator) {
case "none":
return "✅";
case "minor":
return "⚠️";
case "major":
return "🟠";
case "critical":
return "🔴";
case "maintenance":
return "🔧";
default:
return "";
}
}
@@ -1,61 +0,0 @@
/**
* Storage abstraction for settings and cache persistence.
*/
import * as fs from "node:fs";
import * as path from "node:path";
export interface StorageAdapter {
readFile(path: string): string | undefined;
writeFile(path: string, contents: string): void;
writeFileExclusive(path: string, contents: string): boolean;
exists(path: string): boolean;
removeFile(path: string): void;
ensureDir(path: string): void;
}
export function createFsStorage(): StorageAdapter {
return {
readFile(filePath: string): string | undefined {
try {
return fs.readFileSync(filePath, "utf-8");
} catch {
return undefined;
}
},
writeFile(filePath: string, contents: string): void {
fs.writeFileSync(filePath, contents, "utf-8");
},
writeFileExclusive(filePath: string, contents: string): boolean {
try {
fs.writeFileSync(filePath, contents, { flag: "wx" });
return true;
} catch {
return false;
}
},
exists(filePath: string): boolean {
return fs.existsSync(filePath);
},
removeFile(filePath: string): void {
try {
fs.unlinkSync(filePath);
} catch {
// Ignore remove errors
}
},
ensureDir(dirPath: string): void {
fs.mkdirSync(path.resolve(dirPath), { recursive: true });
},
};
}
let activeStorage: StorageAdapter = createFsStorage();
export function getStorage(): StorageAdapter {
return activeStorage;
}
export function setStorage(storage: StorageAdapter): void {
activeStorage = storage;
}
@@ -1,60 +0,0 @@
/**
* File lock helpers for storage-backed locks.
*/
import { getStorage } from "../storage.js";
export function tryAcquireFileLock(lockPath: string, staleAfterMs: number): boolean {
const storage = getStorage();
try {
if (storage.writeFileExclusive(lockPath, String(Date.now()))) {
return true;
}
} catch {
// ignore
}
try {
if (storage.exists(lockPath)) {
const lockContent = storage.readFile(lockPath) ?? "";
const lockTime = parseInt(lockContent, 10);
if (Date.now() - lockTime > staleAfterMs) {
storage.writeFile(lockPath, String(Date.now()));
return true;
}
}
} catch {
// Ignore, lock is held by another process
}
return false;
}
export function releaseFileLock(lockPath: string): void {
const storage = getStorage();
try {
if (storage.exists(lockPath)) {
storage.removeFile(lockPath);
}
} catch {
// Ignore
}
}
export async function waitForLockRelease(
lockPath: string,
maxWaitMs: number,
pollMs: number = 100
): Promise<boolean> {
const storage = getStorage();
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
await new Promise((resolve) => setTimeout(resolve, pollMs));
if (!storage.exists(lockPath)) {
return true;
}
}
return false;
}
@@ -1,33 +0,0 @@
/**
* Core types for the sub-bar extension
*/
import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
export type {
ProviderName,
StatusIndicator,
ProviderStatus,
RateWindow,
UsageSnapshot,
UsageError,
UsageErrorCode,
ProviderUsageEntry,
SubCoreState,
SubCoreEvents,
} from "../../src/shared.js";
export { PROVIDERS } from "../../src/shared.js";
/**
* Dependencies that can be injected for testing
*/
export interface Dependencies {
fetch: typeof globalThis.fetch;
readFile: (path: string) => string | undefined;
fileExists: (path: string) => boolean;
// Use static commands/args only (no user-controlled input).
execFileSync: (file: string, args: string[], options?: ExecFileSyncOptionsWithStringEncoding) => string;
homedir: () => string;
env: NodeJS.ProcessEnv;
}
@@ -1,290 +0,0 @@
import type { Component, SettingItem, SettingsListTheme } from "@mariozechner/pi-tui";
import {
Input,
fuzzyFilter,
getEditorKeybindings,
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from "@mariozechner/pi-tui";
export interface SettingsListOptions {
enableSearch?: boolean;
}
export const CUSTOM_OPTION = "__custom__";
export const CUSTOM_LABEL = "custom";
export type { SettingItem, SettingsListTheme };
export class SettingsList implements Component {
private items: SettingItem[];
private filteredItems: SettingItem[];
private theme: SettingsListTheme;
private selectedIndex = 0;
private maxVisible: number;
private onChange: (id: string, newValue: string) => void;
private onCancel: () => void;
private searchInput?: Input;
private searchEnabled: boolean;
private submenuComponent: Component | null = null;
private submenuItemIndex: number | null = null;
constructor(
items: SettingItem[],
maxVisible: number,
theme: SettingsListTheme,
onChange: (id: string, newValue: string) => void,
onCancel: () => void,
options: SettingsListOptions = {},
) {
this.items = items;
this.filteredItems = items;
this.maxVisible = maxVisible;
this.theme = theme;
this.onChange = onChange;
this.onCancel = onCancel;
this.searchEnabled = options.enableSearch ?? false;
if (this.searchEnabled) {
this.searchInput = new Input();
}
}
/** Update an item's currentValue */
updateValue(id: string, newValue: string): void {
const item = this.items.find((i) => i.id === id);
if (item) {
item.currentValue = newValue;
}
}
invalidate(): void {
this.submenuComponent?.invalidate?.();
}
render(width: number): string[] {
// If submenu is active, render it instead
if (this.submenuComponent) {
return this.submenuComponent.render(width);
}
return this.renderMainList(width);
}
private renderMainList(width: number): string[] {
const lines: string[] = [];
if (this.searchEnabled && this.searchInput) {
lines.push(...this.searchInput.render(width));
lines.push("");
}
if (this.items.length === 0) {
lines.push(this.theme.hint(" No settings available"));
if (this.searchEnabled) {
this.addHintLine(lines);
}
return lines;
}
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
if (displayItems.length === 0) {
lines.push(this.theme.hint(" No matching settings"));
this.addHintLine(lines);
return lines;
}
// Calculate visible range with scrolling
const startIndex = Math.max(
0,
Math.min(
this.selectedIndex - Math.floor(this.maxVisible / 2),
displayItems.length - this.maxVisible,
),
);
const endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);
// Calculate max label width for alignment
const maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));
// Render visible items
for (let i = startIndex; i < endIndex; i++) {
const item = displayItems[i];
if (!item) continue;
const isSelected = i === this.selectedIndex;
const prefix = isSelected ? this.theme.cursor : " ";
const prefixWidth = visibleWidth(prefix);
// Pad label to align values
const labelPadded = item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
const labelText = this.theme.label(labelPadded, isSelected);
// Calculate space for value
const separator = " ";
const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);
const valueMaxWidth = Math.max(1, width - usedWidth - 2);
const optionLines = isSelected && item.values && item.values.length > 0
? wrapTextWithAnsi(this.formatOptionsInline(item, item.values), valueMaxWidth)
: null;
const valueText = optionLines
? optionLines[0] ?? ""
: this.theme.value(truncateToWidth(item.currentValue, valueMaxWidth, ""), isSelected);
const line = prefix + labelText + separator + valueText;
lines.push(truncateToWidth(line, width, ""));
if (optionLines && optionLines.length > 1) {
const indent = " ".repeat(prefixWidth + maxLabelWidth + visibleWidth(separator));
for (const continuation of optionLines.slice(1)) {
lines.push(truncateToWidth(indent + continuation, width, ""));
}
}
}
// Add scroll indicator if needed
if (startIndex > 0 || endIndex < displayItems.length) {
const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;
lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, "")));
}
// Add description for selected item
const selectedItem = displayItems[this.selectedIndex];
if (selectedItem?.description) {
lines.push("");
const wrapWidth = Math.max(1, width - 4);
const wrappedDesc = wrapTextWithAnsi(selectedItem.description, wrapWidth);
for (const line of wrappedDesc) {
const prefixed = ` ${line}`;
lines.push(this.theme.description(truncateToWidth(prefixed, width, "")));
}
}
// Add hint
this.addHintLine(lines);
return lines;
}
handleInput(data: string): void {
// If submenu is active, delegate all input to it
// The submenu's onCancel (triggered by escape) will call done() which closes it
if (this.submenuComponent) {
this.submenuComponent.handleInput?.(data);
return;
}
const kb = getEditorKeybindings();
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
if (kb.matches(data, "selectUp")) {
if (displayItems.length === 0) return;
this.selectedIndex = this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;
} else if (kb.matches(data, "selectDown")) {
if (displayItems.length === 0) return;
this.selectedIndex = this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;
} else if (kb.matches(data, "cursorLeft")) {
this.stepValue(-1);
} else if (kb.matches(data, "cursorRight")) {
this.stepValue(1);
} else if (kb.matches(data, "selectConfirm") || data === " ") {
this.activateItem();
} else if (kb.matches(data, "selectCancel")) {
this.onCancel();
} else if (this.searchEnabled && this.searchInput) {
const sanitized = data.replace(/ /g, "");
if (!sanitized) {
return;
}
this.searchInput.handleInput(sanitized);
this.applyFilter(this.searchInput.getValue());
}
}
private stepValue(direction: -1 | 1): void {
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
const item = displayItems[this.selectedIndex];
if (!item || !item.values || item.values.length === 0) return;
const values = item.values;
let currentIndex = values.indexOf(item.currentValue);
if (currentIndex === -1) {
currentIndex = direction > 0 ? 0 : values.length - 1;
}
const nextIndex = (currentIndex + direction + values.length) % values.length;
const newValue = values[nextIndex];
if (newValue === CUSTOM_OPTION) {
item.currentValue = newValue;
this.onChange(item.id, newValue);
return;
}
item.currentValue = newValue;
this.onChange(item.id, newValue);
}
private activateItem(): void {
const item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];
if (!item) return;
const hasCustom = Boolean(item.values && item.values.includes(CUSTOM_OPTION));
const currentIsCustom = hasCustom && item.values && !item.values.includes(item.currentValue);
if (item.submenu && hasCustom) {
if (currentIsCustom || item.currentValue === CUSTOM_OPTION) {
this.openSubmenu(item);
}
return;
}
if (item.submenu) {
this.openSubmenu(item);
}
}
private closeSubmenu(): void {
this.submenuComponent = null;
// Restore selection to the item that opened the submenu
if (this.submenuItemIndex !== null) {
this.selectedIndex = this.submenuItemIndex;
this.submenuItemIndex = null;
}
}
private applyFilter(query: string): void {
this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
this.selectedIndex = 0;
}
private formatOptionsInline(item: SettingItem, values: string[]): string {
const separator = this.theme.description(" • ");
const hasCustom = values.includes(CUSTOM_OPTION);
const currentIsCustom = hasCustom && !values.includes(item.currentValue);
return values
.map((value) => {
const label = value === CUSTOM_OPTION
? (currentIsCustom ? `${CUSTOM_LABEL} (${item.currentValue})` : CUSTOM_LABEL)
: value;
const selected = value === item.currentValue || (currentIsCustom && value === CUSTOM_OPTION);
return this.theme.value(label, selected);
})
.join(separator);
}
private openSubmenu(item: SettingItem): void {
if (!item.submenu) return;
this.submenuItemIndex = this.selectedIndex;
this.submenuComponent = item.submenu(item.currentValue, (selectedValue) => {
if (selectedValue !== undefined) {
item.currentValue = selectedValue;
this.onChange(item.id, selectedValue);
}
this.closeSubmenu();
});
}
private addHintLine(lines: string[]): void {
lines.push("");
lines.push(
this.theme.hint(
this.searchEnabled
? " Type to search · ←/→ change · Enter/Space edit custom · Esc to cancel"
: " ←/→ change · Enter/Space edit custom · Esc to cancel",
),
);
}
}
@@ -1,250 +0,0 @@
/**
* Usage refresh and provider selection controller.
*/
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
import type { ProviderName, UsageSnapshot } from "../types.js";
import type { Settings } from "../settings-types.js";
import { detectProviderFromModel } from "../providers/detection.js";
import { isExpectedMissingData } from "../errors.js";
import { formatElapsedSince } from "../utils.js";
import { fetchUsageForProvider, refreshStatusForProvider } from "./fetch.js";
import type { Dependencies } from "../types.js";
import { getCachedData, readCache } from "../cache.js";
import { hasProviderCredentials } from "../providers/registry.js";
export interface UsageControllerState {
currentProvider?: ProviderName;
cachedUsage?: UsageSnapshot;
lastSuccessAt?: number;
providerCycleIndex: number;
}
export interface UsageUpdate {
provider?: ProviderName;
usage?: UsageSnapshot;
}
export type UsageUpdateHandler = (update: UsageUpdate) => void;
export function createUsageController(deps: Dependencies) {
function isProviderAvailable(
settings: Settings,
provider: ProviderName,
options?: { skipCredentials?: boolean }
): boolean {
const setting = settings.providers[provider];
if (setting.enabled === "off" || setting.enabled === false) return false;
if (setting.enabled === "on" || setting.enabled === true) return true;
if (options?.skipCredentials) return true;
return hasProviderCredentials(provider, deps);
}
function getEnabledProviders(settings: Settings): ProviderName[] {
return settings.providerOrder.filter((p) => isProviderAvailable(settings, p));
}
function resolveProvider(
ctx: ExtensionContext,
settings: Settings,
state: UsageControllerState,
options?: { skipCredentials?: boolean }
): ProviderName | undefined {
const detected = detectProviderFromModel(ctx.model);
if (detected && isProviderAvailable(settings, detected, options)) {
return detected;
}
return undefined;
}
function emitUpdate(state: UsageControllerState, onUpdate: UsageUpdateHandler): void {
onUpdate({
provider: state.currentProvider,
usage: state.cachedUsage,
});
}
async function refresh(
ctx: ExtensionContext,
settings: Settings,
state: UsageControllerState,
onUpdate: UsageUpdateHandler,
options?: { force?: boolean; allowStaleCache?: boolean; forceStatus?: boolean; skipFetch?: boolean }
): Promise<void> {
const provider = resolveProvider(ctx, settings, state, { skipCredentials: options?.skipFetch });
if (!provider) {
state.currentProvider = undefined;
state.cachedUsage = undefined;
emitUpdate(state, onUpdate);
return;
}
const providerChanged = provider !== state.currentProvider;
state.currentProvider = provider;
if (providerChanged) {
state.cachedUsage = undefined;
}
const cache = readCache();
let cachedEntry = await getCachedData(provider, settings.behavior.refreshInterval * 1000, cache);
if (!cachedEntry && options?.allowStaleCache) {
cachedEntry = cache[provider] ?? null;
}
if (cachedEntry?.usage) {
state.cachedUsage = {
...cachedEntry.usage,
status: cachedEntry.status,
lastSuccessAt: cachedEntry.fetchedAt,
};
if (!cachedEntry.usage.error) {
state.lastSuccessAt = cachedEntry.fetchedAt;
}
}
emitUpdate(state, onUpdate);
if (options?.skipFetch) {
return;
}
const result = await fetchUsageForProvider(deps, settings, provider, options);
const error = result.usage?.error;
const fetchError = Boolean(error && !isExpectedMissingData(error));
if (fetchError) {
let fallback = state.cachedUsage;
let fallbackFetchedAt = state.lastSuccessAt;
if (!fallback || fallback.windows.length === 0) {
const cachedEntry = cache[provider];
const cachedUsage = cachedEntry?.usage ? { ...cachedEntry.usage, status: cachedEntry.status } : undefined;
fallback = cachedUsage && cachedUsage.windows.length > 0 ? cachedUsage : undefined;
if (cachedEntry?.fetchedAt) fallbackFetchedAt = cachedEntry.fetchedAt;
}
if (fallback && fallback.windows.length > 0) {
const lastSuccessAt = fallbackFetchedAt ?? state.lastSuccessAt;
const elapsed = lastSuccessAt ? formatElapsedSince(lastSuccessAt) : undefined;
const description = elapsed ? (elapsed === "just now" ? "just now" : `${elapsed} ago`) : "Fetch failed";
state.cachedUsage = {
...fallback,
lastSuccessAt,
error,
status: { indicator: "minor", description },
};
} else {
state.cachedUsage = result.usage ? { ...result.usage, status: result.status } : undefined;
}
} else {
const successAt = Date.now();
state.cachedUsage = result.usage
? { ...result.usage, status: result.status, lastSuccessAt: successAt }
: undefined;
if (result.usage && !result.usage.error) {
state.lastSuccessAt = successAt;
}
}
emitUpdate(state, onUpdate);
}
async function refreshStatus(
ctx: ExtensionContext,
settings: Settings,
state: UsageControllerState,
onUpdate: UsageUpdateHandler,
options?: { force?: boolean; allowStaleCache?: boolean; skipFetch?: boolean }
): Promise<void> {
const provider = resolveProvider(ctx, settings, state, { skipCredentials: options?.skipFetch });
if (!provider) {
state.currentProvider = undefined;
state.cachedUsage = undefined;
emitUpdate(state, onUpdate);
return;
}
const providerChanged = provider !== state.currentProvider;
state.currentProvider = provider;
if (providerChanged) {
state.cachedUsage = undefined;
}
const cache = readCache();
let cachedEntry = await getCachedData(provider, settings.behavior.refreshInterval * 1000, cache);
if (!cachedEntry && options?.allowStaleCache) {
cachedEntry = cache[provider] ?? null;
}
if (cachedEntry?.usage) {
state.cachedUsage = {
...cachedEntry.usage,
status: cachedEntry.status,
lastSuccessAt: cachedEntry.fetchedAt,
};
if (!cachedEntry.usage.error) {
state.lastSuccessAt = cachedEntry.fetchedAt;
}
}
if (options?.skipFetch) {
emitUpdate(state, onUpdate);
return;
}
const status = await refreshStatusForProvider(deps, settings, provider, { force: options?.force });
if (status && state.cachedUsage) {
state.cachedUsage = { ...state.cachedUsage, status };
}
emitUpdate(state, onUpdate);
}
async function cycleProvider(
ctx: ExtensionContext,
settings: Settings,
state: UsageControllerState,
onUpdate: UsageUpdateHandler
): Promise<void> {
const enabledProviders = getEnabledProviders(settings);
if (enabledProviders.length === 0) {
state.currentProvider = undefined;
state.cachedUsage = undefined;
emitUpdate(state, onUpdate);
return;
}
const currentIndex = state.currentProvider
? enabledProviders.indexOf(state.currentProvider)
: -1;
if (currentIndex >= 0) {
state.providerCycleIndex = currentIndex;
}
const total = enabledProviders.length;
for (let i = 0; i < total; i += 1) {
state.providerCycleIndex = (state.providerCycleIndex + 1) % total;
const nextProvider = enabledProviders[state.providerCycleIndex];
const result = await fetchUsageForProvider(deps, settings, nextProvider);
if (!isUsageAvailable(result.usage)) {
continue;
}
state.currentProvider = nextProvider;
state.cachedUsage = result.usage ? { ...result.usage, status: result.status } : undefined;
emitUpdate(state, onUpdate);
return;
}
state.currentProvider = undefined;
state.cachedUsage = undefined;
emitUpdate(state, onUpdate);
}
function isUsageAvailable(usage: UsageSnapshot | undefined): usage is UsageSnapshot {
if (!usage) return false;
if (usage.windows.length > 0) return true;
if (!usage.error) return false;
return !isExpectedMissingData(usage.error);
}
return {
getEnabledProviders,
resolveProvider,
refresh,
refreshStatus,
cycleProvider,
};
}
@@ -1,215 +0,0 @@
/**
* Usage fetching helpers with cache integration.
*/
import type { Dependencies, ProviderName, ProviderStatus, UsageSnapshot } from "../types.js";
import type { Settings } from "../settings-types.js";
import type { ProviderUsageEntry } from "./types.js";
import { createProvider } from "../providers/registry.js";
import { fetchWithCache, getCachedData, readCache, updateCacheStatus, type Cache } from "../cache.js";
import { fetchProviderStatusWithFallback, providerHasStatus } from "../providers/status.js";
import { hasProviderCredentials } from "../providers/registry.js";
import { isExpectedMissingData } from "../errors.js";
export function getCacheTtlMs(settings: Settings): number {
return settings.behavior.refreshInterval * 1000;
}
export function getMinRefreshIntervalMs(settings: Settings): number {
return settings.behavior.minRefreshInterval * 1000;
}
export function getStatusCacheTtlMs(settings: Settings): number {
return settings.statusRefresh.refreshInterval * 1000;
}
export function getStatusMinRefreshIntervalMs(settings: Settings): number {
return settings.statusRefresh.minRefreshInterval * 1000;
}
const PROVIDER_FETCH_CONCURRENCY = 3;
async function mapWithConcurrency<T, R>(
items: T[],
limit: number,
mapper: (item: T, index: number) => Promise<R>
): Promise<R[]> {
if (items.length === 0) return [];
const results = new Array<R>(items.length);
let nextIndex = 0;
const workerCount = Math.min(limit, items.length);
const workers = Array.from({ length: workerCount }, async () => {
while (true) {
const currentIndex = nextIndex++;
if (currentIndex >= items.length) {
return;
}
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
}
});
await Promise.all(workers);
return results;
}
function resolveStatusFetchedAt(entry?: { fetchedAt: number; statusFetchedAt?: number } | null): number | undefined {
if (!entry) return undefined;
return entry.statusFetchedAt ?? entry.fetchedAt;
}
function isWithinMinInterval(fetchedAt: number | undefined, minIntervalMs: number): boolean {
if (!fetchedAt || minIntervalMs <= 0) return false;
return Date.now() - fetchedAt < minIntervalMs;
}
function shouldRefreshStatus(
settings: Settings,
entry?: { fetchedAt: number; statusFetchedAt?: number } | null,
options?: { force?: boolean }
): boolean {
const fetchedAt = resolveStatusFetchedAt(entry);
const minIntervalMs = getStatusMinRefreshIntervalMs(settings);
if (isWithinMinInterval(fetchedAt, minIntervalMs)) return false;
if (options?.force) return true;
const ttlMs = getStatusCacheTtlMs(settings);
if (ttlMs <= 0) return true;
if (!fetchedAt) return true;
return Date.now() - fetchedAt >= ttlMs;
}
export async function refreshStatusForProvider(
deps: Dependencies,
settings: Settings,
provider: ProviderName,
options?: { force?: boolean }
): Promise<ProviderStatus | undefined> {
const enabledSetting = settings.providers[provider].enabled;
if (enabledSetting === "off" || enabledSetting === false) {
return undefined;
}
if (enabledSetting === "auto" && !hasProviderCredentials(provider, deps)) {
return undefined;
}
if (!settings.providers[provider].fetchStatus) {
return undefined;
}
const cache = readCache();
const entry = cache[provider];
const providerInstance = createProvider(provider);
const shouldFetch = providerHasStatus(provider, providerInstance) && shouldRefreshStatus(settings, entry, options);
if (!shouldFetch) {
return entry?.status;
}
const status = await fetchProviderStatusWithFallback(provider, providerInstance, deps);
await updateCacheStatus(provider, status, { statusFetchedAt: Date.now() });
return status;
}
export async function fetchUsageForProvider(
deps: Dependencies,
settings: Settings,
provider: ProviderName,
options?: { force?: boolean; forceStatus?: boolean }
): Promise<{ usage?: UsageSnapshot; status?: ProviderStatus }> {
const enabledSetting = settings.providers[provider].enabled;
if (enabledSetting === "off" || enabledSetting === false) {
return {};
}
if (enabledSetting === "auto" && !hasProviderCredentials(provider, deps)) {
return {};
}
const ttlMs = getCacheTtlMs(settings);
const cache = readCache();
const cachedEntry = cache[provider];
const cachedStatus = cachedEntry?.status;
const minIntervalMs = getMinRefreshIntervalMs(settings);
if (cachedEntry?.usage && isWithinMinInterval(cachedEntry.fetchedAt, minIntervalMs)) {
const usage = { ...cachedEntry.usage, status: cachedEntry.status } as UsageSnapshot;
return { usage, status: cachedEntry.status };
}
const providerInstance = createProvider(provider);
const shouldFetchStatus = Boolean(options?.forceStatus)
&& settings.providers[provider].fetchStatus
&& providerHasStatus(provider, providerInstance);
if (!options?.force) {
const cachedUsage = await getCachedData(provider, ttlMs, cache);
if (cachedUsage) {
let status = cachedUsage.status;
if (shouldFetchStatus) {
status = await refreshStatusForProvider(deps, settings, provider, { force: options?.forceStatus ?? options?.force });
}
const usage = cachedUsage.usage ? { ...cachedUsage.usage, status } : undefined;
return { usage, status };
}
}
return fetchWithCache(
provider,
ttlMs,
async () => {
const usage = await providerInstance.fetchUsage(deps);
let status = cachedStatus;
let statusFetchedAt = resolveStatusFetchedAt(cachedEntry);
if (shouldFetchStatus) {
status = await fetchProviderStatusWithFallback(provider, providerInstance, deps);
statusFetchedAt = Date.now();
} else if (!status) {
status = { indicator: "none" as const };
}
return { usage, status, statusFetchedAt };
},
options,
);
}
export async function getCachedUsageEntry(
provider: ProviderName,
settings: Settings,
cacheSnapshot?: Cache
): Promise<ProviderUsageEntry | undefined> {
const ttlMs = getCacheTtlMs(settings);
const cachedEntry = await getCachedData(provider, ttlMs, cacheSnapshot);
const usage = cachedEntry?.usage ? { ...cachedEntry.usage, status: cachedEntry.status } : undefined;
if (!usage || (usage.error && isExpectedMissingData(usage.error))) {
return undefined;
}
return { provider, usage };
}
export async function getCachedUsageEntries(
providers: ProviderName[],
settings: Settings
): Promise<ProviderUsageEntry[]> {
const cache = readCache();
const entries: ProviderUsageEntry[] = [];
for (const provider of providers) {
const entry = await getCachedUsageEntry(provider, settings, cache);
if (entry) {
entries.push(entry);
}
}
return entries;
}
export async function fetchUsageEntries(
deps: Dependencies,
settings: Settings,
providers: ProviderName[],
options?: { force?: boolean }
): Promise<ProviderUsageEntry[]> {
const concurrency = Math.max(1, Math.min(PROVIDER_FETCH_CONCURRENCY, providers.length));
const results = await mapWithConcurrency(providers, concurrency, async (provider) => {
const result = await fetchUsageForProvider(deps, settings, provider, options);
const usage = result.usage
? ({ ...result.usage, status: result.status } as UsageSnapshot)
: undefined;
if (!usage || (usage.error && isExpectedMissingData(usage.error))) {
return undefined;
}
return { provider, usage } as ProviderUsageEntry;
});
return results.filter(Boolean) as ProviderUsageEntry[];
}
@@ -1,5 +0,0 @@
/**
* Usage data types shared across modules.
*/
export type { ProviderUsageEntry } from "../../../src/shared.js";
@@ -1,122 +0,0 @@
/**
* Utility functions for the sub-bar extension
*/
import type { Dependencies } from "./types.js";
import { MODEL_MULTIPLIERS } from "./config.js";
// Only allow simple CLI names (no spaces/paths) to avoid unsafe command execution.
const SAFE_CLI_NAME = /^[a-zA-Z0-9._-]+$/;
/**
* Format a reset date as a relative time string
*/
export function formatReset(date: Date): string {
const diffMs = date.getTime() - Date.now();
if (diffMs < 0) return "now";
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m`;
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
if (hours < 24) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
const days = Math.floor(hours / 24);
const remHours = hours % 24;
return remHours > 0 ? `${days}d${remHours}h` : `${days}d`;
}
/**
* Format elapsed time since a timestamp (milliseconds)
*/
export function formatElapsedSince(timestamp: number): string {
const diffMs = Date.now() - timestamp;
if (diffMs < 60000) return "just now";
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m`;
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
if (hours < 24) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
const days = Math.floor(hours / 24);
const remHours = hours % 24;
return remHours > 0 ? `${days}d${remHours}h` : `${days}d`;
}
/**
* Strip ANSI escape codes from a string
*/
export function stripAnsi(text: string): string {
return text.replace(/\x1B\[[0-9;?]*[A-Za-z]|\x1B\].*?\x07/g, "");
}
/**
* Normalize a string into tokens for fuzzy matching
*/
export function normalizeTokens(value: string): string[] {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
.trim()
.split(" ")
.filter(Boolean);
}
// Pre-computed token entries for model multiplier matching
const MODEL_MULTIPLIER_TOKENS = Object.entries(MODEL_MULTIPLIERS).map(([label, multiplier]) => ({
label,
multiplier,
tokens: normalizeTokens(label),
}));
/**
* Get the request multiplier for a model ID
* Uses fuzzy matching against known model names
*/
export function getModelMultiplier(modelId: string | undefined): number | undefined {
if (!modelId) return undefined;
const modelTokens = normalizeTokens(modelId);
if (modelTokens.length === 0) return undefined;
let bestMatch: { multiplier: number; tokenCount: number } | undefined;
for (const entry of MODEL_MULTIPLIER_TOKENS) {
const isMatch = entry.tokens.every((token) => modelTokens.includes(token));
if (!isMatch) continue;
const tokenCount = entry.tokens.length;
if (!bestMatch || tokenCount > bestMatch.tokenCount) {
bestMatch = { multiplier: entry.multiplier, tokenCount };
}
}
return bestMatch?.multiplier;
}
/**
* Check if a command exists in PATH
*/
export function whichSync(cmd: string, deps: Dependencies): string | null {
if (!SAFE_CLI_NAME.test(cmd)) {
return null;
}
try {
return deps.execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
} catch {
return null;
}
}
/**
* Create an abort controller with a timeout
*/
export function createTimeoutController(timeoutMs: number): { controller: AbortController; clear: () => void } {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
return {
controller,
clear: () => clearTimeout(timeoutId),
};
}
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["index.ts", "src/**/*.ts"],
"exclude": ["node_modules"]
}
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["index.ts", "shared.ts", "src/**/*.ts"],
"exclude": ["node_modules"]
}