feat: add quota tracking

This commit is contained in:
2026-09-12 09:51:04 +02:00
parent 4ea9b4a695
commit 411cdd13ff
13 changed files with 1269 additions and 16 deletions
+78
View File
@@ -0,0 +1,78 @@
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 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 percent(value: number) {
return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0);
}
export function formatQuotaWindow(
window: QuotaWindow & { remaining: number; sessionConsumed: number },
) {
const label = window.label
? `${window.label} ${duration(window.seconds)}`
: duration(window.seconds);
return `${label}: ${percent(window.remaining)}% left · ~${percent(window.sessionConsumed)}pp this session`;
}
export async function loadQuotaStatus(load: () => Promise<QuotaStatus | null>) {
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: { sessionID: string }) => {
const [status] = createResource(
() => `${props.sessionID}:${revision()}`,
() => loadQuotaStatus(() => quota.status({ sessionID: props.sessionID })),
);
return (
<Show when={status()}>
<box flexDirection="column">
<text fg={context.theme.text.default}>
<b>ChatGPT quota ({status()!.plan})</b>
</text>
<For each={status()!.windows}>
{(window) => (
<text fg={context.theme.text.muted}>
{formatQuotaWindow(window)}
</text>
)}
</For>
</box>
</Show>
);
};
const remove = context.ui.slot({
append: "sidebar.footer",
render: ({ sessionID }) => <Slot sessionID={sessionID} />,
});
return () => {
remove();
unsubscribe();
clearInterval(retry);
};
},
});