oc-go-usage-display 1.0.2 → 2.0.0
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.md +249 -68
- package/bin/lib.js +433 -29
- package/bin/oc-go-usage-display-init.js +42 -13
- package/bin/oc-go-usage-display-remove.js +38 -0
- package/bin/oc-go-usage-display-show.js +47 -39
- package/bin/oc-go-usage-display-status.js +33 -42
- package/bin/oc-go-usage-display-update.js +27 -14
- package/dist/helpers.d.ts +116 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +568 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +258 -278
- package/dist/index.js.map +1 -1
- package/dist/plugins/oc-go-usage-display.kilo.ts +678 -0
- package/dist/plugins/oc-go-usage-display.kilo.tsx +1272 -0
- package/dist/plugins/oc-go-usage-display.ts +678 -0
- package/dist/plugins/oc-go-usage-display.tsx +1018 -0
- package/dist/shared.d.ts +96 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +575 -0
- package/dist/shared.js.map +1 -0
- package/dist/tui-shared.d.ts +109 -0
- package/dist/tui-shared.d.ts.map +1 -0
- package/dist/tui-shared.js +372 -0
- package/dist/tui-shared.js.map +1 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +226 -377
- package/dist/tui.js.map +1 -1
- package/dist/tui.kilo.d.ts +8 -0
- package/dist/tui.kilo.d.ts.map +1 -0
- package/dist/tui.kilo.js +502 -0
- package/dist/tui.kilo.js.map +1 -0
- package/package.json +47 -13
- package/src/helpers.ts +733 -0
- package/src/index.ts +331 -304
- package/src/shared.ts +696 -0
- package/src/tui-shared.tsx +705 -0
- package/src/tui.kilo.tsx +816 -0
- package/src/tui.tsx +356 -465
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
// oc-go-usage-display: opencode tui plugin bundle (self-contained)
|
|
2
|
+
|
|
3
|
+
// src/tui.tsx
|
|
4
|
+
import { For as For2, Show as Show2, createMemo as createMemo2, createSignal as createSignal2 } from "solid-js";
|
|
5
|
+
|
|
6
|
+
// src/shared.ts
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
function resolveHomedir() {
|
|
11
|
+
const envHome = process.env.HOME;
|
|
12
|
+
if (envHome && envHome.length > 0) return envHome;
|
|
13
|
+
return os.homedir();
|
|
14
|
+
}
|
|
15
|
+
var ENV_PREFIX = {
|
|
16
|
+
opencode: "OPENCODE_OC_GO_",
|
|
17
|
+
kilo: "KILO_OC_GO_"
|
|
18
|
+
};
|
|
19
|
+
function hostEnvName(host, suffix) {
|
|
20
|
+
return `${ENV_PREFIX[host]}${suffix}`;
|
|
21
|
+
}
|
|
22
|
+
function hostEnv(host, suffix, env = process.env) {
|
|
23
|
+
return env?.[hostEnvName(host, suffix)];
|
|
24
|
+
}
|
|
25
|
+
function homedirOrEmpty(homedir2) {
|
|
26
|
+
try {
|
|
27
|
+
return homedir2();
|
|
28
|
+
} catch {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function resolveHostRoots(host, env = process.env, homedir2 = resolveHomedir) {
|
|
33
|
+
const home = homedirOrEmpty(homedir2);
|
|
34
|
+
const configRoot = toNonEmptyString(env.XDG_CONFIG_HOME) ?? safeJoinPath(home, ".config");
|
|
35
|
+
const dataRoot = toNonEmptyString(env.XDG_DATA_HOME) ?? safeJoinPath(home, ".local", "share");
|
|
36
|
+
if (host === "kilo") {
|
|
37
|
+
return {
|
|
38
|
+
configDir: toNonEmptyString(env.KILO_CONFIG_DIR) ?? safeJoinPath(configRoot, "kilo"),
|
|
39
|
+
dataDir: safeJoinPath(dataRoot, "kilo")
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
configDir: safeJoinPath(configRoot, "opencode"),
|
|
44
|
+
dataDir: safeJoinPath(dataRoot, "opencode")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
var CONFIG_DIR = resolveHostRoots("opencode").configDir;
|
|
48
|
+
function toPathSegment(value) {
|
|
49
|
+
if (typeof value === "string") return value;
|
|
50
|
+
try {
|
|
51
|
+
return String(value);
|
|
52
|
+
} catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function safeJoinPath(base, ...segments) {
|
|
57
|
+
return path.join(toPathSegment(base), ...segments.map(toPathSegment));
|
|
58
|
+
}
|
|
59
|
+
function authJsonPaths(host = "opencode", env = process.env, homedir2 = resolveHomedir) {
|
|
60
|
+
const roots = resolveHostRoots(host, env, homedir2);
|
|
61
|
+
return [safeJoinPath(roots.dataDir, "auth.json"), safeJoinPath(roots.configDir, "auth.json")];
|
|
62
|
+
}
|
|
63
|
+
function isRecord(value) {
|
|
64
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
function toFiniteNumber(value) {
|
|
67
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function toUsageCount(value) {
|
|
71
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
72
|
+
return Math.max(value, 0);
|
|
73
|
+
}
|
|
74
|
+
function toNonEmptyString(value) {
|
|
75
|
+
if (typeof value !== "string") return null;
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
78
|
+
}
|
|
79
|
+
function errorMessage(error) {
|
|
80
|
+
if (error === null || error === void 0) return "unknown error";
|
|
81
|
+
if (error instanceof Error) {
|
|
82
|
+
return toNonEmptyString(error.message) ?? toNonEmptyString(error.name) ?? "unknown error";
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return toNonEmptyString(String(error)) ?? "unknown error";
|
|
86
|
+
} catch {
|
|
87
|
+
return "unknown error";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var GO_PROVIDER_ID = "opencode-go";
|
|
91
|
+
var KILO_MODEL_NAME_MAX_CHARS = 19;
|
|
92
|
+
var SIDEBAR_COLLAPSED_GLYPH = "\u25B6";
|
|
93
|
+
var SIDEBAR_EXPANDED_GLYPH = "\u25BE";
|
|
94
|
+
var OPENCODE_MODEL_NAME_MAX_CHARS = 12;
|
|
95
|
+
var PERCENT_CELL_WIDTH = 5;
|
|
96
|
+
var TOP_GO_MODELS_LIMIT = 3;
|
|
97
|
+
var GO_MODEL_MIX_NAME_MAX_CHARS = 6;
|
|
98
|
+
var GO_MODEL_MIX_BUDGET = 28;
|
|
99
|
+
var GO_MODEL_MIX_SEPARATOR = "\xB7";
|
|
100
|
+
var TOP_GO_MODELS_LABEL = "Top Go models";
|
|
101
|
+
var MODELS_EMPTY_LABEL = "No model usage yet";
|
|
102
|
+
var SECONDS_PER_MINUTE = 60;
|
|
103
|
+
var SECONDS_PER_HOUR = 3600;
|
|
104
|
+
var SECONDS_PER_DAY = 86400;
|
|
105
|
+
var SECONDS_PER_WEEK = 604800;
|
|
106
|
+
function formatResetDuration(totalSec) {
|
|
107
|
+
if (totalSec === null || !Number.isFinite(totalSec) || totalSec < 0) return null;
|
|
108
|
+
const sec = Math.floor(totalSec);
|
|
109
|
+
if (sec >= SECONDS_PER_WEEK) {
|
|
110
|
+
const weeks = Math.floor(sec / SECONDS_PER_WEEK);
|
|
111
|
+
const days = Math.floor(sec % SECONDS_PER_WEEK / SECONDS_PER_DAY);
|
|
112
|
+
return `${weeks}w ${days}d`;
|
|
113
|
+
}
|
|
114
|
+
if (sec >= SECONDS_PER_DAY) {
|
|
115
|
+
const days = Math.floor(sec / SECONDS_PER_DAY);
|
|
116
|
+
const hours2 = Math.floor(sec % SECONDS_PER_DAY / SECONDS_PER_HOUR);
|
|
117
|
+
return `${days}d ${hours2}h`;
|
|
118
|
+
}
|
|
119
|
+
const hours = Math.floor(sec / SECONDS_PER_HOUR);
|
|
120
|
+
const minutes = Math.floor(sec % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
|
|
121
|
+
if (hours > 0) return `${hours}h${minutes}m`;
|
|
122
|
+
if (minutes > 0) return `${minutes}m`;
|
|
123
|
+
return `${sec}s`;
|
|
124
|
+
}
|
|
125
|
+
function readAuthJsonApiKey(host = "opencode", env = process.env, homedir2 = resolveHomedir) {
|
|
126
|
+
for (const authPath of authJsonPaths(host, env, homedir2)) {
|
|
127
|
+
let raw;
|
|
128
|
+
try {
|
|
129
|
+
raw = fs.readFileSync(authPath, "utf8");
|
|
130
|
+
} catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(raw);
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!isRecord(parsed)) continue;
|
|
140
|
+
const goEntry = parsed["opencode-go"];
|
|
141
|
+
const fallbackEntry = parsed["opencode"];
|
|
142
|
+
const goKey = isRecord(goEntry) ? toNonEmptyString(goEntry.key) : null;
|
|
143
|
+
if (goKey) return goKey;
|
|
144
|
+
const fallbackKey = isRecord(fallbackEntry) ? toNonEmptyString(fallbackEntry.key) : null;
|
|
145
|
+
if (fallbackKey) return fallbackKey;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
function usableCountdown(seconds) {
|
|
150
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) return null;
|
|
151
|
+
return seconds;
|
|
152
|
+
}
|
|
153
|
+
function secondsUntilReset(resetsAt, now) {
|
|
154
|
+
const instant = toNonEmptyString(resetsAt);
|
|
155
|
+
if (instant === null) return null;
|
|
156
|
+
return usableCountdown(Math.round((Date.parse(instant) - now) / 1e3));
|
|
157
|
+
}
|
|
158
|
+
var LIMITED_STATUSES = /* @__PURE__ */ new Set(["rate-limited", "limited", "exhausted", "capped"]);
|
|
159
|
+
function isLimitedStatus(status) {
|
|
160
|
+
if (status === null) return false;
|
|
161
|
+
const normalized = status.trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
162
|
+
return LIMITED_STATUSES.has(normalized);
|
|
163
|
+
}
|
|
164
|
+
function extractWindow(candidate, now = Date.now()) {
|
|
165
|
+
if (!isRecord(candidate)) return null;
|
|
166
|
+
const percent = toFiniteNumber(
|
|
167
|
+
candidate.percent ?? candidate.usagePercent ?? candidate.usedPercent ?? candidate.value ?? candidate.usage
|
|
168
|
+
);
|
|
169
|
+
if (percent === null) return null;
|
|
170
|
+
const status = toNonEmptyString(candidate.status);
|
|
171
|
+
const resetInSec = usableCountdown(candidate.resetInSec ?? candidate.resetInSeconds) ?? secondsUntilReset(candidate.resetsAt, now);
|
|
172
|
+
const resetText = toNonEmptyString(candidate.resetText ?? candidate.reset ?? null);
|
|
173
|
+
return { percent: Math.round(percent), status, limited: isLimitedStatus(status), resetInSec, resetText };
|
|
174
|
+
}
|
|
175
|
+
function extractSnapshotFromApiPayload(payload, now = Date.now()) {
|
|
176
|
+
if (!isRecord(payload)) return null;
|
|
177
|
+
const containers = [payload];
|
|
178
|
+
for (const key of ["usage", "data", "go"]) {
|
|
179
|
+
if (isRecord(payload[key])) containers.push(payload[key]);
|
|
180
|
+
}
|
|
181
|
+
for (const container of containers) {
|
|
182
|
+
if (!isRecord(container)) continue;
|
|
183
|
+
const rolling = extractWindow(container.rolling ?? container.rollingUsage ?? container["5h"], now);
|
|
184
|
+
const weekly = extractWindow(container.weekly ?? container.weeklyUsage ?? container["7d"], now);
|
|
185
|
+
const monthly = extractWindow(container.monthly ?? container.monthlyUsage ?? container["30d"], now);
|
|
186
|
+
if (rolling !== null || weekly !== null || monthly !== null) {
|
|
187
|
+
return {
|
|
188
|
+
rolling,
|
|
189
|
+
weekly,
|
|
190
|
+
monthly,
|
|
191
|
+
source: "api",
|
|
192
|
+
fetchedAt: now
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function mockSnapshot() {
|
|
199
|
+
return {
|
|
200
|
+
rolling: { percent: 42, status: "active", limited: false, resetInSec: 7543, resetText: null },
|
|
201
|
+
weekly: { percent: 15, status: "active", limited: false, resetInSec: null, resetText: null },
|
|
202
|
+
monthly: { percent: 61, status: "active", limited: false, resetInSec: null, resetText: null },
|
|
203
|
+
source: "mock",
|
|
204
|
+
fetchedAt: Date.now()
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function unavailableSnapshot(error) {
|
|
208
|
+
return {
|
|
209
|
+
rolling: null,
|
|
210
|
+
weekly: null,
|
|
211
|
+
monthly: null,
|
|
212
|
+
source: "unavailable",
|
|
213
|
+
fetchedAt: Date.now(),
|
|
214
|
+
apiUnavailable: true,
|
|
215
|
+
apiError: error
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
var EMPTY_TOKENS = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
|
|
219
|
+
function parseUsageTokens(candidate) {
|
|
220
|
+
if (!isRecord(candidate)) return { ...EMPTY_TOKENS, cache: { ...EMPTY_TOKENS.cache } };
|
|
221
|
+
const cache = isRecord(candidate.cache) ? candidate.cache : {};
|
|
222
|
+
return {
|
|
223
|
+
input: toUsageCount(candidate.input),
|
|
224
|
+
output: toUsageCount(candidate.output),
|
|
225
|
+
reasoning: toUsageCount(candidate.reasoning),
|
|
226
|
+
cache: { read: toUsageCount(cache.read), write: toUsageCount(cache.write) }
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function parseUsageTotals(candidate) {
|
|
230
|
+
if (!isRecord(candidate)) return { steps: 0, cost: 0, tokens: { ...EMPTY_TOKENS, cache: { ...EMPTY_TOKENS.cache } } };
|
|
231
|
+
return {
|
|
232
|
+
steps: toUsageCount(candidate.steps),
|
|
233
|
+
cost: toUsageCount(candidate.cost),
|
|
234
|
+
tokens: parseUsageTokens(candidate.tokens)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function addUsageTokens(left, right) {
|
|
238
|
+
return {
|
|
239
|
+
input: left.input + right.input,
|
|
240
|
+
output: left.output + right.output,
|
|
241
|
+
reasoning: left.reasoning + right.reasoning,
|
|
242
|
+
cache: { read: left.cache.read + right.cache.read, write: left.cache.write + right.cache.write }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function aggregateModelUsageFromMessages(messages) {
|
|
246
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
247
|
+
const sessionIDs = [];
|
|
248
|
+
for (const entry of messages) {
|
|
249
|
+
if (!isRecord(entry) || entry.role !== "assistant") continue;
|
|
250
|
+
const providerID = toNonEmptyString(entry.providerID);
|
|
251
|
+
const modelID = toNonEmptyString(entry.modelID);
|
|
252
|
+
if (providerID === null || modelID === null) continue;
|
|
253
|
+
const parsed = parseUsageTotals(entry);
|
|
254
|
+
const key = `${providerID}/${modelID}`;
|
|
255
|
+
const existing = byModel.get(key);
|
|
256
|
+
if (existing === void 0) {
|
|
257
|
+
byModel.set(key, {
|
|
258
|
+
providerID,
|
|
259
|
+
modelID,
|
|
260
|
+
steps: 1,
|
|
261
|
+
cost: parsed.cost,
|
|
262
|
+
tokens: { ...parsed.tokens, cache: { ...parsed.tokens.cache } }
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
existing.steps += 1;
|
|
266
|
+
existing.cost += parsed.cost;
|
|
267
|
+
existing.tokens = addUsageTokens(existing.tokens, parsed.tokens);
|
|
268
|
+
}
|
|
269
|
+
const sessionID = toNonEmptyString(entry.sessionID);
|
|
270
|
+
if (sessionID !== null && !sessionIDs.includes(sessionID)) sessionIDs.push(sessionID);
|
|
271
|
+
}
|
|
272
|
+
const models = [...byModel.values()];
|
|
273
|
+
const totals = models.reduce(
|
|
274
|
+
(sum, model) => ({
|
|
275
|
+
steps: sum.steps + model.steps,
|
|
276
|
+
cost: sum.cost + model.cost,
|
|
277
|
+
tokens: addUsageTokens(sum.tokens, model.tokens)
|
|
278
|
+
}),
|
|
279
|
+
{ steps: 0, cost: 0, tokens: { ...EMPTY_TOKENS, cache: { ...EMPTY_TOKENS.cache } } }
|
|
280
|
+
);
|
|
281
|
+
return { sessionIDs, totals, models };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/helpers.ts
|
|
285
|
+
function isDisplayMode(value) {
|
|
286
|
+
return value === "sidebar" || value === "statusline" || value === "both";
|
|
287
|
+
}
|
|
288
|
+
function providerIdFromModel(model) {
|
|
289
|
+
if (typeof model !== "string" || model.length === 0) return void 0;
|
|
290
|
+
const provider = model.split("/")[0];
|
|
291
|
+
return provider !== void 0 && provider.length > 0 ? provider : void 0;
|
|
292
|
+
}
|
|
293
|
+
function resolveProviderId(state, sessionId, fallback) {
|
|
294
|
+
try {
|
|
295
|
+
const fromSession = state?.session?.get?.(sessionId)?.model?.providerID;
|
|
296
|
+
if (typeof fromSession === "string" && fromSession.length > 0) return fromSession;
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const fromConfig = providerIdFromModel(state?.config?.model);
|
|
301
|
+
if (fromConfig !== void 0) return fromConfig;
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
return fallback;
|
|
305
|
+
}
|
|
306
|
+
function surfaceSelectionFromDisplayMode(mode) {
|
|
307
|
+
return { sidebar: mode !== "statusline", statusline: mode !== "sidebar" };
|
|
308
|
+
}
|
|
309
|
+
function parseBooleanFlag(value) {
|
|
310
|
+
if (typeof value === "boolean") return value;
|
|
311
|
+
if (typeof value === "number") {
|
|
312
|
+
if (value === 1) return true;
|
|
313
|
+
if (value === 0) return false;
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (typeof value !== "string") return null;
|
|
317
|
+
const normalized = value.trim().toLowerCase();
|
|
318
|
+
if (normalized === "1" || normalized === "true") return true;
|
|
319
|
+
if (normalized === "0" || normalized === "false") return false;
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function isSnapshotEmpty(snapshot) {
|
|
323
|
+
return snapshot.rolling === null && snapshot.weekly === null && snapshot.monthly === null;
|
|
324
|
+
}
|
|
325
|
+
function relevantReset(snapshot) {
|
|
326
|
+
const rows = buildPlanRows(snapshot);
|
|
327
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
328
|
+
const row = rows[i];
|
|
329
|
+
if (row?.reset != null) return { label: row.label, text: row.reset };
|
|
330
|
+
}
|
|
331
|
+
return soonestReset(snapshot);
|
|
332
|
+
}
|
|
333
|
+
function soonestReset(snapshot) {
|
|
334
|
+
const windows = [
|
|
335
|
+
["5h", snapshot.rolling],
|
|
336
|
+
["7d", snapshot.weekly],
|
|
337
|
+
["30d", snapshot.monthly]
|
|
338
|
+
];
|
|
339
|
+
let soonest = null;
|
|
340
|
+
for (const [name, window] of windows) {
|
|
341
|
+
if (window === null) continue;
|
|
342
|
+
const seconds = window.resetInSec;
|
|
343
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) continue;
|
|
344
|
+
if (soonest !== null && seconds >= soonest.seconds) continue;
|
|
345
|
+
soonest = { label: name, seconds };
|
|
346
|
+
}
|
|
347
|
+
if (soonest === null) return null;
|
|
348
|
+
const text = formatResetDuration(soonest.seconds);
|
|
349
|
+
return text === null ? null : { label: soonest.label, text };
|
|
350
|
+
}
|
|
351
|
+
function formatStatusline(snapshot) {
|
|
352
|
+
const rolling = snapshot.rolling === null ? "5h n/a" : `5h ${snapshot.rolling.percent}%`;
|
|
353
|
+
const weekly = snapshot.weekly === null ? "7d n/a" : `7d ${snapshot.weekly.percent}%`;
|
|
354
|
+
const monthly = snapshot.monthly === null ? "30d n/a" : `30d ${snapshot.monthly.percent}%`;
|
|
355
|
+
const windows = `Go ${rolling} | ${weekly} | ${monthly}`;
|
|
356
|
+
const reset = relevantReset(snapshot);
|
|
357
|
+
return reset === null ? windows : `${windows} \xB7 resets in ${reset.text}`;
|
|
358
|
+
}
|
|
359
|
+
var WARNING_PERCENT = 75;
|
|
360
|
+
var ERROR_PERCENT = 90;
|
|
361
|
+
function usageMeterSeverity(window) {
|
|
362
|
+
if (window.limited) return "error";
|
|
363
|
+
return meterSeverityForPercent(window.percent);
|
|
364
|
+
}
|
|
365
|
+
function meterSeverityForPercent(percent) {
|
|
366
|
+
if (!Number.isFinite(percent)) return "muted";
|
|
367
|
+
if (percent >= ERROR_PERCENT) return "error";
|
|
368
|
+
if (percent >= WARNING_PERCENT) return "warning";
|
|
369
|
+
return "muted";
|
|
370
|
+
}
|
|
371
|
+
function meterFillPercent(percent) {
|
|
372
|
+
if (!Number.isFinite(percent)) return 0;
|
|
373
|
+
return Math.min(Math.max(percent, 0), 100);
|
|
374
|
+
}
|
|
375
|
+
function buildPlanRows(snapshot) {
|
|
376
|
+
const windows = [
|
|
377
|
+
["5h", snapshot.rolling],
|
|
378
|
+
["7d", snapshot.weekly],
|
|
379
|
+
["30d", snapshot.monthly]
|
|
380
|
+
];
|
|
381
|
+
const rows = [];
|
|
382
|
+
for (const [label, window] of windows) {
|
|
383
|
+
if (window === null) continue;
|
|
384
|
+
rows.push({
|
|
385
|
+
label,
|
|
386
|
+
percent: window.percent,
|
|
387
|
+
severity: usageMeterSeverity(window),
|
|
388
|
+
// A countdown only tells the user something once the window is capped;
|
|
389
|
+
// before that the schedule is noise, and the header rows already show it.
|
|
390
|
+
reset: window.limited ? formatResetDuration(window.resetInSec) ?? window.resetText : null
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return rows;
|
|
394
|
+
}
|
|
395
|
+
var COUNT_FORMAT = new Intl.NumberFormat("en-US");
|
|
396
|
+
var CURRENCY_FORMAT = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
|
|
397
|
+
function formatUsageCount(value) {
|
|
398
|
+
return COUNT_FORMAT.format(toUsageCount(value));
|
|
399
|
+
}
|
|
400
|
+
function formatUsageCost(value) {
|
|
401
|
+
return CURRENCY_FORMAT.format(toUsageCount(value));
|
|
402
|
+
}
|
|
403
|
+
function usageTokenCount(tokens) {
|
|
404
|
+
return toUsageCount(tokens.input) + toUsageCount(tokens.output) + toUsageCount(tokens.reasoning) + toUsageCount(tokens.cache.read) + toUsageCount(tokens.cache.write);
|
|
405
|
+
}
|
|
406
|
+
function goSharePercent(modelTokens, totalGoTokens) {
|
|
407
|
+
const total = toUsageCount(totalGoTokens);
|
|
408
|
+
if (total === 0) return 0;
|
|
409
|
+
return toUsageCount(modelTokens) / total * 100;
|
|
410
|
+
}
|
|
411
|
+
function truncateModelName(value, max = KILO_MODEL_NAME_MAX_CHARS) {
|
|
412
|
+
if (value.length <= max) return value;
|
|
413
|
+
const budget = Math.max(max - 1, 0);
|
|
414
|
+
return `${value.slice(0, budget)}\u2026`;
|
|
415
|
+
}
|
|
416
|
+
function displayModelName(name) {
|
|
417
|
+
return name.trim().replace(/^[^:]+:\s+/, "").replace(/^[^/\s]+\/(?=[^/]+$)/, "").replace(/\s*\([^)]*%\s*off[^)]*\)\s*$/i, "").replace(/^([A-Za-z]{2,})(?=\d)/, "$1 ").replace(/\s+/g, " ");
|
|
418
|
+
}
|
|
419
|
+
function modelDisplayName(catalogName, modelID, max = KILO_MODEL_NAME_MAX_CHARS) {
|
|
420
|
+
const raw = toNonEmptyString(catalogName) ?? toNonEmptyString(modelID) ?? "";
|
|
421
|
+
return truncateModelName(displayModelName(raw), max);
|
|
422
|
+
}
|
|
423
|
+
function shortModelName(value, max = GO_MODEL_MIX_NAME_MAX_CHARS) {
|
|
424
|
+
const trimmed = value.trim().toLowerCase();
|
|
425
|
+
if (trimmed.length === 0) return "";
|
|
426
|
+
const head = trimmed.split(/[-._/]/)[0] ?? trimmed;
|
|
427
|
+
return truncateModelName(head, max);
|
|
428
|
+
}
|
|
429
|
+
var PLAN_LABEL_WIDTH = 3;
|
|
430
|
+
function formatPercentCell(percent, width = PERCENT_CELL_WIDTH) {
|
|
431
|
+
const cells = Math.max(Math.floor(width), 1);
|
|
432
|
+
const rounded = Number.isFinite(percent) ? Math.round(percent) : 0;
|
|
433
|
+
return `${`${rounded}%`}`.padStart(cells, " ");
|
|
434
|
+
}
|
|
435
|
+
function formatTokenCount(value) {
|
|
436
|
+
const count = toUsageCount(value);
|
|
437
|
+
if (count >= 1e9) return `${trimTrailingZeros((count / 1e9).toFixed(2))}B`;
|
|
438
|
+
if (count >= 1e6) return `${trimTrailingZeros((count / 1e6).toFixed(2))}M`;
|
|
439
|
+
if (count >= 1e3) return `${Math.round(count / 1e3)}K`;
|
|
440
|
+
return COUNT_FORMAT.format(count);
|
|
441
|
+
}
|
|
442
|
+
function trimTrailingZeros(value) {
|
|
443
|
+
return value.replace(/\.?0+$/, "");
|
|
444
|
+
}
|
|
445
|
+
function weightGoModels(models, limit = TOP_GO_MODELS_LIMIT) {
|
|
446
|
+
const goModels = models.filter((model) => model.providerID === GO_PROVIDER_ID);
|
|
447
|
+
const goTokens = goModels.reduce((sum, model) => sum + usageTokenCount(model.tokens), 0);
|
|
448
|
+
const ranked = [...goModels].sort(
|
|
449
|
+
(left, right) => usageTokenCount(right.tokens) - usageTokenCount(left.tokens) || toUsageCount(right.steps) - toUsageCount(left.steps) || left.modelID.localeCompare(right.modelID)
|
|
450
|
+
);
|
|
451
|
+
const top = ranked.slice(0, Math.max(Math.floor(limit), 0));
|
|
452
|
+
const listedTokens = top.reduce((sum, model) => sum + usageTokenCount(model.tokens), 0);
|
|
453
|
+
return {
|
|
454
|
+
models: top.map((model) => {
|
|
455
|
+
const tokens = usageTokenCount(model.tokens);
|
|
456
|
+
const share = goSharePercent(tokens, goTokens);
|
|
457
|
+
return {
|
|
458
|
+
providerID: model.providerID,
|
|
459
|
+
modelID: model.modelID,
|
|
460
|
+
tokens,
|
|
461
|
+
share,
|
|
462
|
+
steps: toUsageCount(model.steps),
|
|
463
|
+
cost: toUsageCount(model.cost)
|
|
464
|
+
};
|
|
465
|
+
}),
|
|
466
|
+
goModels: goModels.length,
|
|
467
|
+
goTokens,
|
|
468
|
+
listedTokens,
|
|
469
|
+
listedShare: goSharePercent(listedTokens, goTokens),
|
|
470
|
+
otherCount: Math.max(goModels.length - top.length, 0),
|
|
471
|
+
steps: goModels.reduce((sum, model) => sum + toUsageCount(model.steps), 0),
|
|
472
|
+
cost: goModels.reduce((sum, model) => sum + toUsageCount(model.cost), 0)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function buildModelMixSummary(entries, budget = GO_MODEL_MIX_BUDGET) {
|
|
476
|
+
const first = entries[0];
|
|
477
|
+
if (first === void 0) return "";
|
|
478
|
+
const parts = [];
|
|
479
|
+
let used = 0;
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
const part = `${entry.name} ${Math.round(entry.share)}%`;
|
|
482
|
+
const width = part.length + (parts.length === 0 ? 0 : GO_MODEL_MIX_SEPARATOR.length);
|
|
483
|
+
if (parts.length > 0 && used + width > budget) break;
|
|
484
|
+
parts.push(part);
|
|
485
|
+
used += width;
|
|
486
|
+
}
|
|
487
|
+
return parts.join(GO_MODEL_MIX_SEPARATOR);
|
|
488
|
+
}
|
|
489
|
+
function buildGoModelFooters(weights) {
|
|
490
|
+
if (weights.models.length === 0) return [];
|
|
491
|
+
return [
|
|
492
|
+
`${formatTokenCount(weights.listedTokens)} of ${formatTokenCount(weights.goTokens)} Go tokens`,
|
|
493
|
+
`${formatUsageCount(weights.steps)} steps \xB7 ${formatUsageCost(weights.cost)}`
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/tui-shared.tsx
|
|
498
|
+
import { For, Show, createEffect, createMemo, createSignal } from "solid-js";
|
|
499
|
+
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
500
|
+
var API_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
501
|
+
var POLL_INTERVAL_MS = 6e4;
|
|
502
|
+
var EVENT_TTL_MS = 15e3;
|
|
503
|
+
var DEBOUNCE_MS = 5e3;
|
|
504
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
505
|
+
var LOG_SERVICE = "oc-go-usage-display";
|
|
506
|
+
var KV_DISPLAY_KEY = "display";
|
|
507
|
+
var KV_COLLAPSED_SIDEBAR_KEY = "collapsed_sidebar";
|
|
508
|
+
var KV_COLLAPSED_STATUSLINE_KEY = "collapsed_statusline";
|
|
509
|
+
var KV_COLLAPSED_LEGACY_KEY = "collapsed";
|
|
510
|
+
function reactiveChild(accessor) {
|
|
511
|
+
return accessor;
|
|
512
|
+
}
|
|
513
|
+
function meterColor(theme, severity) {
|
|
514
|
+
if (severity === "error") return theme.current.error;
|
|
515
|
+
if (severity === "warning") return theme.current.warning;
|
|
516
|
+
return theme.current.textMuted;
|
|
517
|
+
}
|
|
518
|
+
async function logUsageError(api, message) {
|
|
519
|
+
try {
|
|
520
|
+
await api.client.app.log({ service: LOG_SERVICE, level: "error", message });
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async function fetchJsonWithTimeout(url, apiKey) {
|
|
525
|
+
const controller = new AbortController();
|
|
526
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
527
|
+
try {
|
|
528
|
+
const response = await fetch(url, {
|
|
529
|
+
signal: controller.signal,
|
|
530
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
|
|
531
|
+
});
|
|
532
|
+
if (response.status === 401 || response.status === 403) return { __rejected: true };
|
|
533
|
+
if (!response.ok) return null;
|
|
534
|
+
try {
|
|
535
|
+
return await response.json();
|
|
536
|
+
} catch {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
return null;
|
|
541
|
+
} finally {
|
|
542
|
+
clearTimeout(timer);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function loadUsageSnapshot(host) {
|
|
546
|
+
if (hostEnv(host, "MOCK") === "1") return mockSnapshot();
|
|
547
|
+
const apiKey = toNonEmptyString(hostEnv(host, "API_KEY")) ?? readAuthJsonApiKey(host);
|
|
548
|
+
if (apiKey === null) return null;
|
|
549
|
+
const payload = await fetchJsonWithTimeout(API_USAGE_URL, apiKey);
|
|
550
|
+
if (payload === null) return null;
|
|
551
|
+
if (isRecord(payload) && payload.__rejected === true) {
|
|
552
|
+
return unavailableSnapshot("API key rejected (401/403)");
|
|
553
|
+
}
|
|
554
|
+
return extractSnapshotFromApiPayload(payload);
|
|
555
|
+
}
|
|
556
|
+
function createUsageStore(api, host) {
|
|
557
|
+
const [snapshot, setSnapshot] = createSignal(null);
|
|
558
|
+
let cachedAt = 0;
|
|
559
|
+
let lastFetchAt = 0;
|
|
560
|
+
let refreshInFlight = false;
|
|
561
|
+
async function refresh(ttlOverride) {
|
|
562
|
+
if (refreshInFlight) return;
|
|
563
|
+
const effectiveTtl = ttlOverride ?? POLL_INTERVAL_MS;
|
|
564
|
+
if (ttlOverride !== void 0) {
|
|
565
|
+
if (Date.now() - lastFetchAt < DEBOUNCE_MS) return;
|
|
566
|
+
if (Date.now() - cachedAt < effectiveTtl && snapshot() !== null) return;
|
|
567
|
+
} else if (Date.now() - cachedAt < POLL_INTERVAL_MS && snapshot() !== null) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
refreshInFlight = true;
|
|
571
|
+
try {
|
|
572
|
+
const next = await loadUsageSnapshot(host);
|
|
573
|
+
if (next === null) {
|
|
574
|
+
if (snapshot() === null) {
|
|
575
|
+
await logUsageError(api, "Go usage unavailable (not configured or request failed)");
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
cachedAt = Date.now();
|
|
580
|
+
setSnapshot(next);
|
|
581
|
+
} catch {
|
|
582
|
+
} finally {
|
|
583
|
+
lastFetchAt = Date.now();
|
|
584
|
+
refreshInFlight = false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
snapshot,
|
|
589
|
+
refresh: () => refresh(),
|
|
590
|
+
refreshSafely: (ttlOverride) => {
|
|
591
|
+
void refresh(ttlOverride).catch(() => {
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function resolveSurfaceSelection(options, api, host) {
|
|
597
|
+
if (options !== void 0) {
|
|
598
|
+
const sidebarOption = parseBooleanFlag(options.sidebar);
|
|
599
|
+
const statuslineOption = parseBooleanFlag(options.statusline);
|
|
600
|
+
if (sidebarOption !== null || statuslineOption !== null) {
|
|
601
|
+
return { sidebar: sidebarOption ?? true, statusline: statuslineOption ?? true };
|
|
602
|
+
}
|
|
603
|
+
if (isDisplayMode(options.display)) return surfaceSelectionFromDisplayMode(options.display);
|
|
604
|
+
}
|
|
605
|
+
const sidebarEnv = parseBooleanFlag(hostEnv(host, "SIDEBAR"));
|
|
606
|
+
const statuslineEnv = parseBooleanFlag(hostEnv(host, "STATUSLINE"));
|
|
607
|
+
if (sidebarEnv !== null || statuslineEnv !== null) {
|
|
608
|
+
return { sidebar: sidebarEnv ?? true, statusline: statuslineEnv ?? true };
|
|
609
|
+
}
|
|
610
|
+
const displayEnv = toNonEmptyString(hostEnv(host, "DISPLAY"));
|
|
611
|
+
if (displayEnv !== null && isDisplayMode(displayEnv)) {
|
|
612
|
+
return surfaceSelectionFromDisplayMode(displayEnv);
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const stored = api.kv.get(KV_DISPLAY_KEY, "both");
|
|
616
|
+
if (isDisplayMode(stored)) return surfaceSelectionFromDisplayMode(stored);
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
return { sidebar: true, statusline: true };
|
|
620
|
+
}
|
|
621
|
+
function readCollapsedFlag(api, key) {
|
|
622
|
+
try {
|
|
623
|
+
return api.kv.get(key, false) === true;
|
|
624
|
+
} catch {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function migrateLegacyCollapsedFlag(api) {
|
|
629
|
+
let legacyCollapsed = false;
|
|
630
|
+
try {
|
|
631
|
+
legacyCollapsed = api.kv.get(KV_COLLAPSED_LEGACY_KEY, false) === true;
|
|
632
|
+
} catch {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (!legacyCollapsed) return;
|
|
636
|
+
try {
|
|
637
|
+
api.kv.set(KV_COLLAPSED_SIDEBAR_KEY, true);
|
|
638
|
+
api.kv.set(KV_COLLAPSED_STATUSLINE_KEY, true);
|
|
639
|
+
api.kv.set(KV_COLLAPSED_LEGACY_KEY, false);
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function createCollapseState(api) {
|
|
644
|
+
migrateLegacyCollapsedFlag(api);
|
|
645
|
+
const [isSidebarCollapsed, setIsSidebarCollapsed] = createSignal(
|
|
646
|
+
readCollapsedFlag(api, KV_COLLAPSED_SIDEBAR_KEY)
|
|
647
|
+
);
|
|
648
|
+
const [isStatuslineCollapsed, setIsStatuslineCollapsed] = createSignal(
|
|
649
|
+
readCollapsedFlag(api, KV_COLLAPSED_STATUSLINE_KEY)
|
|
650
|
+
);
|
|
651
|
+
return {
|
|
652
|
+
isSidebarCollapsed,
|
|
653
|
+
isStatuslineCollapsed,
|
|
654
|
+
toggleSidebar() {
|
|
655
|
+
try {
|
|
656
|
+
const next = !isSidebarCollapsed();
|
|
657
|
+
setIsSidebarCollapsed(next);
|
|
658
|
+
api.kv.set(KV_COLLAPSED_SIDEBAR_KEY, next);
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
toggleStatusline() {
|
|
663
|
+
try {
|
|
664
|
+
const next = !isStatuslineCollapsed();
|
|
665
|
+
setIsStatuslineCollapsed(next);
|
|
666
|
+
api.kv.set(KV_COLLAPSED_STATUSLINE_KEY, next);
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function isGoUsageProvider(providerId) {
|
|
673
|
+
return providerId === GO_PROVIDER_ID;
|
|
674
|
+
}
|
|
675
|
+
function makeProviderResolver(state, fallback) {
|
|
676
|
+
return (sessionId) => resolveProviderId(state, sessionId, fallback());
|
|
677
|
+
}
|
|
678
|
+
function readModelDisplayName(source, providerID, modelID) {
|
|
679
|
+
try {
|
|
680
|
+
const provider = source.state?.provider?.find((entry) => entry.id === providerID);
|
|
681
|
+
return toNonEmptyString(provider?.models?.[modelID]?.name);
|
|
682
|
+
} catch {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
var MIN_METER_WIDTH = 6;
|
|
687
|
+
function MeterBar(props) {
|
|
688
|
+
const fill = createMemo(() => meterFillPercent(props.percent));
|
|
689
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "row", flexGrow: 1, flexShrink: 1, minWidth: MIN_METER_WIDTH, children: [
|
|
690
|
+
/* @__PURE__ */ jsx("box", { width: `${fill()}%`, backgroundColor: meterColor(props.theme, props.severity) }),
|
|
691
|
+
/* @__PURE__ */ jsx("box", { flexGrow: 1, backgroundColor: props.theme.current.backgroundElement })
|
|
692
|
+
] });
|
|
693
|
+
}
|
|
694
|
+
function GoPlanRow(props) {
|
|
695
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
696
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
|
|
697
|
+
/* @__PURE__ */ jsx("box", { width: PLAN_LABEL_WIDTH, marginRight: 1, flexShrink: 0, flexDirection: "row", children: /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: props.row.label }) }),
|
|
698
|
+
/* @__PURE__ */ jsx(MeterBar, { theme: props.theme, percent: props.row.percent, severity: props.row.severity }),
|
|
699
|
+
/* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", flexShrink: 0, children: formatPercentCell(props.row.percent) })
|
|
700
|
+
] }),
|
|
701
|
+
/* @__PURE__ */ jsx(Show, { when: props.row.reset, children: (reset) => /* @__PURE__ */ jsxs("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: [
|
|
702
|
+
"resets in ",
|
|
703
|
+
reset()
|
|
704
|
+
] }) })
|
|
705
|
+
] });
|
|
706
|
+
}
|
|
707
|
+
function SectionHeader(props) {
|
|
708
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "row", gap: 1, flexShrink: 0, onMouseDown: props.onToggle, children: [
|
|
709
|
+
/* @__PURE__ */ jsx("text", { fg: props.theme.current.text, wrapMode: "none", flexShrink: 0, children: reactiveChild(() => props.expanded() ? SIDEBAR_EXPANDED_GLYPH : SIDEBAR_COLLAPSED_GLYPH) }),
|
|
710
|
+
/* @__PURE__ */ jsx("text", { fg: props.theme.current.text, wrapMode: "none", children: /* @__PURE__ */ jsx("b", { children: reactiveChild(() => {
|
|
711
|
+
const count = props.count();
|
|
712
|
+
return count === null ? props.label : `${props.label} (${count})`;
|
|
713
|
+
}) }) })
|
|
714
|
+
] });
|
|
715
|
+
}
|
|
716
|
+
function CollapsibleSection(props) {
|
|
717
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
718
|
+
/* @__PURE__ */ jsx(
|
|
719
|
+
SectionHeader,
|
|
720
|
+
{
|
|
721
|
+
theme: props.theme,
|
|
722
|
+
label: props.label,
|
|
723
|
+
count: props.count,
|
|
724
|
+
expanded: props.expanded,
|
|
725
|
+
onToggle: props.onToggle
|
|
726
|
+
}
|
|
727
|
+
),
|
|
728
|
+
/* @__PURE__ */ jsx("box", { flexDirection: "column", gap: 1, children: reactiveChild(() => props.expanded() ? props.body() : null) })
|
|
729
|
+
] });
|
|
730
|
+
}
|
|
731
|
+
function GoUsageBlock(props) {
|
|
732
|
+
createEffect(() => {
|
|
733
|
+
const snapshot = props.snapshot();
|
|
734
|
+
if (snapshot !== null && snapshot.source === "unavailable") {
|
|
735
|
+
void logUsageError(
|
|
736
|
+
props.api,
|
|
737
|
+
snapshot.apiError ? `Go usage unavailable (${snapshot.apiError})` : "Go usage snapshot unavailable"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
const body = createMemo(() => {
|
|
742
|
+
const snapshot = props.snapshot();
|
|
743
|
+
if (snapshot === null) {
|
|
744
|
+
return /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: "Go loading\u2026" });
|
|
745
|
+
}
|
|
746
|
+
if (snapshot.source === "unavailable") {
|
|
747
|
+
return /* @__PURE__ */ jsxs("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: [
|
|
748
|
+
"Go n/a (",
|
|
749
|
+
snapshot.apiError ?? "unavailable",
|
|
750
|
+
")"
|
|
751
|
+
] });
|
|
752
|
+
}
|
|
753
|
+
const planRows = buildPlanRows(snapshot).map((row) => ({ ...row, reset: null }));
|
|
754
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
755
|
+
/* @__PURE__ */ jsx(Show, { when: props.resetLine === void 0 ? null : props.resetLine(), children: (reset) => /* @__PURE__ */ jsxs("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: [
|
|
756
|
+
reset().label,
|
|
757
|
+
" resets in ",
|
|
758
|
+
reset().text
|
|
759
|
+
] }) }),
|
|
760
|
+
/* @__PURE__ */ jsx(For, { each: planRows, children: (row) => /* @__PURE__ */ jsx(GoPlanRow, { theme: props.theme, row }) })
|
|
761
|
+
] });
|
|
762
|
+
});
|
|
763
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
764
|
+
/* @__PURE__ */ jsx("text", { fg: props.theme.current.text, children: /* @__PURE__ */ jsx("b", { children: "Go Usage" }) }),
|
|
765
|
+
reactiveChild(body)
|
|
766
|
+
] });
|
|
767
|
+
}
|
|
768
|
+
function GoStatusline(props) {
|
|
769
|
+
return /* @__PURE__ */ jsx(Show, { when: props.snapshot(), fallback: null, children: (snapshot) => {
|
|
770
|
+
if (isSnapshotEmpty(snapshot())) return null;
|
|
771
|
+
return /* @__PURE__ */ jsx("text", { children: formatStatusline(snapshot()) });
|
|
772
|
+
} });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/tui.tsx
|
|
776
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "@opentui/solid/jsx-runtime";
|
|
777
|
+
var HOST = "opencode";
|
|
778
|
+
var SLOT_ORDER = 50;
|
|
779
|
+
async function initializeTui(api, options) {
|
|
780
|
+
const surfaces = resolveSurfaceSelection(options, api, HOST);
|
|
781
|
+
const usageStore = createUsageStore(api, HOST);
|
|
782
|
+
const collapse = createCollapseState(api);
|
|
783
|
+
const [activeProviderId, setActiveProviderId] = createSignal2(void 0);
|
|
784
|
+
try {
|
|
785
|
+
const modelString = api.state?.config?.model;
|
|
786
|
+
if (typeof modelString === "string" && modelString.length > 0) {
|
|
787
|
+
const providerPart = modelString.split("/")[0];
|
|
788
|
+
if (providerPart !== void 0 && providerPart.length > 0) setActiveProviderId(providerPart);
|
|
789
|
+
}
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
const resolveActiveProviderId = makeProviderResolver(api.state, activeProviderId);
|
|
793
|
+
const [stateToken, setStateToken] = createSignal2(0);
|
|
794
|
+
function readSessionMessages(sessionId) {
|
|
795
|
+
if (sessionId.length === 0) return [];
|
|
796
|
+
try {
|
|
797
|
+
return api.state.session.messages(sessionId);
|
|
798
|
+
} catch {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function WeightedModelRow(props) {
|
|
803
|
+
const rounded = createMemo2(() => Math.round(props.model.share));
|
|
804
|
+
const name = createMemo2(
|
|
805
|
+
() => modelDisplayName(
|
|
806
|
+
readModelDisplayName(api, props.model.providerID, props.model.modelID),
|
|
807
|
+
props.model.modelID,
|
|
808
|
+
OPENCODE_MODEL_NAME_MAX_CHARS
|
|
809
|
+
)
|
|
810
|
+
);
|
|
811
|
+
return /* @__PURE__ */ jsxs2("box", { flexDirection: "row", gap: 1, flexShrink: 0, children: [
|
|
812
|
+
/* @__PURE__ */ jsx2("text", { fg: props.theme.current.text, wrapMode: "none", flexShrink: 0, children: name() }),
|
|
813
|
+
/* @__PURE__ */ jsx2(
|
|
814
|
+
MeterBar,
|
|
815
|
+
{
|
|
816
|
+
theme: props.theme,
|
|
817
|
+
percent: props.model.share,
|
|
818
|
+
severity: meterSeverityForPercent(rounded())
|
|
819
|
+
}
|
|
820
|
+
),
|
|
821
|
+
/* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", flexShrink: 0, children: formatPercentCell(rounded()) })
|
|
822
|
+
] });
|
|
823
|
+
}
|
|
824
|
+
function TopGoModelsSection(props) {
|
|
825
|
+
const [isExpanded, setIsExpanded] = createSignal2(false);
|
|
826
|
+
const weights = createMemo2(() => weightGoModels(props.usage()?.models ?? []));
|
|
827
|
+
const rows = () => weights().models;
|
|
828
|
+
const mix = createMemo2(
|
|
829
|
+
() => buildModelMixSummary(
|
|
830
|
+
rows().map((model) => ({
|
|
831
|
+
name: shortModelName(
|
|
832
|
+
modelDisplayName(
|
|
833
|
+
readModelDisplayName(api, model.providerID, model.modelID),
|
|
834
|
+
model.modelID,
|
|
835
|
+
OPENCODE_MODEL_NAME_MAX_CHARS
|
|
836
|
+
)
|
|
837
|
+
),
|
|
838
|
+
share: model.share
|
|
839
|
+
}))
|
|
840
|
+
)
|
|
841
|
+
);
|
|
842
|
+
const body = createMemo2(() => /* @__PURE__ */ jsxs2("box", { flexDirection: "column", gap: 1, paddingTop: 1, paddingLeft: 2, children: [
|
|
843
|
+
/* @__PURE__ */ jsx2(For2, { each: rows(), children: (model) => /* @__PURE__ */ jsx2(WeightedModelRow, { theme: props.theme, model }) }),
|
|
844
|
+
/* @__PURE__ */ jsx2(For2, { each: buildGoModelFooters(weights()), children: (line) => /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: line }) })
|
|
845
|
+
] }));
|
|
846
|
+
const section = createMemo2(() => {
|
|
847
|
+
if (weights().goModels === 0) {
|
|
848
|
+
return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", children: [
|
|
849
|
+
/* @__PURE__ */ jsx2("text", { fg: props.theme.current.text, wrapMode: "none", children: /* @__PURE__ */ jsx2("b", { children: TOP_GO_MODELS_LABEL }) }),
|
|
850
|
+
/* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: MODELS_EMPTY_LABEL })
|
|
851
|
+
] });
|
|
852
|
+
}
|
|
853
|
+
return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", children: [
|
|
854
|
+
/* @__PURE__ */ jsx2(
|
|
855
|
+
CollapsibleSection,
|
|
856
|
+
{
|
|
857
|
+
theme: props.theme,
|
|
858
|
+
label: TOP_GO_MODELS_LABEL,
|
|
859
|
+
count: () => weights().goModels,
|
|
860
|
+
expanded: isExpanded,
|
|
861
|
+
onToggle: () => setIsExpanded((current) => !current),
|
|
862
|
+
body
|
|
863
|
+
}
|
|
864
|
+
),
|
|
865
|
+
/* @__PURE__ */ jsx2(Show2, { when: !isExpanded(), children: /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", paddingLeft: 2, children: reactiveChild(mix) }) })
|
|
866
|
+
] });
|
|
867
|
+
});
|
|
868
|
+
return /* @__PURE__ */ jsx2("box", { flexDirection: "column", children: reactiveChild(section) });
|
|
869
|
+
}
|
|
870
|
+
function GoSidebarPanel(props) {
|
|
871
|
+
const planResetLine = createMemo2(() => {
|
|
872
|
+
const snapshot = usageStore.snapshot();
|
|
873
|
+
if (snapshot === null || snapshot.source === "unavailable") return null;
|
|
874
|
+
return relevantReset(snapshot);
|
|
875
|
+
});
|
|
876
|
+
const sessionUsage = createMemo2(() => {
|
|
877
|
+
stateToken();
|
|
878
|
+
return aggregateModelUsageFromMessages(readSessionMessages(props.sessionId));
|
|
879
|
+
});
|
|
880
|
+
return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", gap: 1, children: [
|
|
881
|
+
/* @__PURE__ */ jsx2(
|
|
882
|
+
GoUsageBlock,
|
|
883
|
+
{
|
|
884
|
+
api,
|
|
885
|
+
theme: props.theme,
|
|
886
|
+
snapshot: usageStore.snapshot,
|
|
887
|
+
resetLine: planResetLine
|
|
888
|
+
}
|
|
889
|
+
),
|
|
890
|
+
/* @__PURE__ */ jsx2(TopGoModelsSection, { theme: props.theme, usage: sessionUsage })
|
|
891
|
+
] });
|
|
892
|
+
}
|
|
893
|
+
if (surfaces.sidebar) {
|
|
894
|
+
try {
|
|
895
|
+
api.slots.register({
|
|
896
|
+
order: SLOT_ORDER,
|
|
897
|
+
slots: {
|
|
898
|
+
sidebar_content(ctx, props) {
|
|
899
|
+
try {
|
|
900
|
+
if (props.session_id.length === 0) return null;
|
|
901
|
+
if (!isGoUsageProvider(resolveActiveProviderId(props.session_id))) return null;
|
|
902
|
+
if (api.route.current.name !== "session") return null;
|
|
903
|
+
if (collapse.isSidebarCollapsed()) return null;
|
|
904
|
+
return /* @__PURE__ */ jsx2(GoSidebarPanel, { theme: ctx.theme, sessionId: props.session_id });
|
|
905
|
+
} catch {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (surfaces.statusline) {
|
|
915
|
+
try {
|
|
916
|
+
api.slots.register({
|
|
917
|
+
order: SLOT_ORDER,
|
|
918
|
+
slots: {
|
|
919
|
+
session_prompt_right(_ctx, props) {
|
|
920
|
+
try {
|
|
921
|
+
if (props.session_id.length === 0) return null;
|
|
922
|
+
if (!isGoUsageProvider(resolveActiveProviderId(props.session_id))) return null;
|
|
923
|
+
if (collapse.isStatuslineCollapsed()) return null;
|
|
924
|
+
return /* @__PURE__ */ jsx2(GoStatusline, { snapshot: usageStore.snapshot });
|
|
925
|
+
} catch {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
let unregisterToggleCommand = () => {
|
|
935
|
+
};
|
|
936
|
+
try {
|
|
937
|
+
const unregister = api.command?.register(() => [
|
|
938
|
+
{
|
|
939
|
+
title: "Go usage: toggle sidebar",
|
|
940
|
+
value: "oc-go-usage-display.toggle-sidebar",
|
|
941
|
+
category: "Go",
|
|
942
|
+
onSelect: () => collapse.toggleSidebar()
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
title: "Go usage: toggle statusline",
|
|
946
|
+
value: "oc-go-usage-display.toggle-statusline",
|
|
947
|
+
category: "Go",
|
|
948
|
+
onSelect: () => collapse.toggleStatusline()
|
|
949
|
+
}
|
|
950
|
+
]);
|
|
951
|
+
if (typeof unregister === "function") unregisterToggleCommand = unregister;
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
let unsubscribeSession = () => {
|
|
955
|
+
};
|
|
956
|
+
let unsubscribeMessage = () => {
|
|
957
|
+
};
|
|
958
|
+
try {
|
|
959
|
+
const unsubscribe = api.event.on("session.updated", (event) => {
|
|
960
|
+
const providerId = event?.properties?.info?.model?.providerID;
|
|
961
|
+
if (providerId !== void 0) setActiveProviderId(providerId);
|
|
962
|
+
usageStore.refreshSafely(0);
|
|
963
|
+
});
|
|
964
|
+
if (typeof unsubscribe === "function") unsubscribeSession = unsubscribe;
|
|
965
|
+
} catch {
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const unsubscribe = api.event.on("message.updated", () => {
|
|
969
|
+
usageStore.refreshSafely(EVENT_TTL_MS);
|
|
970
|
+
setStateToken((current) => current + 1);
|
|
971
|
+
});
|
|
972
|
+
if (typeof unsubscribe === "function") unsubscribeMessage = unsubscribe;
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
let pollTimer = null;
|
|
976
|
+
try {
|
|
977
|
+
pollTimer = setInterval(() => usageStore.refreshSafely(), POLL_INTERVAL_MS);
|
|
978
|
+
} catch {
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
api.lifecycle.onDispose(() => {
|
|
982
|
+
if (pollTimer !== null) {
|
|
983
|
+
try {
|
|
984
|
+
clearInterval(pollTimer);
|
|
985
|
+
} catch {
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
for (const teardown of [unsubscribeSession, unsubscribeMessage, unregisterToggleCommand]) {
|
|
989
|
+
try {
|
|
990
|
+
teardown();
|
|
991
|
+
} catch {
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
} catch {
|
|
996
|
+
if (pollTimer !== null) {
|
|
997
|
+
try {
|
|
998
|
+
clearInterval(pollTimer);
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
await usageStore.refresh();
|
|
1004
|
+
}
|
|
1005
|
+
var goUsageTui = async (api, options) => {
|
|
1006
|
+
try {
|
|
1007
|
+
await initializeTui(api, options);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
void logUsageError(
|
|
1010
|
+
api,
|
|
1011
|
+
`Go usage TUI failed to initialize: ${errorMessage(error)}; continuing without display`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
var tui_default = { id: "oc-go-usage-display", tui: goUsageTui };
|
|
1016
|
+
export {
|
|
1017
|
+
tui_default as default
|
|
1018
|
+
};
|