@arandevcode/opencode-quota-extended 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README-es.md +19 -13
  2. package/README.md +18 -12
  3. package/bin/quota.mjs +8 -1
  4. package/dist/lib/balance.d.ts +16 -0
  5. package/dist/lib/balance.d.ts.map +1 -0
  6. package/dist/lib/balance.js +45 -0
  7. package/dist/lib/balance.js.map +1 -0
  8. package/dist/lib/opencode-go-config.d.ts +6 -1
  9. package/dist/lib/opencode-go-config.d.ts.map +1 -1
  10. package/dist/lib/opencode-go-config.js +29 -3
  11. package/dist/lib/opencode-go-config.js.map +1 -1
  12. package/dist/lib/pace.d.ts +28 -0
  13. package/dist/lib/pace.d.ts.map +1 -0
  14. package/dist/lib/pace.js +46 -0
  15. package/dist/lib/pace.js.map +1 -0
  16. package/dist/lib/refresh-scheduler.d.ts +20 -0
  17. package/dist/lib/refresh-scheduler.d.ts.map +1 -0
  18. package/dist/lib/refresh-scheduler.js +57 -0
  19. package/dist/lib/refresh-scheduler.js.map +1 -0
  20. package/dist/lib/types.d.ts +19 -0
  21. package/dist/lib/types.d.ts.map +1 -1
  22. package/dist/providers/opencode-go.d.ts +8 -4
  23. package/dist/providers/opencode-go.d.ts.map +1 -1
  24. package/dist/providers/opencode-go.js +167 -43
  25. package/dist/providers/opencode-go.js.map +1 -1
  26. package/dist/tui.d.ts.map +1 -1
  27. package/dist/tui.tsx +58 -83
  28. package/dist/ui/SidebarPanel.d.ts.map +1 -1
  29. package/dist/ui/SidebarPanel.tsx +2 -13
  30. package/dist/ui/cards.d.ts.map +1 -1
  31. package/dist/ui/cards.tsx +24 -4
  32. package/images/opencode_quota_extended_cli.png +0 -0
  33. package/images/opencode_quota_extended_sidebar.png +0 -0
  34. package/package.json +7 -3
  35. package/scripts/clean.mjs +8 -0
  36. package/scripts/postbuild.mjs +56 -0
  37. package/scripts/setup.mjs +53 -0
  38. package/dist/lib/sanitize.d.ts +0 -10
  39. package/dist/lib/sanitize.d.ts.map +0 -1
  40. package/dist/lib/sanitize.js +0 -14
  41. package/dist/lib/sanitize.js.map +0 -1
  42. package/dist/lib/throttle.d.ts +0 -18
  43. package/dist/lib/throttle.d.ts.map +0 -1
  44. package/dist/lib/throttle.js +0 -26
  45. package/dist/lib/throttle.js.map +0 -1
  46. package/dist/lib/toast-format.d.ts +0 -24
  47. package/dist/lib/toast-format.d.ts.map +0 -1
  48. package/dist/lib/toast-format.js +0 -79
  49. package/dist/lib/toast-format.js.map +0 -1
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * OpenCode Go quota provider.
3
3
  *
4
- * Fetches usage data by scraping the opencode.ai workspace dashboard.
5
- * Requires a workspace ID and auth cookie (either via env vars
6
- * OPENCODE_GO_WORKSPACE_ID / OPENCODE_GO_AUTH_COOKIE, or a sidecar file at
7
- * ~/.config/opencode/opencode-quota/opencode-go.json).
4
+ * Fetches usage data from the opencode.ai console API:
5
+ * GET https://opencode.ai/console/api/go/status
6
+ *
7
+ * Preferred auth: a personal API key (`oc_sk_...`) with All permissions, sent
8
+ * as `Authorization: Bearer`. It also unlocks dollar spend and the credit
9
+ * balance. A legacy browser session cookie is still accepted for backwards
10
+ * compatibility: `{ workspaceId, authCookie }` sent as `x-org-id` + Cookie.
11
+ *
8
12
  * Additional instances can be configured via opencode-go-<suffix>.json files,
9
13
  * each rendered as its own "Opencode Go <suffix>" section.
10
14
  * Returns up to 3 windows: 5h (rolling), Weekly, Monthly.
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-go.d.ts","sourceRoot":"","sources":["../../src/providers/opencode-go.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,YAAY,CAAC;AAyC5D,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,yBAAyB,GAAG,QAAQ,CAgFlF;AAED,eAAO,MAAM,kBAAkB,EAAE,QAG/B,CAAC"}
1
+ {"version":3,"file":"opencode-go.d.ts","sourceRoot":"","sources":["../../src/providers/opencode-go.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,YAAY,CAAC;AAyL5D,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,yBAAyB,GAAG,QAAQ,CA4ElF;AAED,eAAO,MAAM,kBAAkB,EAAE,QAG/B,CAAC"}
@@ -1,27 +1,156 @@
1
1
  import { resolveOpenCodeGoInstanceCached } from "../lib/opencode-go-config.js";
2
2
  import { fetchWithTimeout } from "../lib/http.js";
3
3
  import { clampPercent } from "../lib/reset-format.js";
4
- const DASHBOARD_URL_PREFIX = "https://opencode.ai/workspace/";
5
- const DASHBOARD_URL_SUFFIX = "/go";
4
+ const GO_STATUS_URL = "https://opencode.ai/console/api/go/status";
5
+ const BILLING_STATUS_URL = "https://opencode.ai/console/api/billing/status";
6
+ const AUTO_RECHARGE_URL = "https://opencode.ai/console/api/billing/auto-recharge";
6
7
  const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0";
7
- const WINDOWS = [
8
- { field: "rollingUsage", id: "5h", label: "5h" },
9
- { field: "weeklyUsage", id: "weekly", label: "Weekly" },
10
- { field: "monthlyUsage", id: "monthly", label: "Monthly" },
11
- ];
12
- const RE_FIELD_PCT_FIRST = (field) => new RegExp(`${field}[^=]*=\\s*\\{[^}]*?usagePercent\\s*:\\s*(\\d+)[^}]*?resetInSec\\s*:\\s*(\\d+)`);
13
- const RE_FIELD_RESET_FIRST = (field) => new RegExp(`${field}[^=]*=\\s*\\{[^}]*?resetInSec\\s*:\\s*(\\d+)[^}]*?usagePercent\\s*:\\s*(\\d+)`);
14
- function parseWindow(html, field) {
15
- const m1 = html.match(RE_FIELD_PCT_FIRST(field));
16
- if (m1 && m1[1] !== undefined && m1[2] !== undefined) {
17
- return { usagePercent: Number(m1[1]), resetInSec: Number(m1[2]) };
8
+ /** Parse a microCents value (string or number) into a bigint. */
9
+ function toBigInt(value) {
10
+ if (typeof value === "bigint")
11
+ return value;
12
+ if (typeof value === "number") {
13
+ if (!Number.isFinite(value))
14
+ return null;
15
+ return BigInt(Math.trunc(value));
18
16
  }
19
- const m2 = html.match(RE_FIELD_RESET_FIRST(field));
20
- if (m2 && m2[1] !== undefined && m2[2] !== undefined) {
21
- return { usagePercent: Number(m2[2]), resetInSec: Number(m2[1]) };
17
+ if (typeof value === "string") {
18
+ const trimmed = value.trim();
19
+ if (trimmed === "")
20
+ return null;
21
+ try {
22
+ return BigInt(trimmed);
23
+ }
24
+ catch {
25
+ const n = Number(trimmed);
26
+ return Number.isFinite(n) ? BigInt(Math.trunc(n)) : null;
27
+ }
22
28
  }
23
29
  return null;
24
30
  }
31
+ /** Percent used, computed exactly as the opencode.ai dashboard does. */
32
+ function percentUsed(used, limit) {
33
+ if (limit <= 0n)
34
+ return 0;
35
+ return Number((used * 200n + limit) / (limit * 2n));
36
+ }
37
+ /**
38
+ * Build the Cookie header for the console API. The console authenticates with
39
+ * the `__Host-console_session` cookie (value `st_...`); older setups stored the
40
+ * site `auth` cookie (`Fe26....`), so pick the name by value shape.
41
+ */
42
+ function sessionCookieHeader(value) {
43
+ const name = value.startsWith("st_") ? "__Host-console_session" : "auth";
44
+ return `${name}=${value}`;
45
+ }
46
+ /** Build console API headers for either API-key or legacy cookie auth. */
47
+ function buildHeaders(config) {
48
+ const headers = {
49
+ "User-Agent": USER_AGENT,
50
+ Accept: "application/json",
51
+ };
52
+ if (config.auth === "api-key") {
53
+ headers.Authorization = `Bearer ${config.apiKey}`;
54
+ return headers;
55
+ }
56
+ headers.Cookie = sessionCookieHeader(config.authCookie);
57
+ headers["x-org-id"] = config.workspaceId;
58
+ return headers;
59
+ }
60
+ /** GET a console JSON endpoint. Errors never carry the response body or headers. */
61
+ async function getConsoleJson(url, headers, timeoutMs) {
62
+ const res = await fetchWithTimeout(url, { method: "GET", headers }, timeoutMs);
63
+ if (!res.ok)
64
+ throw new Error(`HTTP ${res.status}`);
65
+ return JSON.parse(await res.text());
66
+ }
67
+ /** Parse the `billing/status` body; null when the available credit is unreadable. */
68
+ function parseAvailableMicroCents(data) {
69
+ if (!data || typeof data !== "object")
70
+ return null;
71
+ const value = toBigInt(data.availableMicroCents);
72
+ return value === null || value < 0n ? null : value.toString();
73
+ }
74
+ /** Parse the `billing/auto-recharge` body; null when it does not carry `enabled`. */
75
+ function parseAutoRecharge(data) {
76
+ if (!data || typeof data !== "object")
77
+ return null;
78
+ const raw = data;
79
+ if (typeof raw.enabled !== "boolean")
80
+ return null;
81
+ const out = { enabled: raw.enabled };
82
+ if (typeof raw.thresholdDollars === "number" && Number.isFinite(raw.thresholdDollars)) {
83
+ out.thresholdDollars = raw.thresholdDollars;
84
+ }
85
+ if (typeof raw.rechargeAmountDollars === "number" && Number.isFinite(raw.rechargeAmountDollars)) {
86
+ out.rechargeAmountDollars = raw.rechargeAmountDollars;
87
+ }
88
+ return out;
89
+ }
90
+ /**
91
+ * Credit balance for the account. Both lookups are best-effort: when the
92
+ * balance itself is missing the result is undefined, and a failed auto-recharge
93
+ * lookup only drops that sub-field.
94
+ */
95
+ async function fetchBalance(headers, timeoutMs, useBalance) {
96
+ const [billing, recharge] = await Promise.allSettled([
97
+ getConsoleJson(BILLING_STATUS_URL, headers, timeoutMs),
98
+ getConsoleJson(AUTO_RECHARGE_URL, headers, timeoutMs),
99
+ ]);
100
+ if (billing.status !== "fulfilled")
101
+ return undefined;
102
+ const availableMicroCents = parseAvailableMicroCents(billing.value);
103
+ if (availableMicroCents === null)
104
+ return undefined;
105
+ const balance = { availableMicroCents, useBalance };
106
+ if (recharge.status === "fulfilled") {
107
+ const autoRecharge = parseAutoRecharge(recharge.value);
108
+ if (autoRecharge)
109
+ balance.autoRecharge = autoRecharge;
110
+ }
111
+ return balance;
112
+ }
113
+ function toIso(value) {
114
+ if (typeof value !== "string" || value.trim() === "")
115
+ return undefined;
116
+ const d = new Date(value);
117
+ if (Number.isNaN(d.getTime()))
118
+ return undefined;
119
+ return d.toISOString();
120
+ }
121
+ function buildWindows(status) {
122
+ const access = status.access;
123
+ if (!access || typeof access !== "object")
124
+ return [];
125
+ const meters = access.meters ?? {};
126
+ const specs = [
127
+ { meter: meters.fiveHour, id: "5h", label: "5h", resetsAt: meters.fiveHour?.resetsAt },
128
+ { meter: meters.week, id: "weekly", label: "Weekly", resetsAt: meters.week?.resetsAt },
129
+ { meter: meters.month, id: "monthly", label: "Monthly", resetsAt: access.endsAt },
130
+ ];
131
+ const windows = [];
132
+ for (const spec of specs) {
133
+ const meter = spec.meter;
134
+ if (!meter)
135
+ continue;
136
+ const limit = toBigInt(meter.limitMicroCents);
137
+ const used = toBigInt(meter.usedMicroCents);
138
+ if (limit === null || used === null)
139
+ continue;
140
+ const window = {
141
+ id: spec.id,
142
+ label: spec.label,
143
+ percentRemaining: clampPercent(100 - percentUsed(used, limit)),
144
+ spentMicroCents: used.toString(),
145
+ limitMicroCents: limit.toString(),
146
+ };
147
+ const resetTimeIso = toIso(spec.resetsAt);
148
+ if (resetTimeIso)
149
+ window.resetTimeIso = resetTimeIso;
150
+ windows.push(window);
151
+ }
152
+ return windows;
153
+ }
25
154
  export function createOpenCodeGoProvider(opts) {
26
155
  const { id, label } = opts;
27
156
  return {
@@ -53,47 +182,42 @@ export function createOpenCodeGoProvider(opts) {
53
182
  if (r.state === "invalid") {
54
183
  return { ...base, status: "error", error: `Invalid config: ${r.error}` };
55
184
  }
56
- const { workspaceId, authCookie } = r.config;
185
+ const headers = buildHeaders(r.config);
57
186
  try {
58
- const url = `${DASHBOARD_URL_PREFIX}${encodeURIComponent(workspaceId)}${DASHBOARD_URL_SUFFIX}`;
59
- const res = await fetchWithTimeout(url, {
60
- method: "GET",
61
- headers: {
62
- "User-Agent": USER_AGENT,
63
- Accept: "text/html",
64
- Cookie: `auth=${authCookie}`,
65
- },
66
- }, ctx.requestTimeoutMs);
187
+ const res = await fetchWithTimeout(GO_STATUS_URL, { method: "GET", headers }, ctx.requestTimeoutMs);
188
+ const bodyText = await res.text();
67
189
  if (!res.ok) {
68
- const text = (await res.text()).slice(0, 120);
69
190
  return {
70
191
  ...base,
71
192
  status: "error",
72
- error: `OpenCode Go dashboard error ${res.status}: ${text}`,
193
+ error: `OpenCode Go API error ${res.status}: ${bodyText.slice(0, 120)}`,
73
194
  };
74
195
  }
75
- const html = await res.text();
76
- const now = Date.now();
77
- const windows = [];
78
- for (const spec of WINDOWS) {
79
- const parsed = parseWindow(html, spec.field);
80
- if (!parsed)
81
- continue;
82
- windows.push({
83
- id: spec.id,
84
- label: spec.label,
85
- percentRemaining: clampPercent(100 - parsed.usagePercent),
86
- resetTimeIso: new Date(now + parsed.resetInSec * 1000).toISOString(),
87
- });
196
+ let data;
197
+ try {
198
+ data = JSON.parse(bodyText);
199
+ }
200
+ catch {
201
+ return {
202
+ ...base,
203
+ status: "error",
204
+ error: "OpenCode Go API returned invalid JSON",
205
+ };
88
206
  }
207
+ const status = (data ?? {});
208
+ const windows = buildWindows(status);
89
209
  if (windows.length === 0) {
90
210
  return {
91
211
  ...base,
92
212
  status: "error",
93
- error: "Could not parse any known OpenCode Go dashboard usage windows",
213
+ error: status.access
214
+ ? "Could not parse any known OpenCode Go usage windows"
215
+ : "No active OpenCode Go subscription",
94
216
  };
95
217
  }
96
- return { ...base, status: "ok", windows };
218
+ const useBalance = typeof status.useBalance === "boolean" ? status.useBalance : null;
219
+ const balance = await fetchBalance(headers, ctx.requestTimeoutMs, useBalance);
220
+ return { ...base, status: "ok", windows, ...(balance ? { balance } : {}) };
97
221
  }
98
222
  catch (err) {
99
223
  const message = err instanceof Error ? err.message : String(err);
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-go.js","sourceRoot":"","sources":["../../src/providers/opencode-go.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"opencode-go.js","sourceRoot":"","sources":["../../src/providers/opencode-go.ts"],"names":[],"mappings":"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"}
package/dist/tui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAgB,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAWxF,eAAO,MAAM,mBAAmB,EAAE,SA2GjC,CAAC;AAEF,QAAA,MAAM,YAAY,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAGjD,CAAC;AAEF,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAW1E,eAAO,MAAM,mBAAmB,EAAE,SAkFjC,CAAC;AAEF,QAAA,MAAM,YAAY,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAGjD,CAAC;AAEF,eAAe,YAAY,CAAC"}
package/dist/tui.tsx CHANGED
@@ -1,13 +1,13 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { createSignal, ErrorBoundary, onCleanup } from "solid-js";
3
- import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui";
2
+ import { createSignal, ErrorBoundary } from "solid-js";
3
+ import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui";
4
4
  import { loadCardsConfig } from "./config/loader.js";
5
+ import { createRefreshScheduler } from "./lib/refresh-scheduler.js";
5
6
  import { collectSnapshot } from "./providers/registry.js";
6
7
  import { SidebarPanel } from "./ui/SidebarPanel.js";
7
8
 
8
9
  const REFRESH_INTERVAL_MS = 60_000;
9
- const EVENT_REFRESH_DELAYS_MS = [150, 600] as const;
10
- const MOUNT_RECOVERY_DELAYS_MS = [500, 1500, 4000] as const;
10
+ const MOUNT_RECOVERY_DELAY_MS = 1_500;
11
11
 
12
12
  type CardsSnapshot = Awaited<ReturnType<typeof collectSnapshot>>;
13
13
 
@@ -20,95 +20,70 @@ export const quotaCardsTuiPlugin: TuiPlugin = async (api, _options, _meta) => {
20
20
  return;
21
21
  }
22
22
 
23
- const sessionResources = new Map<string, {
24
- snapshot: () => CardsSnapshot;
25
- dispose: () => void;
26
- }>();
27
-
28
- function acquire(sessionID: string) {
29
- const existing = sessionResources.get(sessionID);
30
- if (existing) return existing;
31
-
32
- const [snapshot, setSnapshot] = createSignal<CardsSnapshot>({
33
- generatedAt: new Date(0).toISOString(),
34
- providers: [],
35
- });
36
-
37
- let inFlight = false;
38
- let queued = false;
39
- let version = 0;
40
- const timers = new Set<ReturnType<typeof setTimeout>>();
41
-
42
- const reload = () => {
43
- if (inFlight) {
44
- queued = true;
45
- version += 1;
46
- return;
47
- }
48
- inFlight = true;
49
- const v = ++version;
50
- collectSnapshot(config).then((next) => {
51
- if (v === version) setSnapshot(next);
52
- }).catch((err) => {
53
- const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
54
- try { process.stderr.write(`[quota-cards] snapshot refresh failed: ${msg}\n`); } catch {}
55
- }).finally(() => {
56
- inFlight = false;
57
- if (queued) { queued = false; reload(); }
58
- });
59
- };
60
-
61
- const queueRefresh = (delay: number) => {
62
- const t = setTimeout(() => { timers.delete(t); reload(); }, delay);
63
- timers.add(t);
64
- };
65
- const scheduleRefresh = () => {
66
- for (const d of EVENT_REFRESH_DELAYS_MS) queueRefresh(d);
67
- };
68
- const scheduleMountRecovery = () => {
69
- for (const d of MOUNT_RECOVERY_DELAYS_MS) queueRefresh(d);
70
- };
71
-
72
- const interval = setInterval(reload, config.refreshIntervalMs ?? REFRESH_INTERVAL_MS);
73
- const unsubs = [
74
- api.event.on("session.updated", (e) => { try { if (e?.properties?.info?.id === sessionID) scheduleRefresh(); } catch {} }),
75
- api.event.on("message.updated", (e) => { try { if (e?.properties?.info?.sessionID === sessionID) scheduleRefresh(); } catch {} }),
76
- api.event.on("message.removed", (e) => { try { if (e?.properties?.sessionID === sessionID) scheduleRefresh(); } catch {} }),
77
- api.event.on("tui.session.select", (e) => { try { if (e?.properties?.sessionID === sessionID) scheduleRefresh(); } catch {} }),
78
- ];
79
-
80
- reload();
81
- scheduleMountRecovery();
82
-
83
- const dispose = () => {
84
- clearInterval(interval);
85
- for (const t of timers) clearTimeout(t);
86
- timers.clear();
87
- for (const u of unsubs) u();
88
- sessionResources.delete(sessionID);
89
- };
90
-
91
- const resource = { snapshot, dispose };
92
- sessionResources.set(sessionID, resource);
93
- return resource;
94
- }
23
+ // Quota is account-wide, not per session: one shared snapshot and one
24
+ // poller serve every mounted sidebar instead of one per session.
25
+ const [snapshot, setSnapshot] = createSignal<CardsSnapshot>({
26
+ generatedAt: new Date(0).toISOString(),
27
+ providers: [],
28
+ });
95
29
 
96
- function SidebarContentView(props: { sessionID: string }) {
97
- const r = acquire(props.sessionID);
98
- onCleanup(() => {
99
- r.dispose();
30
+ let inFlight = false;
31
+ let queued = false;
32
+ let version = 0;
33
+ let disposed = false;
34
+
35
+ const reload = () => {
36
+ if (disposed) return;
37
+ if (inFlight) {
38
+ queued = true;
39
+ return;
40
+ }
41
+ inFlight = true;
42
+ const v = ++version;
43
+ collectSnapshot(config).then((next) => {
44
+ if (v === version && !disposed) setSnapshot(next);
45
+ }).catch((err) => {
46
+ const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
47
+ try { process.stderr.write(`[quota-cards] snapshot refresh failed: ${msg}\n`); } catch {}
48
+ }).finally(() => {
49
+ inFlight = false;
50
+ if (queued) { queued = false; reload(); }
100
51
  });
52
+ };
101
53
 
102
- return <SidebarPanel api={api} config={config} snapshot={r.snapshot} />;
54
+ const scheduler = createRefreshScheduler({
55
+ refresh: reload,
56
+ intervalMs: config.refreshIntervalMs ?? REFRESH_INTERVAL_MS,
57
+ });
58
+ const unsubs = [
59
+ api.event.on("session.updated", () => scheduler.schedule()),
60
+ api.event.on("message.updated", () => scheduler.schedule()),
61
+ api.event.on("message.removed", () => scheduler.schedule()),
62
+ api.event.on("tui.session.select", () => scheduler.schedule()),
63
+ ];
64
+
65
+ const dispose = () => {
66
+ disposed = true;
67
+ scheduler.dispose();
68
+ for (const u of unsubs) u();
69
+ };
70
+ api.lifecycle.onDispose(dispose);
71
+
72
+ scheduler.start();
73
+ reload();
74
+ scheduler.schedule(MOUNT_RECOVERY_DELAY_MS, true);
75
+
76
+ function SidebarContentView() {
77
+ return <SidebarPanel api={api} config={config} snapshot={snapshot} />;
103
78
  }
104
79
 
105
80
  api.slots.register({
106
81
  order: config.cardsSidebar.order,
107
82
  slots: {
108
- sidebar_content(_ctx: unknown, props: { session_id: string }) {
83
+ sidebar_content(_ctx: unknown, _props: { session_id: string }) {
109
84
  return (
110
85
  <ErrorBoundary fallback={<text fg={api.theme.current.error}>Quota error</text>}>
111
- <SidebarContentView sessionID={props.session_id} />
86
+ <SidebarContentView />
112
87
  </ErrorBoundary>
113
88
  );
114
89
  },
@@ -1 +1 @@
1
- {"version":3,"file":"SidebarPanel.d.ts","sourceRoot":"","sources":["../../src/ui/SidebarPanel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,GAAG,EAAE,YAAY,CAAC;IAClB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,aAAa,CAAC;CAC/B,GAAG,GAAG,CAAC,OAAO,CAiCd"}
1
+ {"version":3,"file":"SidebarPanel.d.ts","sourceRoot":"","sources":["../../src/ui/SidebarPanel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,GAAG,EAAE,YAAY,CAAC;IAClB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,aAAa,CAAC;CAC/B,GAAG,GAAG,CAAC,OAAO,CAsBd"}
@@ -19,17 +19,6 @@ export function SidebarPanel(props: {
19
19
  props.snapshot().providers.filter((p) => p.status === "ok").length;
20
20
  const totalCount = () => props.snapshot().providers.length;
21
21
 
22
- const estimatedHeight = () => {
23
- let total = 0
24
- for (const p of props.snapshot().providers) {
25
- if (p.status === "skipped") continue
26
- if (p.status === "no-auth" || p.status === "error") { total += 1; continue }
27
- const n = p.windows?.length ?? 1
28
- total += 4 * n + 1 // title + gap + n*3 rows + (n-1)*gaps = 4n+1
29
- }
30
- return Math.min(total, 50)
31
- }
32
-
33
22
  return (
34
23
  <box flexDirection="column" gap={0}>
35
24
  <box flexDirection="row" gap={1}>
@@ -37,11 +26,11 @@ export function SidebarPanel(props: {
37
26
  Quota ({providerCount()}/{totalCount()})
38
27
  </text>
39
28
  </box>
40
- <scrollbox viewportCulling={true} height={estimatedHeight()} flexDirection="column" gap={1}>
29
+ <box flexDirection="column" gap={1}>
41
30
  <For each={props.snapshot().providers}>
42
31
  {(p) => <ProviderCards api={props.api} provider={p} showOnlyMultiWindow={props.config.showOnlyMultiWindow} />}
43
32
  </For>
44
- </scrollbox>
33
+ </box>
45
34
  </box>
46
35
  );
47
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../../src/ui/cards.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,iBAAiB,CAAC;AAIlE,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,GAAG,EAAE;QAAE,KAAK,EAAE;YAAE,OAAO,EAAE,eAAe,CAAA;SAAE,CAAA;KAAE,CAAC;IAC7C,QAAQ,EAAE,aAAa,CAAC;IACxB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,GAAG,CAAC,OAAO,CA+Cd"}
1
+ {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../../src/ui/cards.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,iBAAiB,CAAC;AAMlE,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,GAAG,EAAE;QAAE,KAAK,EAAE;YAAE,OAAO,EAAE,eAAe,CAAA;SAAE,CAAA;KAAE,CAAC;IAC7C,QAAQ,EAAE,aAAa,CAAC;IACxB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,GAAG,CAAC,OAAO,CAmDd"}
package/dist/ui/cards.tsx CHANGED
@@ -1,11 +1,13 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { For } from "solid-js";
2
+ import { For, createMemo } from "solid-js";
3
3
  import { TextAttributes } from "@opentui/core";
4
4
  import type { JSX } from "@opentui/solid";
5
5
  import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui";
6
6
  import type { ProviderQuota, QuotaWindow } from "../lib/types.js";
7
7
  import { trafficLight, trafficLightColorKey } from "./progress-bar.js";
8
8
  import { formatResetCombined } from "../lib/reset-format.js";
9
+ import { formatPace, quotaPace } from "../lib/pace.js";
10
+ import { formatBalance, formatWindowSpend } from "../lib/balance.js";
9
11
 
10
12
  export function ProviderCards(props: {
11
13
  api: { theme: { current: TuiThemeCurrent } };
@@ -15,6 +17,7 @@ export function ProviderCards(props: {
15
17
  const theme = () => props.api.theme.current;
16
18
  const muted = () => theme().textMuted;
17
19
  const errorColor = () => theme().error;
20
+ const credit = () => formatBalance(props.provider.balance);
18
21
 
19
22
  if (props.provider.status === "skipped") return null;
20
23
 
@@ -48,9 +51,12 @@ export function ProviderCards(props: {
48
51
 
49
52
  return (
50
53
  <box flexDirection="column" gap={0} width="100%">
51
- <text fg={theme().text} attributes={TextAttributes.BOLD}>
52
- {props.provider.label}
53
- </text>
54
+ <box flexDirection="row" gap={1} width="100%">
55
+ <text fg={theme().text} attributes={TextAttributes.BOLD}>
56
+ {props.provider.label}
57
+ </text>
58
+ {credit() ? <text fg={theme().accent}>{credit()}</text> : null}
59
+ </box>
54
60
  <box flexDirection="column" gap={1} width="100%">
55
61
  <For each={props.provider.windows}>
56
62
  {(window) => <WindowRow api={props.api} window={window} />}
@@ -74,6 +80,14 @@ function WindowRow(props: {
74
80
  };
75
81
  const label = () => windowDisplayLabel(props.window);
76
82
  const reset = () => formatReset(props.window.resetTimeIso);
83
+ const spend = () => formatWindowSpend(props.window);
84
+ const pace = createMemo(() => quotaPace(props.window));
85
+ const paceText = () => formatPace(props.window);
86
+ const paceColor = () => {
87
+ const reading = pace();
88
+ if (!reading || reading.pace === "on-pace") return muted();
89
+ return reading.pace === "buffer" ? theme().success : theme().error;
90
+ };
77
91
 
78
92
  return (
79
93
  <box flexDirection="column" gap={0} width="100%">
@@ -86,6 +100,12 @@ function WindowRow(props: {
86
100
  <text fg={barColor()}>{percent()}% left</text>
87
101
  <text fg={muted()}>{100 - percent()}% used</text>
88
102
  </box>
103
+ {spend() || paceText() ? (
104
+ <box flexDirection="row" gap={0} justifyContent="space-between" width="100%">
105
+ <text fg={muted()}>{spend()}</text>
106
+ <text fg={paceColor()}>{paceText()}</text>
107
+ </box>
108
+ ) : null}
89
109
  </box>
90
110
  );
91
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arandevcode/opencode-quota-extended",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "OpenCode plugin: quota usage cards with theme-colored gauge progress bars (5h / weekly / monthly) for OpenAI, GitHub Copilot and OpenCode Go in the TUI sidebar.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,13 +30,17 @@
30
30
  "dist",
31
31
  "bin",
32
32
  "images",
33
+ "scripts/clean.mjs",
34
+ "scripts/postbuild.mjs",
35
+ "scripts/setup.mjs",
33
36
  "README.md",
34
37
  "README-es.md",
35
38
  "LICENSE"
36
39
  ],
37
40
  "scripts": {
38
- "build": "tsc && node scripts/postbuild.mjs",
39
- "prepublish": "tsc && node scripts/postbuild.mjs",
41
+ "clean": "node scripts/clean.mjs",
42
+ "build": "npm run clean && tsc && node scripts/postbuild.mjs",
43
+ "prepublish": "npm run build",
40
44
  "build:watch": "tsc --watch",
41
45
  "typecheck": "tsc --noEmit",
42
46
  "quota": "node bin/quota.mjs",
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { rm } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { join } from "node:path";
5
+
6
+ const ROOT = fileURLToPath(new URL("..", import.meta.url));
7
+
8
+ await rm(join(ROOT, "dist"), { recursive: true, force: true });