diff --git a/pi/files.linux/agent/settings.json b/pi/files.linux/agent/settings.json index a030c1d..b97d588 100644 --- a/pi/files.linux/agent/settings.json +++ b/pi/files.linux/agent/settings.json @@ -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": { diff --git a/pi/files/agent/extensions/sub-bar/index.ts b/pi/files/agent/extensions/sub-bar/index.ts deleted file mode 100644 index 2203d58..0000000 --- a/pi/files/agent/extensions/sub-bar/index.ts +++ /dev/null @@ -1,1030 +0,0 @@ -/** - * sub-bar - Usage Widget Extension - * Shows current provider's usage in a widget above the editor. - * Only shows stats for the currently selected provider. - */ - -import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@mariozechner/pi-coding-agent"; -import { Container, Input, SelectList, Spacer, Text, truncateToWidth, wrapTextWithAnsi, visibleWidth } from "@mariozechner/pi-tui"; -import * as fs from "node:fs"; -import { homedir, tmpdir } from "node:os"; -import { join } from "node:path"; -import type { ProviderName, ProviderUsageEntry, SubCoreAllState, SubCoreState, UsageSnapshot } from "./src/types.js"; -import { type Settings, type BaseTextColor } from "./src/settings-types.js"; -import { isBackgroundColor, resolveBaseTextColor, resolveDividerColor } from "./src/settings-types.js"; -import { buildDividerLine } from "./src/dividers.js"; -import type { CoreSettings } from "./src/shared.js"; -import type { KeyId } from "@mariozechner/pi-tui"; -import { formatUsageStatus, formatUsageStatusWithWidth } from "./src/formatting.js"; -import type { ContextInfo } from "./src/formatting.js"; -import { clearSettingsCache, loadSettings, saveSettings, SETTINGS_PATH } from "./src/settings.js"; -import { showSettingsUI } from "./src/settings-ui.js"; -import { decodeDisplayShareString } from "./src/share.js"; -import { upsertDisplayTheme } from "./src/settings/themes.js"; -import { getFallbackCoreSettings } from "./src/core-settings.js"; - -type SubCoreRequest = - | { - type?: "current"; - includeSettings?: boolean; - reply: (payload: { state: SubCoreState; settings?: CoreSettings }) => void; - } - | { - type: "entries"; - force?: boolean; - reply: (payload: { entries: ProviderUsageEntry[] }) => void; - }; - -type SubCoreAction = { - type: "refresh" | "cycleProvider"; - force?: boolean; -}; - -function applyBackground(lines: string[], theme: Theme, color: BaseTextColor, width: number): string[] { - const bgAnsi = isBackgroundColor(color) - ? theme.getBgAnsi(color as Parameters[0]) - : theme.getFgAnsi(resolveDividerColor(color)).replace(/\x1b\[38;/g, "\x1b[48;").replace(/\x1b\[39m/g, "\x1b[49m"); - if (!bgAnsi || bgAnsi === "\x1b[49m") return lines; - return lines.map((line) => { - const padding = Math.max(0, width - visibleWidth(line)); - return `${bgAnsi}${line}${" ".repeat(padding)}\x1b[49m`; - }); -} - -function applyBaseTextColor(theme: Theme, color: BaseTextColor, text: string): string { - if (isBackgroundColor(color)) { - const fgAnsi = theme - .getBgAnsi(color as Parameters[0]) - .replace(/\x1b\[48;/g, "\x1b[38;") - .replace(/\x1b\[49m/g, "\x1b[39m"); - return `${fgAnsi}${text}\x1b[39m`; - } - return theme.fg(resolveDividerColor(color), text); -} - -type PiSettings = { - enabledModels?: unknown; -}; - -const AGENT_SETTINGS_ENV = "PI_CODING_AGENT_DIR"; -const DEFAULT_AGENT_DIR = join(homedir(), ".pi", "agent"); -const PROJECT_SETTINGS_DIR = ".pi"; -const SETTINGS_FILE_NAME = "settings.json"; - -let scopedModelPatternsCache: { cwd: string; patterns: string[] } | undefined; - -function expandTilde(value: string): string { - if (value === "~") return homedir(); - if (value.startsWith("~/")) return join(homedir(), value.slice(2)); - return value; -} - -function resolveAgentSettingsPath(): string { - const envDir = process.env[AGENT_SETTINGS_ENV]; - const agentDir = envDir ? expandTilde(envDir) : DEFAULT_AGENT_DIR; - return join(agentDir, SETTINGS_FILE_NAME); -} - -function readPiSettings(path: string): PiSettings | null { - try { - if (!fs.existsSync(path)) return null; - const content = fs.readFileSync(path, "utf-8"); - return JSON.parse(content) as PiSettings; - } catch { - return null; - } -} - -function loadScopedModelPatterns(cwd: string): string[] { - if (scopedModelPatternsCache?.cwd === cwd) { - return scopedModelPatternsCache.patterns; - } - - const globalSettings = readPiSettings(resolveAgentSettingsPath()); - const projectSettingsPath = join(cwd, PROJECT_SETTINGS_DIR, SETTINGS_FILE_NAME); - const projectSettings = readPiSettings(projectSettingsPath); - - let enabledModels = Array.isArray(globalSettings?.enabledModels) - ? (globalSettings?.enabledModels as string[]) - : undefined; - - if (projectSettings && Object.prototype.hasOwnProperty.call(projectSettings, "enabledModels")) { - enabledModels = Array.isArray(projectSettings.enabledModels) - ? (projectSettings.enabledModels as string[]) - : []; - } - - const patterns = !enabledModels || enabledModels.length === 0 - ? [] - : enabledModels.filter((value) => typeof value === "string"); - scopedModelPatternsCache = { cwd, patterns }; - return patterns; -} - -/** - * Create the extension - */ -export default function createExtension(pi: ExtensionAPI) { - let lastContext: ExtensionContext | undefined; - let settings: Settings = loadSettings(); - let uiEnabled = true; - let currentUsage: UsageSnapshot | undefined; - let usageEntries: Partial> = {}; - let coreAvailable = false; - let coreSettings: CoreSettings = getFallbackCoreSettings(settings); - let fetchFailureTimer: NodeJS.Timeout | undefined; - const antigravityHiddenModels = new Set(["tab_flash_lite_preview"]); - let settingsWatcher: fs.FSWatcher | undefined; - let settingsPoll: NodeJS.Timeout | undefined; - let settingsDebounce: NodeJS.Timeout | undefined; - let settingsSnapshot = ""; - let settingsMtimeMs = 0; - let settingsWatchStarted = false; - let subCoreBootstrapAttempted = false; - - async function probeSubCore(timeoutMs = 200): Promise { - return new Promise((resolve) => { - let resolved = false; - const timer = setTimeout(() => { - if (!resolved) { - resolved = true; - resolve(false); - } - }, timeoutMs); - - const request: SubCoreRequest = { - type: "current", - reply: () => { - if (resolved) return; - resolved = true; - clearTimeout(timer); - resolve(true); - }, - }; - pi.events.emit("sub-core:request", request); - }); - } - - async function ensureSubCoreLoaded(): Promise { - if (subCoreBootstrapAttempted) return; - subCoreBootstrapAttempted = true; - const hasCore = await probeSubCore(); - if (hasCore) return; - try { - const module = await import("./sub-core/index.js"); - const createCore = module.default as undefined | ((api: ExtensionAPI) => void | Promise); - if (typeof createCore === "function") { - void createCore(pi); - return; - } - } catch (error) { - console.warn("Failed to auto-load sub-core:", error); - } - } - - - async function promptImportAction(ctx: ExtensionContext): Promise<"save-apply" | "save" | "cancel"> { - return new Promise((resolve) => { - ctx.ui.custom((_tui, theme, _kb, done) => { - const items = [ - { value: "save-apply", label: "Save & apply", description: "save and use this theme" }, - { value: "save", label: "Save", description: "save without applying" }, - { value: "cancel", label: "Cancel", description: "discard import" }, - ]; - const list = new SelectList(items, items.length, { - 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), - }); - list.onSelect = (item) => { - done(undefined); - resolve(item.value as "save-apply" | "save" | "cancel"); - }; - list.onCancel = () => { - done(undefined); - resolve("cancel"); - }; - return list; - }); - }); - } - - async function promptImportString(ctx: ExtensionContext): Promise { - return new Promise((resolve) => { - ctx.ui.custom((_tui, theme, _kb, done) => { - const input = new Input(); - input.focused = true; - input.onSubmit = (value) => { - done(undefined); - resolve(value.trim()); - }; - input.onEscape = () => { - done(undefined); - resolve(undefined); - }; - const container = new Container(); - container.addChild(new Text(theme.fg("muted", "Paste Theme Share string"), 1, 0)); - container.addChild(new Spacer(1)); - container.addChild(input); - return { - render: (width: number) => container.render(width), - invalidate: () => container.invalidate(), - handleInput: (data: string) => input.handleInput(data), - }; - }); - }); - } - - async function promptImportName(ctx: ExtensionContext): Promise { - while (true) { - const name = await ctx.ui.input("Theme name", "Theme"); - if (name === undefined) return undefined; - const trimmed = name.trim(); - if (trimmed) return trimmed; - ctx.ui.notify("Enter a theme name", "warning"); - } - } - - const THEME_GIST_FILE_BASE = "pi-sub-bar Theme"; - const THEME_GIST_STATUS_KEY = "sub-bar:share"; - - function buildThemeGistFileName(name: string): string { - const trimmed = name.trim(); - if (!trimmed) return THEME_GIST_FILE_BASE; - const safeName = trimmed.replace(/[\\/:*?"<>|]+/g, "-").trim(); - return safeName ? `${THEME_GIST_FILE_BASE} ${safeName}` : THEME_GIST_FILE_BASE; - } - - async function createThemeGist(ctx: ExtensionContext, name: string, shareString: string): Promise { - const notify = (message: string, level: "info" | "warning" | "error") => { - if (ctx.hasUI) { - ctx.ui.notify(message, level); - return; - } - if (level === "error") { - console.error(message); - } else if (level === "warning") { - console.warn(message); - } else { - console.log(message); - } - }; - - try { - const authResult = await pi.exec("gh", ["auth", "status"]); - if (authResult.code !== 0) { - notify("GitHub CLI is not logged in. Run 'gh auth login' first.", "error"); - return null; - } - } catch { - notify("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/", "error"); - return null; - } - - const tempDir = fs.mkdtempSync(join(tmpdir(), "pi-sub-bar-")); - const fileName = buildThemeGistFileName(name); - const filePath = join(tempDir, fileName); - fs.writeFileSync(filePath, shareString, "utf-8"); - - if (ctx.hasUI) { - ctx.ui.setStatus(THEME_GIST_STATUS_KEY, "Creating gist..."); - } - - try { - const result = await pi.exec("gh", ["gist", "create", "--public=false", filePath]); - if (result.code !== 0) { - const errorMsg = result.stderr?.trim() || "Unknown error"; - notify(`Failed to create gist: ${errorMsg}`, "error"); - return null; - } - const gistUrl = result.stdout?.trim(); - if (!gistUrl) { - notify("Failed to create gist: empty response", "error"); - return null; - } - return gistUrl; - } catch (error) { - notify(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`, "error"); - return null; - } finally { - if (ctx.hasUI) { - ctx.ui.setStatus(THEME_GIST_STATUS_KEY, undefined); - } - try { - fs.rmSync(tempDir, { recursive: true, force: true }); - } catch { - // Ignore cleanup errors - } - } - } - - async function shareThemeString( - ctx: ExtensionContext, - name: string, - shareString: string, - mode: "prompt" | "gist" | "string" = "prompt", - ): Promise { - const trimmedName = name.trim(); - const notify = (message: string, level: "info" | "warning" | "error") => { - if (ctx.hasUI) { - ctx.ui.notify(message, level); - return; - } - if (level === "error") { - console.error(message); - } else if (level === "warning") { - console.warn(message); - } else { - console.log(message); - } - }; - let resolvedMode = mode; - if (resolvedMode === "prompt") { - if (!ctx.hasUI) { - resolvedMode = "string"; - } else { - const wantsGist = await ctx.ui.confirm("Share Theme", "Upload to a secret GitHub gist?"); - resolvedMode = wantsGist ? "gist" : "string"; - } - } - - if (resolvedMode === "gist") { - const gistUrl = await createThemeGist(ctx, trimmedName, shareString); - if (gistUrl) { - pi.sendMessage({ - customType: "sub-bar", - content: `Theme gist:\n${gistUrl}`, - display: true, - }); - notify("Theme gist posted to chat", "info"); - return; - } - notify("Posting share string instead.", "warning"); - } - - pi.sendMessage({ - customType: "sub-bar", - content: `Theme share string:\n${shareString}`, - display: true, - }); - notify("Theme share string posted to chat", "info"); - } - - function readSettingsFile(): string | undefined { - try { - return fs.readFileSync(SETTINGS_PATH, "utf-8"); - } catch { - return undefined; - } - } - - function applySettingsFromDisk(): void { - clearSettingsCache(); - const loaded = loadSettings(); - settings = { - ...settings, - version: loaded.version, - display: loaded.display, - providers: loaded.providers, - displayThemes: loaded.displayThemes, - displayUserTheme: loaded.displayUserTheme, - pinnedProvider: loaded.pinnedProvider, - keybindings: loaded.keybindings, - }; - coreSettings = getFallbackCoreSettings(settings); - updateFetchFailureTicker(); - void ensurePinnedEntries(settings.pinnedProvider ?? null); - if (lastContext) { - renderCurrent(lastContext); - } - } - - 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?.(); - } - - function formatUsageContent( - ctx: ExtensionContext, - theme: Theme, - usage: UsageSnapshot | undefined, - contentWidth: number, - message?: string, - options?: { forceNoFill?: boolean } - ): string[] { - const paddingLeft = settings.display.paddingLeft ?? 0; - const paddingRight = settings.display.paddingRight ?? 0; - const innerWidth = Math.max(1, contentWidth - paddingLeft - paddingRight); - const alignment = settings.display.alignment ?? "left"; - const configuredHasFill = settings.display.barWidth === "fill" || settings.display.dividerBlanks === "fill"; - const hasFill = options?.forceNoFill ? false : configuredHasFill; - const wantsSplit = options?.forceNoFill ? false : alignment === "split"; - const shouldAlign = !hasFill && !wantsSplit && (alignment === "center" || alignment === "right"); - const baseTextColor = resolveBaseTextColor(settings.display.baseTextColor); - const scopedModelPatterns = loadScopedModelPatterns(ctx.cwd); - const modelInfo = ctx.model - ? { provider: ctx.model.provider, id: ctx.model.id, scopedModelPatterns } - : { scopedModelPatterns }; - - // Get context usage info from pi framework - const ctxUsage = ctx.getContextUsage?.(); - const contextInfo: ContextInfo | undefined = ctxUsage && ctxUsage.contextWindow > 0 && ctxUsage.tokens != null && ctxUsage.percent != null - ? { tokens: ctxUsage.tokens, contextWindow: ctxUsage.contextWindow, percent: ctxUsage.percent } - : undefined; - - const formatted = message - ? applyBaseTextColor(theme, baseTextColor, message) - : (!usage) - ? undefined - : (hasFill || wantsSplit) - ? formatUsageStatusWithWidth(theme, usage, innerWidth, modelInfo, settings, { labelGapFill: wantsSplit }, contextInfo) - : formatUsageStatus(theme, usage, modelInfo, settings, contextInfo); - - const alignLine = (line: string) => { - if (!shouldAlign) return line; - const lineWidth = visibleWidth(line); - if (lineWidth >= innerWidth) return line; - const padding = innerWidth - lineWidth; - const leftPad = alignment === "center" ? Math.floor(padding / 2) : padding; - return " ".repeat(leftPad) + line; - }; - - let lines: string[] = []; - if (!formatted) { - lines = []; - } else if (settings.display.overflow === "wrap") { - lines = wrapTextWithAnsi(formatted, innerWidth).map(alignLine); - } else { - const trimmed = alignLine(truncateToWidth(formatted, innerWidth, theme.fg("dim", "..."))); - lines = [trimmed]; - } - - if (paddingLeft > 0 || paddingRight > 0) { - const leftPad = " ".repeat(paddingLeft); - const rightPad = " ".repeat(paddingRight); - lines = lines.map((line) => `${leftPad}${line}${rightPad}`); - } - - return lines; - } - - function renderUsageWidget(ctx: ExtensionContext, usage: UsageSnapshot | undefined, message?: string): void { - if (!ctx.hasUI || !uiEnabled) { - return; - } - - if (!usage && !message) { - ctx.ui.setFooter(undefined); - return; - } - - // Use setFooter to place between editor and status line - ctx.ui.setFooter((_tui, theme, footerData) => { - // Get other extension statuses (LSP, timestamps, etc.) - const otherStatuses = footerData.getExtensionStatuses?.() ?? new Map(); - const otherStatusText = Array.from(otherStatuses.values()).filter((s: string) => s && !s.includes("usage")).join(" "); - - return { - invalidate() {}, - render(width: number): string[] { - const safeWidth = Math.max(1, width); - const dividerColor: ThemeColor = resolveDividerColor(settings.display.dividerColor); - - // Build simple two-line format: - // Line 1: label (reset time) | label (reset time) | label (reset time) - // Line 2: progress bar | progress bar | progress bar - const line1Parts: string[] = []; - const line2Parts: string[] = []; - - if (usage?.windows) { - const dividerChar = theme.fg(dividerColor, "│"); - - for (const window of usage.windows) { - // Line 1: label (reset time) - const resetText = window.resetDescription ? `(${window.resetDescription})` : ""; - const labelText = `${window.label} ${resetText}`.trim(); - line1Parts.push(theme.fg("muted", labelText)); - - // Line 2: progress bar - const percent = window.usedPercent ?? 0; - const barWidth = 12; - const filled = Math.round((percent / 100) * barWidth); - const bar = "─".repeat(filled) + "─".repeat(barWidth - filled); - // Color based on percentage - const barColor = percent > 75 ? "error" : percent > 50 ? "warning" : "success"; - line2Parts.push(theme.fg(barColor, bar)); - } - } - - const line1 = line1Parts.join(` ${theme.fg("dim", "│")} `); - const line2 = line2Parts.join(` ${theme.fg("dim", "│")} `); - - // Combine usage lines with other statuses - const usageLines = [line1, line2].filter(l => l); - if (otherStatusText) { - return [...usageLines, otherStatusText]; - } - return usageLines; - }, - }; - }); - } - - function resolveDisplayedUsage(): UsageSnapshot | undefined { - const pinned = settings.pinnedProvider ?? null; - if (pinned) { - return usageEntries[pinned as ProviderName] ?? currentUsage; - } - return currentUsage; - } - - function syncAntigravityModels(usage?: UsageSnapshot): void { - if (!usage || usage.provider !== "antigravity") return; - const normalizeModel = (label: string) => label.toLowerCase().replace(/\s+/g, "_"); - const labels = usage.windows - .map((window) => window.label?.trim()) - .filter((label): label is string => Boolean(label)) - .filter((label) => !antigravityHiddenModels.has(normalizeModel(label))); - const uniqueModels = Array.from(new Set(labels)); - const antigravitySettings = settings.providers.antigravity; - const visibility = { ...(antigravitySettings.modelVisibility ?? {}) }; - const modelSet = new Set(uniqueModels); - let changed = false; - - for (const model of uniqueModels) { - if (!(model in visibility)) { - visibility[model] = false; - changed = true; - } - } - - for (const existing of Object.keys(visibility)) { - if (!modelSet.has(existing)) { - delete visibility[existing]; - changed = true; - } - } - - const currentOrder = antigravitySettings.modelOrder ?? []; - const orderChanged = currentOrder.length !== uniqueModels.length - || currentOrder.some((model, index) => model !== uniqueModels[index]); - if (orderChanged) { - changed = true; - } - - if (!changed) return; - antigravitySettings.modelVisibility = visibility; - antigravitySettings.modelOrder = uniqueModels; - saveSettings(settings); - } - - function updateEntries(entries: ProviderUsageEntry[] | undefined): void { - if (!entries) return; - const next: Partial> = {}; - for (const entry of entries) { - if (!entry.usage) continue; - next[entry.provider] = entry.usage; - } - usageEntries = next; - syncAntigravityModels(next.antigravity); - updateFetchFailureTicker(); - } - - function updateFetchFailureTicker(): void { - if (!uiEnabled) { - if (fetchFailureTimer) { - clearInterval(fetchFailureTimer); - fetchFailureTimer = undefined; - } - return; - } - const usage = resolveDisplayedUsage(); - const shouldTick = Boolean(usage?.error && usage.lastSuccessAt); - if (shouldTick && !fetchFailureTimer) { - fetchFailureTimer = setInterval(() => { - if (!lastContext) return; - renderCurrent(lastContext); - }, 60000); - fetchFailureTimer.unref?.(); - } - if (!shouldTick && fetchFailureTimer) { - clearInterval(fetchFailureTimer); - fetchFailureTimer = undefined; - } - } - - function renderCurrent(ctx: ExtensionContext): void { - if (!coreAvailable) { - renderUsageWidget(ctx, undefined, "pi-sub-core required. install with: pi install npm:@marckrenn/pi-sub-core"); - return; - } - const usage = resolveDisplayedUsage(); - renderUsageWidget(ctx, usage); - } - - function updateUsage(usage: UsageSnapshot | undefined): void { - currentUsage = usage; - syncAntigravityModels(usage); - updateFetchFailureTicker(); - if (lastContext) { - renderCurrent(lastContext); - } - } - - function applyCoreSettings(next?: CoreSettings): void { - if (!next) return; - coreSettings = next; - settings.behavior = next.behavior ?? settings.behavior; - settings.statusRefresh = next.statusRefresh ?? settings.statusRefresh; - settings.providerOrder = next.providerOrder ?? settings.providerOrder; - settings.defaultProvider = next.defaultProvider ?? settings.defaultProvider; - } - - function applyCoreSettingsPatch(patch: Partial): void { - if (patch.providers) { - for (const [provider, value] of Object.entries(patch.providers)) { - const key = provider as ProviderName; - const current = coreSettings.providers[key]; - if (!current) continue; - coreSettings.providers[key] = { ...current, ...value }; - } - } - if (patch.behavior) { - coreSettings.behavior = { ...coreSettings.behavior, ...patch.behavior }; - } - if (patch.statusRefresh) { - coreSettings.statusRefresh = { ...coreSettings.statusRefresh, ...patch.statusRefresh }; - } - if (patch.providerOrder) { - coreSettings.providerOrder = [...patch.providerOrder]; - } - if (patch.defaultProvider !== undefined) { - coreSettings.defaultProvider = patch.defaultProvider; - } - } - - function emitCoreAction(action: SubCoreAction): void { - pi.events.emit("sub-core:action", action); - } - - function requestCoreState(timeoutMs = 1000): Promise { - return new Promise((resolve) => { - let resolved = false; - const timer = setTimeout(() => { - if (!resolved) { - resolved = true; - resolve(undefined); - } - }, timeoutMs); - - const request: SubCoreRequest = { - type: "current", - includeSettings: true, - reply: (payload) => { - if (resolved) return; - resolved = true; - clearTimeout(timer); - applyCoreSettings(payload.settings); - resolve(payload.state); - }, - }; - - pi.events.emit("sub-core:request", request); - }); - } - - function requestCoreEntries(timeoutMs = 1000): Promise { - return new Promise((resolve) => { - let resolved = false; - const timer = setTimeout(() => { - if (!resolved) { - resolved = true; - resolve(undefined); - } - }, timeoutMs); - - const request: SubCoreRequest = { - type: "entries", - reply: (payload) => { - if (resolved) return; - resolved = true; - clearTimeout(timer); - resolve(payload.entries); - }, - }; - - pi.events.emit("sub-core:request", request); - }); - } - - async function ensurePinnedEntries(pinned: ProviderName | null): Promise { - if (!pinned) return; - if (usageEntries[pinned]) return; - const entries = await requestCoreEntries(); - updateEntries(entries); - if (lastContext) { - renderCurrent(lastContext); - } - } - - pi.events.on("sub-core:update-all", (payload) => { - coreAvailable = true; - const state = payload as { state?: SubCoreAllState }; - updateEntries(state.state?.entries); - if (lastContext) { - renderCurrent(lastContext); - } - }); - - pi.events.on("sub-core:update-current", (payload) => { - coreAvailable = true; - const state = payload as { state?: SubCoreState }; - updateUsage(state.state?.usage); - }); - - pi.events.on("sub-core:ready", (payload) => { - coreAvailable = true; - const state = payload as { state?: SubCoreState; settings?: CoreSettings }; - applyCoreSettings(state.settings); - updateUsage(state.state?.usage); - }); - - pi.events.on("sub-core:settings:updated", (payload) => { - const update = payload as { settings?: CoreSettings }; - applyCoreSettings(update.settings); - if (lastContext) { - renderCurrent(lastContext); - } - }); - - // Register command to open settings - pi.registerCommand("sub-bar:settings", { - description: "Open sub-bar settings", - handler: async (_args, ctx) => { - const newSettings = await showSettingsUI(ctx, { - coreSettings, - onOpenCoreSettings: async () => { - ctx.ui.setEditorText("/sub-core:settings"); - }, - onSettingsChange: async (updatedSettings) => { - const previousPinned = settings.pinnedProvider ?? null; - settings = updatedSettings; - updateFetchFailureTicker(); - if (settings.pinnedProvider && settings.pinnedProvider !== previousPinned) { - void ensurePinnedEntries(settings.pinnedProvider); - } - if (lastContext) { - renderCurrent(lastContext); - } - }, - onCoreSettingsChange: async (patch, _next) => { - applyCoreSettingsPatch(patch); - pi.events.emit("sub-core:settings:patch", { patch }); - if (lastContext) { - renderCurrent(lastContext); - } - }, - onDisplayThemeApplied: (name, options) => { - const content = options?.source === "manual" - ? `sub-bar Theme ${name} loaded` - : `sub-bar Theme ${name} loaded / applied / saved. Restore settings in /sub-bar:settings -> Themes -> Load & Manage themes`; - pi.sendMessage({ - customType: "sub-bar", - content, - display: true, - }); - }, - onDisplayThemeShared: (name, shareString, mode) => shareThemeString(ctx, name, shareString, mode ?? "prompt"), - }); - settings = newSettings; - void ensurePinnedEntries(settings.pinnedProvider ?? null); - if (lastContext) { - renderCurrent(lastContext); - } - }, - }); - - pi.registerCommand("sub-bar:import", { - description: "Import a shared display theme", - handler: async (args, ctx) => { - let input = String(args ?? "").trim(); - if (input.startsWith("/sub-bar:import")) { - input = input.replace(/^\/sub-bar:import\s*/i, "").trim(); - } else if (input.startsWith("sub-bar:import")) { - input = input.replace(/^sub-bar:import\s*/i, "").trim(); - } - if (!input) { - const typed = await promptImportString(ctx); - if (!typed) return; - input = typed; - } - const decoded = decodeDisplayShareString(input); - if (!decoded) { - ctx.ui.notify("Invalid theme share string", "error"); - return; - } - const backup = { ...settings.display }; - settings.display = { ...decoded.display }; - if (lastContext) { - renderUsageWidget(lastContext, currentUsage); - } - - const action = await promptImportAction(ctx); - let resolvedName = decoded.name; - if ((action === "save-apply" || action === "save") && !decoded.hasName) { - const providedName = await promptImportName(ctx); - if (!providedName) { - settings.display = { ...backup }; - if (lastContext) { - renderUsageWidget(lastContext, currentUsage); - } - return; - } - resolvedName = providedName; - } - const notifyImported = (name: string) => { - const message = decoded.isNewerVersion - ? `Imported ${name} (newer version, some fields may be ignored)` - : `Imported ${name}`; - ctx.ui.notify(message, decoded.isNewerVersion ? "warning" : "info"); - }; - - if (action === "save-apply") { - settings.displayUserTheme = { ...backup }; - settings = upsertDisplayTheme(settings, resolvedName, decoded.display, "imported"); - settings.display = { ...decoded.display }; - saveSettings(settings); - if (lastContext) { - renderUsageWidget(lastContext, currentUsage); - } - notifyImported(resolvedName); - pi.sendMessage({ - customType: "sub-bar", - content: `sub-bar Theme ${resolvedName} loaded`, - display: true, - }); - return; - } - - if (action === "save") { - settings = upsertDisplayTheme(settings, resolvedName, decoded.display, "imported"); - settings.display = { ...backup }; - saveSettings(settings); - notifyImported(resolvedName); - if (lastContext) { - renderUsageWidget(lastContext, currentUsage); - } - return; - } - - settings.display = { ...backup }; - if (lastContext) { - renderUsageWidget(lastContext, currentUsage); - } - }, - }); - - // Register shortcut to cycle providers - const cycleProviderKey = settings.keybindings?.cycleProvider || "ctrl+alt+p"; - if (cycleProviderKey !== "none") { - pi.registerShortcut(cycleProviderKey as KeyId, { - description: "Cycle usage provider", - handler: async () => { - emitCoreAction({ type: "cycleProvider" }); - }, - }); - } - - // Register shortcut to toggle reset timer format - const toggleResetFormatKey = settings.keybindings?.toggleResetFormat || "ctrl+alt+r"; - if (toggleResetFormatKey !== "none") { - pi.registerShortcut(toggleResetFormatKey as KeyId, { - description: "Toggle reset timer format", - handler: async () => { - settings.display.resetTimeFormat = settings.display.resetTimeFormat === "datetime" ? "relative" : "datetime"; - saveSettings(settings); - if (lastContext && currentUsage) { - renderUsageWidget(lastContext, currentUsage); - } - }, - }); - } - - pi.on("session_start", async (_event, ctx) => { - lastContext = ctx; - uiEnabled = ctx.hasUI; - if (!uiEnabled) { - return; - } - settings = loadSettings(); - coreSettings = getFallbackCoreSettings(settings); - 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 - } - } - } - - const watchTimer = setTimeout(() => startSettingsWatch(), 0); - watchTimer.unref?.(); - - const sessionContext = ctx; - void (async () => { - await ensureSubCoreLoaded(); - if (!lastContext || lastContext !== sessionContext || !uiEnabled) return; - const state = await requestCoreState(); - if (!lastContext || lastContext !== sessionContext || !uiEnabled) return; - if (state) { - coreAvailable = true; - updateUsage(state.usage); - if (settings.pinnedProvider) { - const entries = await requestCoreEntries(); - if (!lastContext || lastContext !== sessionContext || !uiEnabled) return; - updateEntries(entries); - if (lastContext) { - renderCurrent(lastContext); - } - } - } else if (lastContext && !coreAvailable) { - coreAvailable = false; - renderCurrent(lastContext); - } - })(); - }); - - pi.on("model_select" as unknown as "session_start", async (_event: unknown, ctx: ExtensionContext) => { - lastContext = ctx; - if (!uiEnabled || !ctx.hasUI) { - return; - } - if (currentUsage) { - renderUsageWidget(ctx, currentUsage); - } - }); - - pi.on("session_shutdown", async () => { - lastContext = undefined; - if (fetchFailureTimer) { - clearInterval(fetchFailureTimer); - fetchFailureTimer = undefined; - } - }); - -} diff --git a/pi/files/agent/extensions/sub-bar/package-lock.json b/pi/files/agent/extensions/sub-bar/package-lock.json deleted file mode 100644 index 1568344..0000000 --- a/pi/files/agent/extensions/sub-bar/package-lock.json +++ /dev/null @@ -1,2718 +0,0 @@ -{ - "name": "sub-bar", - "version": "1.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "sub-bar", - "version": "1.0.1", - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.8.0" - }, - "peerDependencies": { - "@mariozechner/pi-coding-agent": "*" - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.71.2", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz", - "integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@borewit/text-codec": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", - "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@google/genai": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.34.0.tgz", - "integrity": "sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "google-auth-library": "^10.3.0", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "peer": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.0.tgz", - "integrity": "sha512-tQrCRAtr58BLmWcvwCqlJo5GJgqBGb3zwOBFFBKCEKvRgD8y/EawhCyXsfOh9XOOde1NTAYsYuYyVOYw2tLnoQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.0", - "@mariozechner/clipboard-darwin-universal": "0.3.0", - "@mariozechner/clipboard-darwin-x64": "0.3.0", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.0", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.0", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.0", - "@mariozechner/clipboard-linux-x64-musl": "0.3.0", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.0", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.0" - } - }, - "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.0.tgz", - "integrity": "sha512-7i4bitLzRSij0fj6q6tPmmf+JrwHqfBsBmf8mOcLVv0LVexD+4gEsyMait4i92exKYmCfna6uHKVS84G4nqehg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.0.tgz", - "integrity": "sha512-FVZLGdIkmvqtPQjD0GQwKLVheL+zV7DjA6I5NcsHGjBeWpG2nACS6COuelNf8ruMoPxJFw7RoB4fjw6mmjT+Nw==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.0.tgz", - "integrity": "sha512-KuurQYEqRhalvBji3CH5xIq1Ts23IgVRE3rjanhqFDI77luOhCnlNbDtqv3No5OxJhEBLykQNrAzfgjqPsPWdA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.0.tgz", - "integrity": "sha512-nWpGMlk43bch7ztGfnALcSi5ZREVziPYzrFKjoJimbwaiULrfY0fGce0gWBynP9ak0nHgDLp0nSa7b4cCl+cIw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.0.tgz", - "integrity": "sha512-4BC08CIaOXSSAGRZLEjqJmQfioED8ohAzwt0k2amZPEbH96YKoBNorq5EdwPf5VT+odS0DeyCwhwtxokRLZIvQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.0.tgz", - "integrity": "sha512-GpNY5Y9nOzr0Vt0Qi5U88qwe6piiIHk44kSMexl8ns90LluN5UTNYmyfi7Xq3/lmPZCpnB2xvBTYbsXCxnopIA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.0.tgz", - "integrity": "sha512-+PnR48/x9GMY5Kh8BLjzHMx6trOegMtxAuqTM9X/bhV3QuW6sLLd7nojDHSGj/ZueK6i0tcQxvOrgNLozVtNDA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.0.tgz", - "integrity": "sha512-+dy2vZ1Ph4EYj0cotB+bVUVk/uKl2bh9LOp/zlnFqoCCYDN6sm+L0VyIOPPo3hjoEVdGpHe1MUxp3qG/OLwXgg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.0.tgz", - "integrity": "sha512-dfpHrUpKHl7ad3xVGE1+gIN3cEnjjPZa4I0BIYMuj2OKq07Gf1FKTXMypB41rDFv6XNzcfhYQnY+ZNgIu9FB8A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/pi-agent-core": { - "version": "0.42.5", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.42.5.tgz", - "integrity": "sha512-z3S9xqgkCeVxusWRmZMJK/KaRzVIxQ+7bXc2r2Adkx0S7prTyxGS+OOnnxff2kgjpQ6WV5W01lxcerjSQDx4XA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@mariozechner/pi-ai": "^0.42.5", - "@mariozechner/pi-tui": "^0.42.5" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@mariozechner/pi-ai": { - "version": "0.42.5", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.42.5.tgz", - "integrity": "sha512-/3NqKOSDXJge7RpDMXgK4vHm2qFMjol7CNwM3Dd477RE4I+tsQtL+xcpnB5I94phjsPUw83QSLHp5QzJb5fkpQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@anthropic-ai/sdk": "0.71.2", - "@google/genai": "1.34.0", - "@mistralai/mistralai": "1.10.0", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "chalk": "^5.6.2", - "openai": "6.10.0", - "partial-json": "^0.1.7", - "zod-to-json-schema": "^3.24.6" - }, - "bin": { - "pi-ai": "dist/cli.js" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@mariozechner/pi-coding-agent": { - "version": "0.42.5", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.42.5.tgz", - "integrity": "sha512-aGCQBiKvJJpSn1avCdTzeU8iWU0rMh3+iZKTfTE0MLuyvzcUoHxWnyzeEkJ2XddqyDsqv4K+E388+juFgFlbYQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@mariozechner/clipboard": "^0.3.0", - "@mariozechner/pi-agent-core": "^0.42.5", - "@mariozechner/pi-ai": "^0.42.5", - "@mariozechner/pi-tui": "^0.42.5", - "chalk": "^5.5.0", - "cli-highlight": "^2.1.11", - "diff": "^8.0.2", - "file-type": "^21.1.1", - "glob": "^11.0.3", - "jiti": "^2.6.1", - "marked": "^15.0.12", - "minimatch": "^10.1.1", - "proper-lockfile": "^4.1.2", - "sharp": "^0.34.2" - }, - "bin": { - "pi": "dist/cli.js" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@mariozechner/pi-tui": { - "version": "0.42.5", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.42.5.tgz", - "integrity": "sha512-/fkMl7fpKWMB9uXlhLXLUKkcd6tYBiJCehVYrQVj/VjqFM44k0FOP9+2+sA9F8BGDCAJqDNbayg1prY5DTMyDw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mime-types": "^2.1.4", - "chalk": "^5.5.0", - "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12", - "mime-types": "^3.0.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@mistralai/mistralai": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.10.0.tgz", - "integrity": "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==", - "peer": true, - "dependencies": { - "zod": "^3.20.0", - "zod-to-json-schema": "^3.24.1" - } - }, - "node_modules/@mistralai/mistralai/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.47", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", - "integrity": "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==", - "license": "MIT", - "peer": true - }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", - "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/node": { - "version": "22.19.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz", - "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT", - "peer": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "license": "ISC", - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "peer": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "peer": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT", - "peer": true - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT", - "peer": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT", - "peer": true - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-type": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", - "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "peer": true, - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "peer": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "license": "BlueOak-1.0.0", - "peer": true, - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC", - "peer": true - }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "license": "MIT", - "peer": true, - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "peer": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "peer": true - }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "license": "BlueOak-1.0.0", - "peer": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "peer": true, - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "license": "BlueOak-1.0.0", - "peer": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "license": "BlueOak-1.0.0", - "peer": true, - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "peer": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/openai": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.10.0.tgz", - "integrity": "sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0", - "peer": true - }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "license": "MIT", - "peer": true - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "license": "MIT", - "peer": true, - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT", - "peer": true - }, - "node_modules/partial-json": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", - "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", - "license": "MIT", - "peer": true - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "license": "BlueOak-1.0.0", - "peer": true, - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "peer": true - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", - "peer": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "peer": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "peer": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "peer": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "peer": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "peer": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "peer": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "license": "MIT", - "peer": true, - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT", - "peer": true - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true, - "peer": true - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "peer": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "peer": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peer": true, - "peerDependencies": { - "zod": "^3.25 || ^4" - } - } - } -} diff --git a/pi/files/agent/extensions/sub-bar/package.json b/pi/files/agent/extensions/sub-bar/package.json deleted file mode 100644 index 76f4aae..0000000 --- a/pi/files/agent/extensions/sub-bar/package.json +++ /dev/null @@ -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": "*" - } -} diff --git a/pi/files/agent/extensions/sub-bar/shared.ts b/pi/files/agent/extensions/sub-bar/shared.ts deleted file mode 100644 index dfe2137..0000000 --- a/pi/files/agent/extensions/sub-bar/shared.ts +++ /dev/null @@ -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"; diff --git a/pi/files/agent/extensions/sub-bar/src/core-settings.ts b/pi/files/agent/extensions/sub-bar/src/core-settings.ts deleted file mode 100644 index 92e507d..0000000 --- a/pi/files/agent/extensions/sub-bar/src/core-settings.ts +++ /dev/null @@ -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, - }; -} diff --git a/pi/files/agent/extensions/sub-bar/src/dividers.ts b/pi/files/agent/extensions/sub-bar/src/dividers.ts deleted file mode 100644 index 79bbb14..0000000 --- a/pi/files/agent/extensions/sub-bar/src/dividers.ts +++ /dev/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> = { - "|": { 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("")); -} diff --git a/pi/files/agent/extensions/sub-bar/src/errors.ts b/pi/files/agent/extensions/sub-bar/src/errors.ts deleted file mode 100644 index b7487d9..0000000 --- a/pi/files/agent/extensions/sub-bar/src/errors.ts +++ /dev/null @@ -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(["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"; - } -} diff --git a/pi/files/agent/extensions/sub-bar/src/formatting.ts b/pi/files/agent/extensions/sub-bar/src/formatting.ts deleted file mode 100644 index 05a0def..0000000 --- a/pi/files/agent/extensions/sub-bar/src/formatting.ts +++ /dev/null @@ -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["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[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[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[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[0], "▕"); - const rightCap = theme.fg(barColor as Parameters[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; -} diff --git a/pi/files/agent/extensions/sub-bar/src/paths.ts b/pi/files/agent/extensions/sub-bar/src/paths.ts deleted file mode 100644 index 19fb840..0000000 --- a/pi/files/agent/extensions/sub-bar/src/paths.ts +++ /dev/null @@ -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"); -} diff --git a/pi/files/agent/extensions/sub-bar/src/providers/extras.ts b/pi/files/agent/extensions/sub-bar/src/providers/extras.ts deleted file mode 100644 index a7d7e13..0000000 --- a/pi/files/agent/extensions/sub-bar/src/providers/extras.ts +++ /dev/null @@ -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 []; -} diff --git a/pi/files/agent/extensions/sub-bar/src/providers/metadata.ts b/pi/files/agent/extensions/sub-bar/src/providers/metadata.ts deleted file mode 100644 index c172c30..0000000 --- a/pi/files/agent/extensions/sub-bar/src/providers/metadata.ts +++ /dev/null @@ -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 = { - 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"], - }, -}; diff --git a/pi/files/agent/extensions/sub-bar/src/providers/settings.ts b/pi/files/agent/extensions/sub-bar/src/providers/settings.ts deleted file mode 100644 index e9ca357..0000000 --- a/pi/files/agent/extensions/sub-bar/src/providers/settings.ts +++ /dev/null @@ -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(); - - 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; -} diff --git a/pi/files/agent/extensions/sub-bar/src/providers/windows.ts b/pi/files/agent/extensions/sub-bar/src/providers/windows.ts deleted file mode 100644 index e9bb515..0000000 --- a/pi/files/agent/extensions/sub-bar/src/providers/windows.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/src/settings-types.ts b/pi/files/agent/extensions/sub-bar/src/settings-types.ts deleted file mode 100644 index fde2aaa..0000000 --- a/pi/files/agent/extensions/sub-bar/src/settings-types.ts +++ /dev/null @@ -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; - 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 { - /** 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(target: T, source: Partial): 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); - } 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 { - const migrated = migrateSettings(loaded); - return deepMerge(getDefaultSettings(), migrated); -} - -function migrateDisplaySettings(display?: Partial | null): void { - if (!display) return; - const displayAny = display as Partial & { 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): Partial { - migrateDisplaySettings(loaded.display); - migrateDisplaySettings(loaded.displayUserTheme); - if (Array.isArray(loaded.displayThemes)) { - for (const theme of loaded.displayThemes) { - migrateDisplaySettings(theme.display as Partial | undefined); - } - } - return loaded; -} diff --git a/pi/files/agent/extensions/sub-bar/src/settings-ui.ts b/pi/files/agent/extensions/sub-bar/src/settings-ui.ts deleted file mode 100644 index 4a88702..0000000 --- a/pi/files/agent/extensions/sub-bar/src/settings-ui.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Settings UI entry point (re-export). - */ - -export { showSettingsUI } from "./settings/ui.js"; diff --git a/pi/files/agent/extensions/sub-bar/src/settings.ts b/pi/files/agent/extensions/sub-bar/src/settings.ts deleted file mode 100644 index 1b1d3d3..0000000 --- a/pi/files/agent/extensions/sub-bar/src/settings.ts +++ /dev/null @@ -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; - 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); -} - -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; -} diff --git a/pi/files/agent/extensions/sub-bar/src/settings/display.ts b/pi/files/agent/extensions/sub-bar/src/settings/display.ts deleted file mode 100644 index 7f5afda..0000000 --- a/pi/files/agent/extensions/sub-bar/src/settings/display.ts +++ /dev/null @@ -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): 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; -} diff --git a/pi/files/agent/extensions/sub-bar/src/settings/menu.ts b/pi/files/agent/extensions/sub-bar/src/settings/menu.ts deleted file mode 100644 index 771d5b0..0000000 --- a/pi/files/agent/extensions/sub-bar/src/settings/menu.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/src/settings/themes.ts b/pi/files/agent/extensions/sub-bar/src/settings/themes.ts deleted file mode 100644 index 31f8ade..0000000 --- a/pi/files/agent/extensions/sub-bar/src/settings/themes.ts +++ /dev/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(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"); -} diff --git a/pi/files/agent/extensions/sub-bar/src/settings/ui.ts b/pi/files/agent/extensions/sub-bar/src/settings/ui.ts deleted file mode 100644 index b8431a3..0000000 --- a/pi/files/agent/extensions/sub-bar/src/settings/ui.ts +++ /dev/null @@ -1,1378 +0,0 @@ -/** - * Settings UI for sub-bar - */ - -import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; -import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent"; -import { Container, Input, 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 type { CoreSettings } from "../../shared.js"; -import { getFallbackCoreSettings } from "../core-settings.js"; -import { getDefaultSettings } from "../settings-types.js"; -import { getSettings, saveSettings } from "../settings.js"; -import { PROVIDER_DISPLAY_NAMES } from "../providers/metadata.js"; -import { buildProviderSettingsItems, applyProviderSettingsChange } from "../providers/settings.js"; -import { - buildDisplayLayoutItems, - buildDisplayResetItems, - buildDisplayColorItems, - buildDisplayBarItems, - buildDisplayProviderItems, - buildDisplayStatusItems, - buildDisplayDividerItems, - buildUsageColorTargetItems, - formatUsageColorTargetsSummary, - applyDisplayChange, -} from "./display.js"; -import { - buildMainMenuItems, - buildProviderListItems, - buildDisplayMenuItems, - buildDisplayThemeMenuItems, - getProviderFromCategory, - type TooltipSelectItem, -} from "./menu.js"; -import { - buildDisplayThemeItems, - buildThemeActionItems, - buildRandomDisplay, - resolveDisplayThemeTarget, - saveDisplayTheme, - renameDisplayTheme, - upsertDisplayTheme, -} from "./themes.js"; -import { - buildDisplayShareString, - buildDisplayShareStringWithoutName, - decodeDisplayShareString, - type DecodedDisplayShare, -} from "../share.js"; - -/** - * Settings category - */ -type ProviderCategory = `provider-${ProviderName}`; - -type SettingsCategory = - | "main" - | "providers" - | "pin-provider" - | ProviderCategory - | "keybindings" - | "display" - | "display-theme" - | "display-theme-save" - | "display-theme-share" - | "display-theme-load" - | "display-theme-action" - | "display-theme-import" - | "display-theme-import-action" - | "display-theme-import-name" - | "display-theme-rename" - | "display-theme-random" - | "display-theme-restore" - | "display-layout" - | "display-bar" - | "display-provider" - | "display-reset" - | "display-status" - | "display-divider" - | "display-color"; - -/** - * Show the settings UI - */ -export async function showSettingsUI( - ctx: ExtensionContext, - options?: { - coreSettings?: CoreSettings; - onSettingsChange?: (settings: Settings) => void | Promise; - onCoreSettingsChange?: (patch: Partial, next: CoreSettings) => void | Promise; - onOpenCoreSettings?: () => void | Promise; - onDisplayThemeApplied?: (name: string, options?: { source?: "manual" }) => void | Promise; - onDisplayThemeShared?: (name: string, shareString: string, mode?: "prompt" | "gist" | "string") => void | Promise; - } -): Promise { - const onSettingsChange = options?.onSettingsChange; - const onCoreSettingsChange = options?.onCoreSettingsChange; - const onOpenCoreSettings = options?.onOpenCoreSettings; - let settings = getSettings(); - let coreSettings = options?.coreSettings ?? getFallbackCoreSettings(settings); - const onDisplayThemeApplied = options?.onDisplayThemeApplied; - const onDisplayThemeShared = options?.onDisplayThemeShared; - let currentCategory: SettingsCategory = "main"; - - return new Promise((resolve) => { - ctx.ui.custom((tui, theme, _kb, done) => { - let container = new Container(); - let activeList: SelectList | SettingsList | { handleInput: (data: string) => void } | null = null; - let themeActionTarget: { id?: string; name: string; display: Settings["display"]; deletable: boolean } | null = null; - let displayPreviewBackup: Settings["display"] | null = null; - let randomThemeBackup: Settings["display"] | null = null; - let displayThemeSelection: string | null = null; - let pinnedProviderBackup: ProviderName | null | undefined; - let importCandidate: DecodedDisplayShare | null = null; - let importBackup: Settings["display"] | null = null; - let importPendingAction: "save" | "save-apply" | null = null; - let pendingShare: { name: string; shareString: string; backCategory: SettingsCategory } | null = null; - const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); - - 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, - description?: 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 container = new Container(); - container.addChild(new Text(theme.fg("muted", label), 1, 0)); - if (description) { - container.addChild(new Text(theme.fg("dim", description), 1, 0)); - } - container.addChild(new Spacer(1)); - container.addChild(input); - - return { - render: (width: number) => container.render(width), - invalidate: () => container.invalidate(), - handleInput: (data: string) => input.handleInput(data), - }; - }; - }; - - const requestThemeShare = (name: string, shareString: string, backCategory: SettingsCategory) => { - pendingShare = { name, shareString, backCategory }; - displayThemeSelection = "display-theme-share"; - currentCategory = "display-theme-share"; - rebuild(); - tui.requestRender(); - }; - - const parseInteger = (raw: string, min: number, max: number): string | null => { - const trimmed = raw.trim().replace(/%$/, ""); - if (!trimmed) { - ctx.ui.notify("Enter a value", "warning"); - return null; - } - const parsed = Number.parseInt(trimmed, 10); - if (Number.isNaN(parsed)) { - ctx.ui.notify("Enter a number", "warning"); - return null; - } - return String(clamp(parsed, min, max)); - }; - - const parseBarWidth = (raw: string): string | null => { - const trimmed = raw.trim().toLowerCase(); - if (!trimmed) { - ctx.ui.notify("Enter a value", "warning"); - return null; - } - if (trimmed === "fill") return "fill"; - return parseInteger(trimmed, 0, 100); - }; - - const parseDividerBlanks = (raw: string): string | null => { - const trimmed = raw.trim().toLowerCase(); - if (!trimmed) { - ctx.ui.notify("Enter a value", "warning"); - return null; - } - if (trimmed === "fill") return "fill"; - return parseInteger(trimmed, 0, 100); - }; - - const parseResetContainment = (raw: string): string | null => { - const trimmed = raw.trim(); - if (!trimmed) { - ctx.ui.notify("Enter 1-2 characters", "warning"); - return null; - } - const normalized = trimmed.toLowerCase(); - if (["none", "blank", "()", "[]", "<>"].includes(normalized)) { - return normalized; - } - const segments = Array.from(segmenter.segment(trimmed), (entry) => entry.segment) - .map((segment) => segment.trim()) - .filter(Boolean); - if (segments.length === 0) { - ctx.ui.notify("Enter 1-2 characters", "warning"); - return null; - } - const first = segments[0]; - const second = segments[1] ?? first; - return `${first}${second}`; - }; - - const parseDividerCharacter = (raw: string): string | null => { - const trimmed = raw.trim(); - if (!trimmed) { - ctx.ui.notify("Enter a character", "warning"); - return null; - } - const normalized = trimmed.toLowerCase(); - if (normalized === "none" || normalized === "blank") { - return normalized; - } - const iterator = segmenter.segment(trimmed)[Symbol.iterator](); - const first = iterator.next().value?.segment ?? trimmed[0]; - return first; - }; - - const parseBarCharacter = (raw: string): string | null => { - const trimmed = raw.trim(); - if (!trimmed) { - ctx.ui.notify("Enter a character", "warning"); - return null; - } - const normalized = trimmed.toLowerCase(); - if (["light", "heavy", "double", "block"].includes(normalized)) { - return normalized; - } - const segments = Array.from(segmenter.segment(raw), (entry) => entry.segment).filter((segment) => segment !== "\n" && segment !== "\r"); - const first = segments[0] ?? trimmed[0]; - const second = segments[1]; - return second ? `${first}${second}` : first; - }; - - const parseStatusIconCustom = (raw: string): string | null => { - const trimmed = raw.trim(); - if (!trimmed) { - ctx.ui.notify("Enter four characters", "warning"); - return null; - } - const segments = Array.from(segmenter.segment(trimmed), (entry) => entry.segment) - .map((segment) => segment.trim()) - .filter(Boolean); - if (segments.length < 4) { - ctx.ui.notify("Enter four characters", "warning"); - return null; - } - return segments.slice(0, 4).join(""); - }; - - const parseProviderLabel = (raw: string): string | null => { - const trimmed = raw.trim(); - if (!trimmed) { - ctx.ui.notify("Enter a label", "warning"); - return null; - } - const normalized = trimmed.toLowerCase(); - if (["none", "plan", "subscription", "sub"].includes(normalized)) { - return normalized; - } - return trimmed; - }; - - const attachCustomInputs = ( - items: SettingItem[], - handlers: Record>, - ) => { - for (const item of items) { - if (!item.values || !item.values.includes(CUSTOM_OPTION)) continue; - const handler = handlers[item.id]; - if (!handler) continue; - item.submenu = handler; - } - }; - - const buildUsageColorSubmenu = () => { - return (_currentValue: string, done: (selectedValue?: string) => void) => { - const items = buildUsageColorTargetItems(settings); - const handleChange = (id: string, value: string) => { - settings = applyDisplayChange(settings, id, value); - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - }; - const list = new SettingsList( - items, - Math.min(items.length + 2, 10), - getSettingsListTheme(), - handleChange, - () => { - done(formatUsageColorTargetsSummary(settings.display.usageColorTargets)); - } - ); - return list; - }; - }; - - 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 = { - main: "sub-bar Settings", - providers: "Provider Settings", - "pin-provider": "Provider Shown", - keybindings: "Keybindings", - display: "Adv. Display Settings", - "display-theme": "Themes", - "display-theme-save": "Save Theme", - "display-theme-share": "Share Theme", - "display-theme-rename": "Rename Theme", - "display-theme-load": "Load & Manage themes", - "display-theme-action": "Manage Theme", - "display-theme-import": "Import Theme", - "display-theme-import-name": "Name Theme", - "display-theme-restore": "Restore Theme", - "display-layout": "Layout & Structure", - "display-bar": "Bars", - "display-provider": "Labels & Text", - "display-reset": "Reset Timer", - "display-status": "Status", - "display-divider": "Dividers", - "display-color": "Colors", - }; - const providerCategory = getProviderFromCategory(currentCategory); - let title = providerCategory - ? `${PROVIDER_DISPLAY_NAMES[providerCategory]} Settings` - : (titles[currentCategory] ?? "sub-bar Settings"); - if (currentCategory === "display-theme-action" && themeActionTarget) { - title = `Manage ${themeActionTarget.name}`; - } - container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0)); - container.addChild(new Spacer(1)); - - if (currentCategory === "main") { - const items = buildMainMenuItems(settings, settings.pinnedProvider); - 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 === "open-core-settings") { - saveSettings(settings); - done(settings); - if (onOpenCoreSettings) { - setTimeout(() => void onOpenCoreSettings(), 0); - } - return; - } - currentCategory = item.value as SettingsCategory; - rebuild(); - tui.requestRender(); - }; - selectList.onCancel = () => { - saveSettings(settings); - done(settings); - }; - activeList = selectList; - container.addChild(selectList); - } else if (currentCategory === "pin-provider") { - if (pinnedProviderBackup === undefined) { - pinnedProviderBackup = settings.pinnedProvider ?? null; - } - const orderedProviders = settings.providerOrder.length > 0 ? settings.providerOrder : (Object.keys(settings.providers) as ProviderName[]); - const items: TooltipSelectItem[] = [ - { - value: "none", - label: "Auto", - description: "current provider", - tooltip: "Show the current provider automatically.", - }, - ...orderedProviders.map((provider) => ({ - value: provider, - label: PROVIDER_DISPLAY_NAMES[provider], - description: provider === settings.pinnedProvider ? "pinned" : "", - tooltip: `Pin ${PROVIDER_DISPLAY_NAMES[provider]} as the current provider.`, - })), - ]; - 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.onSelectionChange = (item) => { - if (!item) return; - const nextPinned = item.value === "none" ? null : (item.value as ProviderName); - if (settings.pinnedProvider === nextPinned) return; - settings.pinnedProvider = nextPinned; - if (onSettingsChange) void onSettingsChange(settings); - }; - selectList.onSelect = (item) => { - settings.pinnedProvider = item.value === "none" ? null : (item.value as ProviderName); - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - pinnedProviderBackup = undefined; - currentCategory = "main"; - rebuild(); - tui.requestRender(); - }; - selectList.onCancel = () => { - if (pinnedProviderBackup !== undefined && settings.pinnedProvider !== pinnedProviderBackup) { - settings.pinnedProvider = pinnedProviderBackup; - if (onSettingsChange) void onSettingsChange(settings); - } - pinnedProviderBackup = undefined; - currentCategory = "main"; - rebuild(); - tui.requestRender(); - }; - activeList = selectList; - container.addChild(selectList); - } else if (currentCategory === "keybindings") { - const parseKeybinding = (raw: string): string | null => { - const trimmed = raw.trim().toLowerCase(); - if (!trimmed) { - ctx.ui.notify("Enter a key combo (e.g. ctrl+alt+p) or 'none' to disable", "warning"); - return null; - } - if (trimmed === "none") return "none"; - const parts = trimmed.split("+"); - const modifiers = new Set(["ctrl", "shift", "alt"]); - const baseKeys = parts.filter((p) => !modifiers.has(p)); - if (baseKeys.length !== 1) { - ctx.ui.notify("Invalid key combo. Use format like ctrl+alt+p or ctrl+s", "warning"); - return null; - } - return trimmed; - }; - - const kbItems: SettingItem[] = [ - { - id: "cycleProvider", - label: "Cycle Provider", - currentValue: settings.keybindings.cycleProvider, - description: "Shortcut to cycle through providers. Changes take effect after pi restart.", - submenu: buildInputSubmenu( - "Cycle Provider shortcut", - parseKeybinding, - undefined, - "Enter a key combo (e.g. ctrl+alt+p) or 'none' to disable.", - ), - }, - { - id: "toggleResetFormat", - label: "Toggle Reset Format", - currentValue: settings.keybindings.toggleResetFormat, - description: "Shortcut to toggle reset timer format. Changes take effect after pi restart.", - submenu: buildInputSubmenu( - "Toggle Reset Format shortcut", - parseKeybinding, - undefined, - "Enter a key combo (e.g. ctrl+alt+r) or 'none' to disable.", - ), - }, - ]; - - const handleKbChange = (id: string, value: string) => { - if (id === "cycleProvider" || id === "toggleResetFormat") { - settings.keybindings = { ...settings.keybindings, [id]: value }; - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - } - }; - - const kbList = new SettingsList( - kbItems, - Math.min(kbItems.length + 3, 10), - getSettingsListTheme(), - handleKbChange, - () => { - currentCategory = "main"; - rebuild(); - tui.requestRender(); - }, - ); - activeList = kbList; - container.addChild(kbList); - } else if (currentCategory === "providers") { - const items = buildProviderListItems(settings, coreSettings.providers); - 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-providers") { - const defaults = getDefaultSettings(); - settings.providers = { ...defaults.providers }; - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - ctx.ui.notify("Provider settings reset to defaults", "info"); - rebuild(); - tui.requestRender(); - return; - } - currentCategory = item.value as SettingsCategory; - rebuild(); - tui.requestRender(); - }; - selectList.onCancel = () => { - currentCategory = "main"; - rebuild(); - tui.requestRender(); - }; - activeList = selectList; - container.addChild(selectList); - } else if (providerCategory) { - const items = buildProviderSettingsItems(settings, providerCategory); - const coreProvider = coreSettings.providers[providerCategory]; - const enabledValue = coreProvider.enabled === "auto" - ? "auto" - : coreProvider.enabled === true || coreProvider.enabled === "on" - ? "on" - : "off"; - items.unshift({ - id: "enabled", - label: "Enabled", - currentValue: enabledValue, - values: ["auto", "on", "off"], - description: "Auto enables if credentials are detected.", - }); - const handleChange = (id: string, value: string) => { - if (id === "enabled") { - const nextEnabled = value === "auto" ? "auto" : value === "on"; - coreProvider.enabled = nextEnabled; - if (onCoreSettingsChange) { - const patch = { - providers: { - [providerCategory]: { enabled: nextEnabled }, - }, - } as unknown as Partial; - void onCoreSettingsChange(patch, coreSettings); - } - return; - } - settings = applyProviderSettingsChange(settings, providerCategory, id, value); - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - }; - 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 = "providers"; - rebuild(); - tui.requestRender(); - } - ); - activeList = settingsList; - container.addChild(settingsList); - } else if (currentCategory === "display") { - const items = buildDisplayMenuItems(); - 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 === "display-theme") { - const items = buildDisplayThemeMenuItems(); - 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), - }); - if (displayThemeSelection) { - const index = items.findIndex((item) => item.value === displayThemeSelection); - if (index >= 0) { - selectList.setSelectedIndex(index); - } - } - attachTooltip(items, selectList); - selectList.onSelect = (item) => { - displayThemeSelection = item.value; - currentCategory = item.value as SettingsCategory; - pendingShare = null; - rebuild(); - tui.requestRender(); - }; - selectList.onCancel = () => { - currentCategory = "display"; - pendingShare = null; - rebuild(); - tui.requestRender(); - }; - activeList = selectList; - container.addChild(selectList); - } else if (currentCategory === "display-theme-save") { - const input = new Input(); - input.focused = true; - const titleText = new Text(theme.fg("muted", "Theme name"), 1, 0); - input.onSubmit = (value) => { - const trimmed = value.trim(); - if (!trimmed) { - ctx.ui.notify("Enter a theme name", "warning"); - return; - } - settings = saveDisplayTheme(settings, trimmed); - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - ctx.ui.notify(`Theme ${trimmed} saved`, "info"); - const shareString = buildDisplayShareString(trimmed, settings.display); - if (onDisplayThemeShared) { - requestThemeShare(trimmed, shareString, "display-theme"); - return; - } - ctx.ui.notify(shareString, "info"); - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - }; - input.onEscape = () => { - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - }; - container.addChild(titleText); - container.addChild(new Spacer(1)); - container.addChild(input); - activeList = input; - } else if (currentCategory === "display-theme-share") { - displayThemeSelection = "display-theme-share"; - const shareTarget = pendingShare ?? { - name: "", - shareString: buildDisplayShareStringWithoutName(settings.display), - backCategory: "display-theme" as SettingsCategory, - }; - pendingShare = shareTarget; - - const shareItems: TooltipSelectItem[] = [ - { - value: "gist", - label: "Upload secret gist", - description: "share via GitHub gist", - tooltip: "Create a secret GitHub gist using the gh CLI.", - }, - { - value: "string", - label: "Post share string", - description: "share in chat", - tooltip: "Post the raw share string to chat.", - }, - { - value: "cancel", - label: "Cancel", - description: "discard share", - tooltip: "Cancel without sharing.", - }, - ]; - - const selectList = new SelectList(shareItems, shareItems.length, { - 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(shareItems, selectList); - - selectList.onSelect = (item) => { - if (item.value === "gist" || item.value === "string") { - if (onDisplayThemeShared) { - void onDisplayThemeShared(shareTarget.name, shareTarget.shareString, item.value as "gist" | "string"); - } else { - ctx.ui.notify(shareTarget.shareString, "info"); - } - } - pendingShare = null; - currentCategory = shareTarget.backCategory; - rebuild(); - tui.requestRender(); - }; - - selectList.onCancel = () => { - pendingShare = null; - currentCategory = shareTarget.backCategory; - rebuild(); - tui.requestRender(); - }; - - activeList = selectList; - container.addChild(selectList); - } else if (currentCategory === "display-theme-load") { - if (!displayPreviewBackup) { - displayPreviewBackup = { ...settings.display }; - } - const defaults = getDefaultSettings(); - const fallbackUser = settings.displayUserTheme ?? displayPreviewBackup; - const themeItems = buildDisplayThemeItems(settings); - - const selectList = new SelectList(themeItems, Math.min(themeItems.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), - }); - selectList.onSelectionChange = (item) => { - if (!item) return; - const target = resolveDisplayThemeTarget(item.value, settings, defaults, fallbackUser); - if (!target) return; - settings.display = { ...target.display }; - if (onSettingsChange) void onSettingsChange(settings); - tui.requestRender(); - }; - attachTooltip(themeItems, selectList); - - selectList.onSelect = (item) => { - const target = resolveDisplayThemeTarget(item.value, settings, defaults, fallbackUser); - if (!target) return; - if (item.value.startsWith("theme:")) { - themeActionTarget = target; - currentCategory = "display-theme-action"; - rebuild(); - tui.requestRender(); - return; - } - - const backup = displayPreviewBackup ?? settings.display; - settings.displayUserTheme = { ...backup }; - settings.display = { ...target.display }; - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - if (onDisplayThemeApplied) void onDisplayThemeApplied(target.name, { source: "manual" }); - displayPreviewBackup = null; - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - }; - selectList.onCancel = () => { - if (displayPreviewBackup) { - settings.display = { ...displayPreviewBackup }; - if (onSettingsChange) void onSettingsChange(settings); - } - displayPreviewBackup = null; - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - }; - activeList = selectList; - container.addChild(selectList); - } else if (currentCategory === "display-theme-random") { - if (!randomThemeBackup) { - randomThemeBackup = { ...settings.display }; - settings.displayUserTheme = { ...randomThemeBackup }; - } - displayThemeSelection = "display-theme-random"; - const randomDisplay = buildRandomDisplay(settings.display); - settings.display = { ...randomDisplay }; - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - } else if (currentCategory === "display-theme-restore") { - displayThemeSelection = "display-theme-restore"; - const defaults = getDefaultSettings(); - const fallbackUser = settings.displayUserTheme ?? settings.display; - const target = resolveDisplayThemeTarget("user", settings, defaults, fallbackUser); - if (target) { - const backup = displayPreviewBackup ?? settings.display; - settings.displayUserTheme = { ...backup }; - settings.display = { ...target.display }; - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - if (onDisplayThemeApplied) void onDisplayThemeApplied(target.name, { source: "manual" }); - displayPreviewBackup = null; - } - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - } else if (currentCategory === "display-theme-import") { - const input = new Input(); - input.focused = true; - const titleText = new Text(theme.fg("muted", "Paste Theme Share string"), 1, 0); - input.onSubmit = (value) => { - const trimmed = value.trim(); - if (!trimmed) { - ctx.ui.notify("Enter a theme share string", "warning"); - return; - } - const decoded = decodeDisplayShareString(trimmed); - if (!decoded) { - ctx.ui.notify("Invalid theme share string", "error"); - return; - } - if (!displayPreviewBackup) { - displayPreviewBackup = { ...settings.display }; - } - importBackup = displayPreviewBackup; - importCandidate = decoded; - settings.display = { ...decoded.display }; - if (onSettingsChange) void onSettingsChange(settings); - currentCategory = "display-theme-import-action"; - rebuild(); - tui.requestRender(); - }; - input.onEscape = () => { - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - }; - container.addChild(titleText); - container.addChild(new Spacer(1)); - container.addChild(input); - activeList = input; - } else if (currentCategory === "display-theme-import-action") { - const candidate = importCandidate; - if (!candidate) { - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - return; - } - - const importItems: TooltipSelectItem[] = [ - { - value: "save-apply", - label: "Save & apply", - description: "save and use this theme", - tooltip: "Save the theme and keep it applied.", - }, - { - value: "save", - label: "Save", - description: "save without applying", - tooltip: "Save the theme and restore the previous display.", - }, - { - value: "cancel", - label: "Cancel", - description: "discard import", - tooltip: "Discard and restore the previous display.", - }, - ]; - - const notifyImported = (name: string) => { - const message = candidate.isNewerVersion - ? `Imported ${name} (newer version, some fields may be ignored)` - : `Imported ${name}`; - ctx.ui.notify(message, candidate.isNewerVersion ? "warning" : "info"); - }; - - const restoreBackup = () => { - if (importBackup) { - settings.display = { ...importBackup }; - if (onSettingsChange) void onSettingsChange(settings); - } - }; - - const selectList = new SelectList(importItems, importItems.length, { - 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), - }); - selectList.onSelectionChange = (item) => { - if (!item) return; - if (item.value === "save-apply") { - settings.display = { ...candidate.display }; - if (onSettingsChange) void onSettingsChange(settings); - return; - } - restoreBackup(); - }; - attachTooltip(importItems, selectList); - - selectList.onSelect = (item) => { - if ((item.value === "save-apply" || item.value === "save") && !candidate.hasName) { - importPendingAction = item.value as "save" | "save-apply"; - currentCategory = "display-theme-import-name"; - rebuild(); - tui.requestRender(); - return; - } - if (item.value === "save-apply") { - const resolvedName = candidate.name; - if (importBackup) { - settings.displayUserTheme = { ...importBackup }; - } - settings = upsertDisplayTheme(settings, resolvedName, candidate.display, "imported"); - settings.display = { ...candidate.display }; - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - if (onDisplayThemeApplied) void onDisplayThemeApplied(resolvedName, { source: "manual" }); - notifyImported(resolvedName); - displayPreviewBackup = null; - importCandidate = null; - importBackup = null; - importPendingAction = null; - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - return; - } - if (item.value === "save") { - const resolvedName = candidate.name; - settings = upsertDisplayTheme(settings, resolvedName, candidate.display, "imported"); - restoreBackup(); - saveSettings(settings); - notifyImported(resolvedName); - importCandidate = null; - importBackup = null; - importPendingAction = null; - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - return; - } - restoreBackup(); - importCandidate = null; - importBackup = null; - importPendingAction = null; - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - }; - selectList.onCancel = () => { - restoreBackup(); - importCandidate = null; - importBackup = null; - importPendingAction = null; - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - }; - activeList = selectList; - container.addChild(selectList); - } else if (currentCategory === "display-theme-import-name") { - const candidate = importCandidate; - if (!candidate) { - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - return; - } - - const notifyImported = (name: string) => { - const message = candidate.isNewerVersion - ? `Imported ${name} (newer version, some fields may be ignored)` - : `Imported ${name}`; - ctx.ui.notify(message, candidate.isNewerVersion ? "warning" : "info"); - }; - - const restoreBackup = () => { - if (importBackup) { - settings.display = { ...importBackup }; - if (onSettingsChange) void onSettingsChange(settings); - } - }; - - const input = new Input(); - input.focused = true; - const titleText = new Text(theme.fg("muted", "Theme name"), 1, 0); - input.onSubmit = (value) => { - const trimmed = value.trim(); - if (!trimmed) { - ctx.ui.notify("Enter a theme name", "warning"); - return; - } - const applyImport = importPendingAction === "save-apply"; - if (applyImport && importBackup) { - settings.displayUserTheme = { ...importBackup }; - } - settings = upsertDisplayTheme(settings, trimmed, candidate.display, "imported"); - if (applyImport) { - settings.display = { ...candidate.display }; - } else { - restoreBackup(); - } - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - if (applyImport && onDisplayThemeApplied) { - void onDisplayThemeApplied(trimmed, { source: "manual" }); - } - notifyImported(trimmed); - displayPreviewBackup = null; - importCandidate = null; - importBackup = null; - importPendingAction = null; - currentCategory = applyImport ? "display-theme" : "display-theme-load"; - rebuild(); - tui.requestRender(); - }; - input.onEscape = () => { - importPendingAction = null; - currentCategory = "display-theme-import-action"; - rebuild(); - tui.requestRender(); - }; - container.addChild(titleText); - container.addChild(new Spacer(1)); - container.addChild(input); - activeList = input; - } else if (currentCategory === "display-theme-rename") { - const target = themeActionTarget; - if (!target || !target.id) { - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - return; - } - - const input = new Input(); - input.focused = true; - const titleText = new Text(theme.fg("muted", `Rename ${target.name}`), 1, 0); - input.onSubmit = (value) => { - const trimmed = value.trim(); - if (!trimmed) { - ctx.ui.notify("Enter a theme name", "warning"); - return; - } - settings = renameDisplayTheme(settings, target.id!, trimmed); - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - themeActionTarget = null; - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - }; - input.onEscape = () => { - currentCategory = "display-theme-action"; - rebuild(); - tui.requestRender(); - }; - container.addChild(titleText); - container.addChild(new Spacer(1)); - container.addChild(input); - activeList = input; - } else if (currentCategory === "display-theme-action") { - const target = themeActionTarget; - if (!target) { - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - return; - } - - const items = buildThemeActionItems(target); - - const selectList = new SelectList(items, items.length, { - 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 === "load") { - const backup = displayPreviewBackup ?? settings.display; - settings.displayUserTheme = { ...backup }; - settings.display = { ...target.display }; - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - if (onDisplayThemeApplied) void onDisplayThemeApplied(target.name, { source: "manual" }); - displayPreviewBackup = null; - themeActionTarget = null; - currentCategory = "display-theme"; - rebuild(); - tui.requestRender(); - return; - } - if (item.value === "share") { - const shareString = buildDisplayShareString(target.name, target.display); - if (onDisplayThemeShared) { - requestThemeShare(target.name, shareString, "display-theme-load"); - return; - } - ctx.ui.notify(shareString, "info"); - themeActionTarget = null; - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - return; - } - if (item.value === "rename" && target.deletable && target.id) { - currentCategory = "display-theme-rename"; - rebuild(); - tui.requestRender(); - return; - } - if (item.value === "delete" && target.deletable && target.id) { - settings.displayThemes = settings.displayThemes.filter((entry) => entry.id !== target.id); - saveSettings(settings); - if (displayPreviewBackup) { - settings.display = { ...displayPreviewBackup }; - if (onSettingsChange) void onSettingsChange(settings); - } - themeActionTarget = null; - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - } - }; - selectList.onCancel = () => { - currentCategory = "display-theme-load"; - rebuild(); - tui.requestRender(); - }; - activeList = selectList; - container.addChild(selectList); - } else { - // Settings list for category - let items: SettingItem[]; - let handleChange: (id: string, value: string) => void; - let backCategory: SettingsCategory = "display"; - - switch (currentCategory) { - case "display-layout": - items = buildDisplayLayoutItems(settings); - break; - case "display-bar": - items = buildDisplayBarItems(settings); - break; - case "display-provider": - items = buildDisplayProviderItems(settings); - break; - case "display-reset": - items = buildDisplayResetItems(settings); - break; - case "display-status": - items = buildDisplayStatusItems(settings); - break; - case "display-divider": - items = buildDisplayDividerItems(settings); - break; - case "display-color": - items = buildDisplayColorItems(settings); - break; - default: - items = []; - } - - const customHandlers: Record> = {}; - if (currentCategory === "display-layout") { - customHandlers.paddingLeft = buildInputSubmenu("Padding Left", (value) => parseInteger(value, 0, 100)); - customHandlers.paddingRight = buildInputSubmenu("Padding Right", (value) => parseInteger(value, 0, 100)); - } - if (currentCategory === "display-color") { - customHandlers.errorThreshold = buildInputSubmenu("Error Threshold (%)", (value) => parseInteger(value, 0, 100)); - customHandlers.warningThreshold = buildInputSubmenu("Warning Threshold (%)", (value) => parseInteger(value, 0, 100)); - customHandlers.successThreshold = buildInputSubmenu("Success Threshold (%)", (value) => parseInteger(value, 0, 100)); - const usageColorItem = items.find((item) => item.id === "usageColorTargets"); - if (usageColorItem) { - usageColorItem.submenu = buildUsageColorSubmenu(); - } - } - if (currentCategory === "display-bar") { - customHandlers.barWidth = buildInputSubmenu("Bar Width", parseBarWidth); - customHandlers.barCharacter = buildInputSubmenu( - "Bar Character", - parseBarCharacter, - undefined, - "Custom bar character(s), set 1 or 2 (fill/empty)", - ); - } - if (currentCategory === "display-provider") { - customHandlers.providerLabel = buildInputSubmenu("Provider Label", parseProviderLabel); - } - if (currentCategory === "display-reset") { - customHandlers.resetTimeContainment = buildInputSubmenu( - "Reset Timer Containment", - parseResetContainment, - undefined, - "Enter 1-2 characters for left/right wrap (e.g. <>).", - ); - } - if (currentCategory === "display-status") { - customHandlers.statusIconPack = buildInputSubmenu( - "Custom Status Icons", - parseStatusIconCustom, - undefined, - "Enter four characters in order: OK, warning, error, unknown (e.g. ✓⚠×?). Applied to none, minor/maintenance, major/critical, and unknown statuses.", - ); - } - if (currentCategory === "display-divider") { - customHandlers.dividerCharacter = buildInputSubmenu("Divider Character", parseDividerCharacter); - customHandlers.dividerBlanks = buildInputSubmenu("Divider Blanks", parseDividerBlanks); - } - attachCustomInputs(items, customHandlers); - - handleChange = (id, value) => { - const previousStatusPack = settings.display.statusIconPack; - settings = applyDisplayChange(settings, id, value); - saveSettings(settings); - if (onSettingsChange) void onSettingsChange(settings); - if (currentCategory === "display-bar" && id === "barType") { - rebuild(); - tui.requestRender(); - return; - } - if (currentCategory === "display-status") { - if (id === "statusIndicatorMode") { - rebuild(); - tui.requestRender(); - return; - } - } - }; - - 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); - } - - // Help text - const usesSettingsList = - Boolean(providerCategory) || - currentCategory === "keybindings" || - currentCategory === "display-layout" || - currentCategory === "display-bar" || - currentCategory === "display-provider" || - currentCategory === "display-reset" || - currentCategory === "display-status" || - currentCategory === "display-divider" || - currentCategory === "display-color"; - if (!usesSettingsList) { - let helpText: string; - if ( - currentCategory === "display-theme-save" || - currentCategory === "display-theme-import-name" || - currentCategory === "display-theme-rename" - ) { - helpText = "Type name • Enter to save • Esc back"; - } else if (currentCategory === "display-theme-import") { - helpText = "Paste theme share string • Enter to import • Esc back"; - } else if ( - currentCategory === "main" || - currentCategory === "providers" || - currentCategory === "display" || - currentCategory === "display-theme" || - currentCategory === "display-theme-load" || - currentCategory === "display-theme-action" || - currentCategory === "display-theme-random" || - currentCategory === "display-theme-restore" - ) { - helpText = "↑↓ 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 (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); - }); -} diff --git a/pi/files/agent/extensions/sub-bar/src/share.ts b/pi/files/agent/extensions/sub-bar/src/share.ts deleted file mode 100644 index 06bd157..0000000 --- a/pi/files/agent/extensions/sub-bar/src/share.ts +++ /dev/null @@ -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).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; - } -} diff --git a/pi/files/agent/extensions/sub-bar/src/shared.ts b/pi/files/agent/extensions/sub-bar/src/shared.ts deleted file mode 100644 index 604a859..0000000 --- a/pi/files/agent/extensions/sub-bar/src/shared.ts +++ /dev/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 = { - 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; - -export const MODEL_MULTIPLIERS: Record = { - "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, -}; diff --git a/pi/files/agent/extensions/sub-bar/src/status.ts b/pi/files/agent/extensions/sub-bar/src/status.ts deleted file mode 100644 index 1d762cf..0000000 --- a/pi/files/agent/extensions/sub-bar/src/status.ts +++ /dev/null @@ -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, Record> = { - 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 { - 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"; - } -} diff --git a/pi/files/agent/extensions/sub-bar/src/storage.ts b/pi/files/agent/extensions/sub-bar/src/storage.ts deleted file mode 100644 index d762d8d..0000000 --- a/pi/files/agent/extensions/sub-bar/src/storage.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/src/types.ts b/pi/files/agent/extensions/sub-bar/src/types.ts deleted file mode 100644 index 02047d0..0000000 --- a/pi/files/agent/extensions/sub-bar/src/types.ts +++ /dev/null @@ -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[]; -}; diff --git a/pi/files/agent/extensions/sub-bar/src/ui/settings-list.ts b/pi/files/agent/extensions/sub-bar/src/ui/settings-list.ts deleted file mode 100644 index 420fd44..0000000 --- a/pi/files/agent/extensions/sub-bar/src/ui/settings-list.ts +++ /dev/null @@ -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", - ), - ); - } -} diff --git a/pi/files/agent/extensions/sub-bar/src/usage/types.ts b/pi/files/agent/extensions/sub-bar/src/usage/types.ts deleted file mode 100644 index eaf4a46..0000000 --- a/pi/files/agent/extensions/sub-bar/src/usage/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Usage data types shared across modules. - */ - -export type { ProviderUsageEntry } from "../../shared.js"; diff --git a/pi/files/agent/extensions/sub-bar/src/utils.ts b/pi/files/agent/extensions/sub-bar/src/utils.ts deleted file mode 100644 index 3e7fdd2..0000000 --- a/pi/files/agent/extensions/sub-bar/src/utils.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/index.ts b/pi/files/agent/extensions/sub-bar/sub-core/index.ts deleted file mode 100644 index ea77e8c..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/index.ts +++ /dev/null @@ -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(target: T, source: Partial): 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 | 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 | undefined; - let statusRefreshInterval: ReturnType | 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 { - 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): 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 { - 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 }).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; - }); -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/package.json b/pi/files/agent/extensions/sub-bar/sub-core/package.json deleted file mode 100644 index 7de2763..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/package.json +++ /dev/null @@ -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": "*" - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/cache.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/cache.ts deleted file mode 100644 index e55e442..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/cache.ts +++ /dev/null @@ -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(); -const cacheSnapshotListeners = new Set(); - -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 { - 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 { - 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( - provider: ProviderName, - ttlMs: number, - fetchFn: () => Promise, - options?: { force?: boolean } -): Promise { - 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 { - 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); - } - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/config.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/config.ts deleted file mode 100644 index d48a408..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/config.ts +++ /dev/null @@ -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; - diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/dependencies.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/dependencies.ts deleted file mode 100644 index 233f219..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/dependencies.ts +++ /dev/null @@ -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, - }; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/errors.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/errors.ts deleted file mode 100644 index b7487d9..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/errors.ts +++ /dev/null @@ -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(["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"; - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/paths.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/paths.ts deleted file mode 100644 index c382e9c..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/paths.ts +++ /dev/null @@ -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"); -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/provider.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/provider.ts deleted file mode 100644 index 1d2a6cc..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/provider.ts +++ /dev/null @@ -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; - - /** - * Fetch current status for this provider (optional) - */ - fetchStatus?(deps: Dependencies): Promise; - - /** - * 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; - - 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>): UsageSnapshot { - return { - provider: this.name, - displayName: this.displayName, - windows: [], - ...data, - }; - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/detection.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/detection.ts deleted file mode 100644 index f7ec476..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/detection.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/anthropic.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/anthropic.ts deleted file mode 100644 index aa29ab6..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/anthropic.ts +++ /dev/null @@ -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 { - 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()); - } - } - -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/antigravity.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/antigravity.ts deleted file mode 100644 index 4d63208..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/antigravity.ts +++ /dev/null @@ -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; -} - -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 { - 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(); - 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 }); - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/codex.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/codex.ts deleted file mode 100644 index 1bc4816..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/codex.ts +++ /dev/null @@ -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 { - const { accessToken, accountId } = loadCodexCredentials(deps); - if (!accessToken) { - return this.emptySnapshot(noCredentials()); - } - - const { controller, clear } = createTimeoutController(API_TIMEOUT_MS); - - try { - const headers: Record = { - 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()); - } - } - -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/copilot.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/copilot.ts deleted file mode 100644 index d6d55d7..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/copilot.ts +++ /dev/null @@ -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 = [ - "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 = {}; - for (const [host, entry] of Object.entries(data as Record)) { - 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 { - 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()); - } - } - -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/gemini.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/gemini.ts deleted file mode 100644 index 033194a..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/gemini.ts +++ /dev/null @@ -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 { - 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 = {}; - 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()); - } - } - -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/kiro.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/kiro.ts deleted file mode 100644 index 8645dab..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/kiro.ts +++ /dev/null @@ -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 { - 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; diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/opencode-go.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/opencode-go.ts deleted file mode 100644 index 0e54a63..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/opencode-go.ts +++ /dev/null @@ -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 { - 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)); - } - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/zai.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/zai.ts deleted file mode 100644 index 2db8206..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/impl/zai.ts +++ /dev/null @@ -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 { - 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 -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/index.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/index.ts deleted file mode 100644 index afadbd9..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Provider registry exports. - */ - -export * from "./registry.js"; diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/metadata.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/metadata.ts deleted file mode 100644 index 6550a97..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/metadata.ts +++ /dev/null @@ -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"; diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/registry.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/registry.ts deleted file mode 100644 index d24fb39..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/registry.ts +++ /dev/null @@ -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 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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/settings.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/settings.ts deleted file mode 100644 index 25c68cd..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/settings.ts +++ /dev/null @@ -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; - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/status.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/providers/status.ts deleted file mode 100644 index 15da024..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/providers/status.ts +++ /dev/null @@ -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 } -): boolean { - return Boolean(providerInstance?.fetchStatus) || Boolean(PROVIDER_METADATA[provider]?.status); -} - -export async function fetchProviderStatusWithFallback( - provider: ProviderName, - providerInstance: { fetchStatus?: (deps: Dependencies) => Promise }, - deps: Dependencies -): Promise { - if (providerInstance.fetchStatus) { - return providerInstance.fetchStatus(deps); - } - return fetchProviderStatus(provider, deps); -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/settings-types.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/settings-types.ts deleted file mode 100644 index 99a2962..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/settings-types.ts +++ /dev/null @@ -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(target: T, source: Partial): 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 { - return deepMerge(getDefaultSettings(), loaded); -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/settings-ui.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/settings-ui.ts deleted file mode 100644 index ed1e4fb..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/settings-ui.ts +++ /dev/null @@ -1 +0,0 @@ -export { showSettingsUI } from "./settings/ui.js"; diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/settings.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/settings.ts deleted file mode 100644 index d0ee7e2..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/settings.ts +++ /dev/null @@ -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; - 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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/behavior.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/settings/behavior.ts deleted file mode 100644 index cc90aa7..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/behavior.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/menu.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/settings/menu.ts deleted file mode 100644 index 975f26a..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/menu.ts +++ /dev/null @@ -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).", - })); -} - diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/tools.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/settings/tools.ts deleted file mode 100644 index 0349dce..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/tools.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/ui.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/settings/ui.ts deleted file mode 100644 index 6eea461..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/settings/ui.ts +++ /dev/null @@ -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 -): Promise { - let settings = getSettings(); - let currentCategory: SettingsCategory = "main"; - let providerOrderSelectedIndex = 0; - let providerOrderReordering = false; - let suppressProviderOrderChange = false; - - return new Promise((resolve) => { - ctx.ui.custom((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 = { - 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> = {}; - 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> = { - 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); - }); -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/status.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/status.ts deleted file mode 100644 index d59a7e4..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/status.ts +++ /dev/null @@ -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; - -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 { - 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 { - 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 { - 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 ""; - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/storage.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/storage.ts deleted file mode 100644 index f26fb07..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/storage.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/storage/lock.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/storage/lock.ts deleted file mode 100644 index e9c965a..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/storage/lock.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/types.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/types.ts deleted file mode 100644 index 8fe6505..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/types.ts +++ /dev/null @@ -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; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/ui/settings-list.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/ui/settings-list.ts deleted file mode 100644 index 49c1ffb..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/ui/settings-list.ts +++ /dev/null @@ -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", - ), - ); - } -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/usage/controller.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/usage/controller.ts deleted file mode 100644 index 38588ef..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/usage/controller.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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, - }; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/usage/fetch.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/usage/fetch.ts deleted file mode 100644 index ef34016..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/usage/fetch.ts +++ /dev/null @@ -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( - items: T[], - limit: number, - mapper: (item: T, index: number) => Promise -): Promise { - if (items.length === 0) return []; - const results = new Array(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 { - 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 { - 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 { - 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 { - 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[]; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/usage/types.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/usage/types.ts deleted file mode 100644 index 48ee631..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/usage/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Usage data types shared across modules. - */ - -export type { ProviderUsageEntry } from "../../../src/shared.js"; diff --git a/pi/files/agent/extensions/sub-bar/sub-core/src/utils.ts b/pi/files/agent/extensions/sub-bar/sub-core/src/utils.ts deleted file mode 100644 index 67a7649..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/src/utils.ts +++ /dev/null @@ -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), - }; -} diff --git a/pi/files/agent/extensions/sub-bar/sub-core/tsconfig.json b/pi/files/agent/extensions/sub-bar/sub-core/tsconfig.json deleted file mode 100644 index 79cf05d..0000000 --- a/pi/files/agent/extensions/sub-bar/sub-core/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["index.ts", "src/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/pi/files/agent/extensions/sub-bar/tsconfig.json b/pi/files/agent/extensions/sub-bar/tsconfig.json deleted file mode 100644 index 0b9334d..0000000 --- a/pi/files/agent/extensions/sub-bar/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["index.ts", "shared.ts", "src/**/*.ts"], - "exclude": ["node_modules"] -}