/** * 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; } }); }