// opencode TUI plugin exposing /cc-zdr, /cc-debug, /cc-status and /cc-usage, plus a sidebar // panel showing the current toggle state and live CommandCode quota. It flips the shared toggle // file (see ./toggles.ts) that the provider reads on every request, so a change takes effect // without restarting opencode, and reads the alpha billing endpoints (see ./quota.ts) with the // provider's API key for the 5-hour, weekly and monthly meters. // // opencode loads this module's default export (`{ id, tui }`) from a `tui.json` plugin // entry. It is loaded from source (not dist) so opencode's Bun/Solid transform compiles // the JSX and maps `solid-js` / `@opentui/solid` to its internal modules; that keeps this // package free of runtime and build dependencies. The ambient types in ./tui-shims.d.ts // back the JSX and `createSignal` references for `tsc -p tsconfig.tui.json`. // // Note: in this opencode version slash commands and the Ctrl+P palette read the same // `namespace: "palette"` command list, so these entries appear in both; `hidden: true` // would remove them from both. import { appendFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createSignal } from "solid-js"; import { fetchQuota, formatQuota, formatReset, percent, quotaBar, type QuotaResult, type QuotaWindow } from "./quota.js"; import { redact } from "./redact.js"; import { debugFromEnvOrFile, readToggles, toggle, type ToggleName, type Toggles } from "./toggles.js"; type ToastVariant = "info" | "success" | "warning" | "error"; type TuiCommand = { name: string; title?: string; desc?: string; category?: string; namespace?: string; slashName?: string; slashAliases?: string[]; suggested?: boolean; hidden?: boolean; enabled?: boolean | (() => boolean); run: () => void | Promise; }; type TuiTheme = { current: { text: unknown; textMuted: unknown; success: unknown; warning: unknown; error: unknown }; }; type TuiSlotHandler = (ctx: { theme: TuiTheme }, props: { session_id: string }) => unknown; type TuiSlotPlugin = { order?: number; slots: Record }; type TuiProvider = { id?: string; key?: string; options?: Record }; type TuiProviderConfig = { options?: Record }; type TuiState = { config?: { provider?: Record }; provider?: ReadonlyArray; }; type TuiApi = { keymap: { registerLayer(layer: { commands?: readonly TuiCommand[] }): () => void }; ui: { toast(input: { title?: string; message: string; variant?: ToastVariant; duration?: number }): void }; slots: { register(plugin: TuiSlotPlugin): string }; event: { on(type: string, handler: (event?: { type?: string }) => void): () => void }; theme: TuiTheme; state: TuiState; lifecycle: { onDispose(fn: () => void): () => void }; }; const ID = "commandcode-toggles"; const CATEGORY = "CommandCode"; const SIDEBAR_ORDER = 90; const DEFAULT_QUOTA_INTERVAL_MS = 180_000; // Extra TUI-bus signals that mark the end of a turn. `session.idle` is a server // plugin event and may never fire here; these keep the panel fresh regardless. const QUOTA_TRIGGER_EVENTS = ["session.idle", "session.status", "session.updated", "message.updated"] as const; function safeEnv(name: string): string | undefined { try { return typeof process === "undefined" ? undefined : process.env?.[name]; } catch { return undefined; } } /** Debug-only trace to the shared debug file. Silent unless debug is on; never logs the key. */ function trace(...args: unknown[]): void { if (!debugFromEnvOrFile()) return; const file = safeEnv("COMMANDCODE_DEBUG_FILE") ?? join(tmpdir(), "commandcode-debug.log"); const parts = args.map((value) => (typeof value === "string" ? value : safeJson(value))); const line = `[commandcode] ${new Date().toISOString()} [tui-quota] ${parts.join(" ")}`; try { appendFileSync(file, redact(line) + "\n", "utf8"); } catch { // Never let tracing break the panel. } } function safeJson(value: unknown): string { try { return JSON.stringify(value); } catch { return String(value); } } function state(value: boolean | undefined): string { return value === true ? "on" : "off"; } function summary(toggles: Toggles = readToggles()): string { return `zdr=${state(toggles.zdr)}, debug=${state(toggles.debug)}`; } /** Expands an `{env:NAME}` config reference; other templates are treated as unset. */ function resolveRef(value: unknown): string | undefined { if (typeof value !== "string" || value.length === 0) return undefined; const trimmed = value.trim(); const template = /^\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(trimmed); if (template) return template[1] ? safeEnv(template[1]) : undefined; if (/^\{.*\}$/.test(trimmed)) return undefined; return trimmed; } function commandCodeOptions(api: TuiApi): Record | undefined { try { return api.state?.config?.provider?.["commandcode"]?.options; } catch { return undefined; } } function providersOf(api: TuiApi): TuiProvider[] { try { const list = api.state?.provider; if (Array.isArray(list)) return list as TuiProvider[]; // Tolerate a non-array provider state shape instead of throwing. if (list && typeof list === "object") return Object.values(list) as TuiProvider[]; } catch { /* fall through */ } return []; } function resolveApiKeySource(api: TuiApi): { key?: string; source: string } { const provider = providersOf(api).find((entry) => entry?.id === "commandcode"); const options = commandCodeOptions(api); const headers = options?.["headers"] as Record | undefined; const named: Array<[string, unknown]> = [ ["provider.key", provider?.key], ["provider.options.apiKey", provider?.options?.["apiKey"]], ["headers.Authorization", headers?.["Authorization"] ?? headers?.["authorization"]], ["options.apiKey", options?.["apiKey"]], ["COMMANDCODE_API_KEY", safeEnv("COMMANDCODE_API_KEY")], ]; for (const [source, candidate] of named) { const value = resolveRef(candidate); if (value) return { key: value.replace(/^Bearer\s+/i, ""), source }; } return { source: "none" }; } function resolveBaseURL(api: TuiApi): string | undefined { return resolveRef(commandCodeOptions(api)?.["baseURL"]); } function quotaIntervalMs(): number { const raw = Number(safeEnv("COMMANDCODE_QUOTA_INTERVAL_MS")); return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_QUOTA_INTERVAL_MS; } function shortWindow(w: QuotaWindow, nowMs: number): string { const label = w.id === "fiveHour" ? "5h" : w.id === "weekly" ? "7d" : "mo"; const reset = formatReset(w.resetAtMs, nowMs); const tail = reset ? ` · ${reset}` : ""; const amount = w.id === "monthly" ? `$${w.used.toFixed(2)}/$${w.cap.toFixed(2)}` : `${percent(w.used, w.cap)}%`; return `${label} ${quotaBar(w.used, w.cap, 8)} ${amount}${tail}`; } export const tui = async (api: TuiApi): Promise => { const [toggles, setToggles] = createSignal(readToggles()); const [quota, setQuota] = createSignal(null); const [now, setNow] = createSignal(Date.now()); let inflight = false; let pending = false; let debounceTimer: ReturnType | undefined; let lastSessionId: string | undefined; const refresh = (): void => setToggles(readToggles()); const refreshQuota = async (): Promise => { if (inflight) { pending = true; trace("coalesced", "inflight"); return; } inflight = true; try { const { key: apiKey, source } = resolveApiKeySource(api); if (!apiKey) { setQuota({ ok: false, error: { kind: "config", message: "No API key found" } }); setNow(Date.now()); trace("no-key", `source=${source}`); return; } trace("fetch-start", `source=${source}`); const result = await fetchQuota({ apiKey, baseURL: resolveBaseURL(api) }); setQuota(result); if (result.ok) { trace( "fetch-ok", `windows=${result.quota.windows.map((w) => `${w.id}:${w.used}/${w.cap}`).join(",")}`, ); } else { trace("fetch-error", `kind=${result.error.kind}`); } } catch (error) { // Key resolution, state access, and fetch can all throw (e.g. unsynced // api.state); surface it instead of wedging the panel on `loading…`. const message = redact(error instanceof Error ? error.message : String(error)); setQuota({ ok: false, error: { kind: "network", message } }); trace("fetch-throw", message); } finally { inflight = false; setNow(Date.now()); if (pending) { pending = false; trace("flush-pending"); void refreshQuota(); } } }; const scheduleQuotaRefresh = (reason: string, delayMs = 800): void => { trace("scheduled", `reason=${reason}`, `delayMs=${delayMs}`); if (debounceTimer !== undefined) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { debounceTimer = undefined; void refreshQuota(); }, delayMs); }; const flip = (name: ToggleName): void => { const next = toggle(name); setToggles(next); api.ui.toast({ title: CATEGORY, message: `${name} ${state(next[name])} (${summary(next)})`, variant: next[name] === true ? "success" : "info", }); }; function tone(w: QuotaWindow, theme: TuiTheme): unknown { if (w.exceeded || percent(w.used, w.cap) >= 100) return theme.current.error; if (percent(w.used, w.cap) >= 80) return theme.current.warning; return theme.current.success; } // Reads quota()/now() inside the JSX so Solid tracks the signals. Hoisting // `quota()` into a `const` above the return would subscribe once under a // non-tracking owner (the slot re-invokes the handler on session change, // which is why /session "fixed" it) and freeze the panel after first paint. const windowsOf = (result: QuotaResult | null): QuotaWindow[] => result?.ok === true ? result.quota.windows : []; const errorOf = (result: QuotaResult | null): string | undefined => result?.ok === false ? result.error.message : undefined; const Status = () => { const theme = api.theme; return ( CommandCode zdr:{state(toggles().zdr)} debug:{state(toggles().debug)} {windowsOf(quota()).map((w) => ( {shortWindow(w, now())} ))} {errorOf(quota()) !== undefined ? ( quota: {errorOf(quota())} ) : quota() === null ? ( quota: loading… ) : null} ); }; api.slots.register({ order: SIDEBAR_ORDER, slots: { sidebar_content(_ctx, props?: { session_id?: string }) { const id = props?.session_id; if (id !== undefined && id !== lastSessionId) { lastSessionId = id; scheduleQuotaRefresh("session-change"); } return ; }, }, }); api.keymap.registerLayer({ commands: [ { name: `${ID}.zdr`, title: "CommandCode: toggle ZDR header", desc: "Send the x-cmd-zdr header on requests", category: CATEGORY, namespace: "palette", slashName: "cc-zdr", run: () => flip("zdr"), }, { name: `${ID}.debug`, title: "CommandCode: toggle debug tracing", desc: "Write a redacted request and stream trace to a log file", category: CATEGORY, namespace: "palette", slashName: "cc-debug", run: () => flip("debug"), }, { name: `${ID}.status`, title: "CommandCode: show toggle status", desc: "Show the current zdr and debug state", category: CATEGORY, namespace: "palette", slashName: "cc-status", run: () => api.ui.toast({ title: CATEGORY, message: summary(), variant: "info" }), }, { name: `${ID}.usage`, title: "CommandCode: show usage and quota", desc: "Show the 5-hour, weekly and monthly quota from the CommandCode API", category: CATEGORY, namespace: "palette", slashName: "cc-usage", run: async () => { const { key: apiKey } = resolveApiKeySource(api); if (!apiKey) { api.ui.toast({ title: CATEGORY, message: "No API key found. Set provider.commandcode.options.apiKey or COMMANDCODE_API_KEY.", variant: "warning", }); return; } let result: QuotaResult; try { result = await fetchQuota({ apiKey, baseURL: resolveBaseURL(api) }); } catch (error) { const message = redact(error instanceof Error ? error.message : String(error)); setQuota({ ok: false, error: { kind: "network", message } }); trace("usage-throw", message); api.ui.toast({ title: CATEGORY, message, variant: "error" }); return; } if (!result.ok) { setQuota(result); api.ui.toast({ title: CATEGORY, message: result.error.message, variant: "error" }); return; } setQuota(result); setNow(Date.now()); api.ui.toast({ title: CATEGORY, message: formatQuota(result.quota), variant: "info", duration: 15000 }); }, }, ], }); void refreshQuota(); const toggleTimer = setInterval(refresh, 1500); const clockTimer = setInterval(() => setNow(Date.now()), 30_000); const quotaTimer = setInterval(() => void refreshQuota(), quotaIntervalMs()); const unsubs = QUOTA_TRIGGER_EVENTS.map((type) => api.event.on(type, (event) => scheduleQuotaRefresh(event?.type ?? type)), ); api.lifecycle.onDispose(() => { for (const unsub of unsubs) unsub(); if (debounceTimer !== undefined) clearTimeout(debounceTimer); clearInterval(toggleTimer); clearInterval(clockTimer); clearInterval(quotaTimer); }); }; export const id = ID; export default { id: ID, tui };