@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.
- package/README-es.md +19 -13
- package/README.md +18 -12
- package/bin/quota.mjs +8 -1
- package/dist/lib/balance.d.ts +16 -0
- package/dist/lib/balance.d.ts.map +1 -0
- package/dist/lib/balance.js +45 -0
- package/dist/lib/balance.js.map +1 -0
- package/dist/lib/opencode-go-config.d.ts +6 -1
- package/dist/lib/opencode-go-config.d.ts.map +1 -1
- package/dist/lib/opencode-go-config.js +29 -3
- package/dist/lib/opencode-go-config.js.map +1 -1
- package/dist/lib/pace.d.ts +28 -0
- package/dist/lib/pace.d.ts.map +1 -0
- package/dist/lib/pace.js +46 -0
- package/dist/lib/pace.js.map +1 -0
- package/dist/lib/refresh-scheduler.d.ts +20 -0
- package/dist/lib/refresh-scheduler.d.ts.map +1 -0
- package/dist/lib/refresh-scheduler.js +57 -0
- package/dist/lib/refresh-scheduler.js.map +1 -0
- package/dist/lib/types.d.ts +19 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/providers/opencode-go.d.ts +8 -4
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +167 -43
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.tsx +58 -83
- package/dist/ui/SidebarPanel.d.ts.map +1 -1
- package/dist/ui/SidebarPanel.tsx +2 -13
- package/dist/ui/cards.d.ts.map +1 -1
- package/dist/ui/cards.tsx +24 -4
- package/images/opencode_quota_extended_cli.png +0 -0
- package/images/opencode_quota_extended_sidebar.png +0 -0
- package/package.json +7 -3
- package/scripts/clean.mjs +8 -0
- package/scripts/postbuild.mjs +56 -0
- package/scripts/setup.mjs +53 -0
- package/dist/lib/sanitize.d.ts +0 -10
- package/dist/lib/sanitize.d.ts.map +0 -1
- package/dist/lib/sanitize.js +0 -14
- package/dist/lib/sanitize.js.map +0 -1
- package/dist/lib/throttle.d.ts +0 -18
- package/dist/lib/throttle.d.ts.map +0 -1
- package/dist/lib/throttle.js +0 -26
- package/dist/lib/throttle.js.map +0 -1
- package/dist/lib/toast-format.d.ts +0 -24
- package/dist/lib/toast-format.d.ts.map +0 -1
- package/dist/lib/toast-format.js +0 -79
- 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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
5
|
-
const
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
185
|
+
const headers = buildHeaders(r.config);
|
|
57
186
|
try {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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
|
|
193
|
+
error: `OpenCode Go API error ${res.status}: ${bodyText.slice(0, 120)}`,
|
|
73
194
|
};
|
|
74
195
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
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
|
-
|
|
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":"
|
|
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,
|
|
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
|
|
3
|
-
import type { TuiPlugin,
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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"}
|
package/dist/ui/SidebarPanel.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
33
|
+
</box>
|
|
45
34
|
</box>
|
|
46
35
|
);
|
|
47
36
|
}
|
package/dist/ui/cards.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
<
|
|
52
|
-
{
|
|
53
|
-
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arandevcode/opencode-quota-extended",
|
|
3
|
-
"version": "0.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
|
-
"
|
|
39
|
-
"
|
|
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 });
|