import { Plugin } from "@opencode/plugin/tui"; import { createResource, createSignal, For, Show } from "solid-js"; import type { QuotaStatus, QuotaWindow } from "./quota.js"; import { QuotaRpc } from "./quota-rpc.js"; function percent(value: number) { return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0); } function duration(seconds: number) { if (seconds % 86_400 === 0) return `${seconds / 86_400}d`; if (seconds % 3_600 === 0) return `${seconds / 3_600}h`; return `${Math.round(seconds / 60)}m`; } function resetTiming(resetsAt: number, now: number) { if (!Number.isFinite(resetsAt) || resetsAt <= 0 || !Number.isFinite(now)) return "unavailable"; const remaining = resetsAt - now; if (remaining <= 0) return "due"; if (remaining < 60_000) return "<1m"; const minutes = Math.floor(remaining / 60_000); const days = Math.floor(minutes / 1_440); if (days > 0) { const hours = Math.floor((minutes % 1_440) / 60); return `${days}d${hours > 0 ? ` ${hours}h` : ""}`; } const hours = Math.floor(minutes / 60); if (hours > 0) { const rest = minutes % 60; return `${hours}h${rest > 0 ? ` ${rest}m` : ""}`; } return `${minutes}m`; } export function formatQuotaWindow(window: QuotaWindow & { remaining: number }, now = Date.now()) { const label = window.label ? `${window.label} ${duration(window.seconds)}` : duration(window.seconds); return `${label}: ${percent(window.remaining)}% ยท ${resetTiming(window.resetsAt, now)}`; } export async function loadQuotaStatus(load: () => Promise) { try { return await load(); } catch { return null; } } export default Plugin.define({ id: "stanp-opencode.quota-tui", setup(context) { const quota = context.client.rpc(QuotaRpc); const [revision, setRevision] = createSignal(0); const retry = setInterval(() => { setRevision((value) => value + 1); }, 60_000); const unsubscribe = quota.events.on("updated", () => { setRevision((value) => value + 1); }); const Slot = (props: { instance: string; centered?: boolean; visible?: () => boolean }) => { const [statuses] = createResource( () => (props.visible?.() === false ? false : `${props.instance}:${revision()}`), async () => { const [chatgpt, claude] = await Promise.all([ loadQuotaStatus(() => quota.status({})), loadQuotaStatus(() => quota.claude({})), ]); return chatgpt || claude ? { chatgpt, claude } : null; }, ); const Section = (section: { title: string; status: QuotaStatus | null }) => ( {(status: () => QuotaStatus) => ( <> {section.title} quota ({status().plan}) {(window) => ( {formatQuotaWindow(window)} )} )} ); const Content = () => (
); return ( }> ); }; const removeHome = context.ui.slot({ after: "prompt.footer", render: () => ( context.ui.router.current().type === "home"} /> ), }); const removeSidebar = context.ui.slot({ append: "sidebar.footer", render: (input) => , }); return () => { removeHome(); removeSidebar(); unsubscribe(); clearInterval(retry); }; }, });