oc-go-usage-display 1.1.0 → 2.1.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.
Files changed (42) hide show
  1. package/README.md +246 -93
  2. package/bin/lib.js +395 -37
  3. package/bin/oc-go-usage-display-init.js +42 -13
  4. package/bin/oc-go-usage-display-remove.js +22 -17
  5. package/bin/oc-go-usage-display-show.js +47 -39
  6. package/bin/oc-go-usage-display-status.js +31 -52
  7. package/bin/oc-go-usage-display-update.js +27 -14
  8. package/dist/helpers.d.ts +117 -0
  9. package/dist/helpers.d.ts.map +1 -0
  10. package/dist/helpers.js +582 -0
  11. package/dist/helpers.js.map +1 -0
  12. package/dist/index.d.ts +5 -13
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +249 -159
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugins/oc-go-usage-display.kilo.ts +699 -0
  17. package/dist/plugins/oc-go-usage-display.kilo.tsx +1364 -0
  18. package/dist/plugins/oc-go-usage-display.ts +699 -0
  19. package/dist/plugins/oc-go-usage-display.tsx +1108 -0
  20. package/dist/shared.d.ts +87 -8
  21. package/dist/shared.d.ts.map +1 -1
  22. package/dist/shared.js +596 -31
  23. package/dist/shared.js.map +1 -1
  24. package/dist/tui-shared.d.ts +109 -0
  25. package/dist/tui-shared.d.ts.map +1 -0
  26. package/dist/tui-shared.js +408 -0
  27. package/dist/tui-shared.js.map +1 -0
  28. package/dist/tui.d.ts +0 -17
  29. package/dist/tui.d.ts.map +1 -1
  30. package/dist/tui.js +229 -282
  31. package/dist/tui.js.map +1 -1
  32. package/dist/tui.kilo.d.ts +8 -0
  33. package/dist/tui.kilo.d.ts.map +1 -0
  34. package/dist/tui.kilo.js +502 -0
  35. package/dist/tui.kilo.js.map +1 -0
  36. package/package.json +49 -15
  37. package/src/helpers.ts +749 -0
  38. package/src/index.ts +316 -175
  39. package/src/shared.ts +693 -32
  40. package/src/tui-shared.tsx +755 -0
  41. package/src/tui.kilo.tsx +817 -0
  42. package/src/tui.tsx +352 -366
@@ -0,0 +1,1364 @@
1
+ // oc-go-usage-display: kilo tui plugin bundle (self-contained)
2
+
3
+ // src/tui.kilo.tsx
4
+ import { For as For2, Show as Show2, createEffect as createEffect2, createMemo as createMemo2, createSignal as createSignal2, onCleanup } 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 KILO_SLOT_ORDER = 125;
91
+ var KILO_INTEGRATED_SLOT_ORDER = 150;
92
+ var KILO_USAGE_PANEL_PLUGIN_ID = "internal:kilo-sidebar-usage";
93
+ var KILO_TOKEN_USAGE_ROWS = [
94
+ "Input",
95
+ "Output",
96
+ "Reasoning",
97
+ "Cache read",
98
+ "Cache write",
99
+ "Cache rate",
100
+ "Cost"
101
+ ];
102
+ var GO_PROVIDER_ID = "opencode-go";
103
+ var KILO_MODEL_NAME_MAX_CHARS = 19;
104
+ var KILO_STEPS_COLUMN_WIDTH = 5;
105
+ var KILO_COST_COLUMN_WIDTH = 9;
106
+ var SIDEBAR_COLLAPSED_GLYPH = "\u25B6";
107
+ var SIDEBAR_EXPANDED_GLYPH = "\u25BE";
108
+ var CACHE_RATE_DECIMALS = 1;
109
+ var CACHE_RATE_EMPTY = "-";
110
+ var PERCENT_CELL_WIDTH = 5;
111
+ var INTEGRATED_TOKENS_SECTION_LABEL = "Session Tokens";
112
+ var INTEGRATED_MODELS_SECTION_LABEL = "Models";
113
+ var GO_PLAN_HEADING = "Go Plan";
114
+ var INTEGRATED_GO_SHARE_LABEL = "Go share";
115
+ var MODELS_EMPTY_LABEL = "No model usage yet";
116
+ var INTEGRATED_LOADING_LABEL = "Loading usage...";
117
+ var INTEGRATED_UNAVAILABLE_LABEL = "Usage unavailable";
118
+ var SECONDS_PER_MINUTE = 60;
119
+ var SECONDS_PER_HOUR = 3600;
120
+ var SECONDS_PER_DAY = 86400;
121
+ var SECONDS_PER_WEEK = 604800;
122
+ function resetDayUnits(sec) {
123
+ const units = [
124
+ { value: Math.floor(sec / SECONDS_PER_WEEK), suffix: "w" },
125
+ { value: Math.floor(sec % SECONDS_PER_WEEK / SECONDS_PER_DAY), suffix: "d" },
126
+ { value: Math.floor(sec % SECONDS_PER_DAY / SECONDS_PER_HOUR), suffix: "h" }
127
+ ];
128
+ return units.filter((unit) => unit.value > 0).map((unit) => `${unit.value}${unit.suffix}`);
129
+ }
130
+ function formatResetDuration(totalSec) {
131
+ if (totalSec === null || !Number.isFinite(totalSec) || totalSec < 0) return null;
132
+ const sec = Math.floor(totalSec);
133
+ if (sec < SECONDS_PER_DAY) {
134
+ const hours = Math.floor(sec / SECONDS_PER_HOUR);
135
+ const minutes = Math.floor(sec % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
136
+ if (hours === 0) return minutes > 0 ? `${minutes}m` : `${sec}s`;
137
+ return minutes > 0 ? `${hours}h${minutes}m` : `${hours}h`;
138
+ }
139
+ return resetDayUnits(sec).join(" ");
140
+ }
141
+ function readAuthJsonApiKey(host = "opencode", env = process.env, homedir2 = resolveHomedir) {
142
+ for (const authPath of authJsonPaths(host, env, homedir2)) {
143
+ let raw;
144
+ try {
145
+ raw = fs.readFileSync(authPath, "utf8");
146
+ } catch {
147
+ continue;
148
+ }
149
+ let parsed;
150
+ try {
151
+ parsed = JSON.parse(raw);
152
+ } catch {
153
+ continue;
154
+ }
155
+ if (!isRecord(parsed)) continue;
156
+ const goEntry = parsed["opencode-go"];
157
+ const fallbackEntry = parsed["opencode"];
158
+ const goKey = isRecord(goEntry) ? toNonEmptyString(goEntry.key) : null;
159
+ if (goKey) return goKey;
160
+ const fallbackKey = isRecord(fallbackEntry) ? toNonEmptyString(fallbackEntry.key) : null;
161
+ if (fallbackKey) return fallbackKey;
162
+ }
163
+ return null;
164
+ }
165
+ function usableCountdown(seconds) {
166
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) return null;
167
+ return seconds;
168
+ }
169
+ function secondsUntilReset(resetsAt, now) {
170
+ const instant = toNonEmptyString(resetsAt);
171
+ if (instant === null) return null;
172
+ return usableCountdown(Math.round((Date.parse(instant) - now) / 1e3));
173
+ }
174
+ var LIMITED_STATUSES = /* @__PURE__ */ new Set(["rate-limited", "limited", "exhausted", "capped"]);
175
+ function isLimitedStatus(status) {
176
+ if (status === null) return false;
177
+ const normalized = status.trim().toLowerCase().replace(/[\s_]+/g, "-");
178
+ return LIMITED_STATUSES.has(normalized);
179
+ }
180
+ function extractWindow(candidate, now = Date.now()) {
181
+ if (!isRecord(candidate)) return null;
182
+ const percent = toFiniteNumber(
183
+ candidate.percent ?? candidate.usagePercent ?? candidate.usedPercent ?? candidate.value ?? candidate.usage
184
+ );
185
+ if (percent === null) return null;
186
+ const status = toNonEmptyString(candidate.status);
187
+ const resetInSec = usableCountdown(candidate.resetInSec ?? candidate.resetInSeconds) ?? secondsUntilReset(candidate.resetsAt, now);
188
+ const resetText = toNonEmptyString(candidate.resetText ?? candidate.reset ?? null);
189
+ return { percent: Math.round(percent), status, limited: isLimitedStatus(status), resetInSec, resetText };
190
+ }
191
+ function extractSnapshotFromApiPayload(payload, now = Date.now()) {
192
+ if (!isRecord(payload)) return null;
193
+ const containers = [payload];
194
+ for (const key of ["usage", "data", "go"]) {
195
+ if (isRecord(payload[key])) containers.push(payload[key]);
196
+ }
197
+ for (const container of containers) {
198
+ if (!isRecord(container)) continue;
199
+ const rolling = extractWindow(container.rolling ?? container.rollingUsage ?? container["5h"], now);
200
+ const weekly = extractWindow(container.weekly ?? container.weeklyUsage ?? container["7d"], now);
201
+ const monthly = extractWindow(container.monthly ?? container.monthlyUsage ?? container["30d"], now);
202
+ if (rolling !== null || weekly !== null || monthly !== null) {
203
+ return {
204
+ rolling,
205
+ weekly,
206
+ monthly,
207
+ source: "api",
208
+ fetchedAt: now
209
+ };
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+ var MOCK_PERCENTS = [42, 15, 61];
215
+ function parseMockPercents(raw) {
216
+ const text = toNonEmptyString(raw);
217
+ if (text === null) return null;
218
+ const parts = text.split(",");
219
+ if (parts.length !== MOCK_PERCENTS.length) return null;
220
+ const values = parts.map((part) => Number(part.trim()));
221
+ const rolling = values[0];
222
+ const weekly = values[1];
223
+ const monthly = values[2];
224
+ if (rolling === void 0 || weekly === void 0 || monthly === void 0) return null;
225
+ if (!Number.isFinite(rolling) || !Number.isFinite(weekly) || !Number.isFinite(monthly)) return null;
226
+ return [rolling, weekly, monthly];
227
+ }
228
+ var MOCK_RESETS = [7543, null, null];
229
+ var MOCK_LIMITED = [false, false, false];
230
+ function parseMockShare(raw) {
231
+ const text = toNonEmptyString(raw);
232
+ if (text === null) return null;
233
+ const value = Number(text.trim());
234
+ return Number.isFinite(value) ? value : null;
235
+ }
236
+ function parseMockResets(raw) {
237
+ const text = toNonEmptyString(raw);
238
+ if (text === null) return null;
239
+ const parts = text.split(",");
240
+ if (parts.length !== MOCK_PERCENTS.length) return null;
241
+ const parsed = parts.map((part) => {
242
+ const slot = part.trim();
243
+ if (slot === "" || slot === "-") return null;
244
+ const value = Number(slot);
245
+ return Number.isFinite(value) && value >= 0 ? value : void 0;
246
+ });
247
+ const [rolling, weekly, monthly] = parsed;
248
+ if (rolling === void 0 || weekly === void 0 || monthly === void 0) return null;
249
+ return [rolling, weekly, monthly];
250
+ }
251
+ function parseMockLimited(raw) {
252
+ const text = toNonEmptyString(raw);
253
+ if (text === null) return null;
254
+ const parts = text.split(",");
255
+ if (parts.length !== MOCK_PERCENTS.length) return null;
256
+ const parsed = parts.map((part) => {
257
+ const slot = part.trim();
258
+ if (slot === "" || slot === "-") return false;
259
+ return MOCK_LIMITED_TRUE.has(slot.toLowerCase());
260
+ });
261
+ const [rolling, weekly, monthly] = parsed;
262
+ if (rolling === void 0 || weekly === void 0 || monthly === void 0) return null;
263
+ return [rolling, weekly, monthly];
264
+ }
265
+ var MOCK_LIMITED_TRUE = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
266
+ function mockGoShare(host, env = process.env) {
267
+ if (hostEnv(host, "MOCK", env) !== "1") return null;
268
+ return parseMockShare(hostEnv(host, "MOCK_SHARE", env));
269
+ }
270
+ function mockSnapshot(overrides = {}) {
271
+ const percents = overrides.percents ?? MOCK_PERCENTS;
272
+ const resets = overrides.resets ?? MOCK_RESETS;
273
+ const limited = overrides.limited ?? MOCK_LIMITED;
274
+ const window = (index, percent) => {
275
+ const capped = limited[index] === true;
276
+ return {
277
+ percent,
278
+ // A capped window says so, because that is what the real payload says, and
279
+ // because `limited` is what gates the per-row countdown: a window with a
280
+ // countdown and no cap is a shape the API does not produce.
281
+ status: capped ? "rate-limited" : "active",
282
+ limited: capped,
283
+ resetInSec: resets[index] ?? null,
284
+ resetText: null
285
+ };
286
+ };
287
+ return {
288
+ rolling: window(0, percents[0]),
289
+ weekly: window(1, percents[1]),
290
+ monthly: window(2, percents[2]),
291
+ source: "mock",
292
+ fetchedAt: Date.now()
293
+ };
294
+ }
295
+ function unavailableSnapshot(error) {
296
+ return {
297
+ rolling: null,
298
+ weekly: null,
299
+ monthly: null,
300
+ source: "unavailable",
301
+ fetchedAt: Date.now(),
302
+ apiUnavailable: true,
303
+ apiError: error
304
+ };
305
+ }
306
+ var EMPTY_TOKENS = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
307
+ function parseUsageTokens(candidate) {
308
+ if (!isRecord(candidate)) return { ...EMPTY_TOKENS, cache: { ...EMPTY_TOKENS.cache } };
309
+ const cache = isRecord(candidate.cache) ? candidate.cache : {};
310
+ return {
311
+ input: toUsageCount(candidate.input),
312
+ output: toUsageCount(candidate.output),
313
+ reasoning: toUsageCount(candidate.reasoning),
314
+ cache: { read: toUsageCount(cache.read), write: toUsageCount(cache.write) }
315
+ };
316
+ }
317
+ function parseUsageTotals(candidate) {
318
+ if (!isRecord(candidate)) return { steps: 0, cost: 0, tokens: { ...EMPTY_TOKENS, cache: { ...EMPTY_TOKENS.cache } } };
319
+ return {
320
+ steps: toUsageCount(candidate.steps),
321
+ cost: toUsageCount(candidate.cost),
322
+ tokens: parseUsageTokens(candidate.tokens)
323
+ };
324
+ }
325
+ function parseSessionModelUsage(payload) {
326
+ if (!isRecord(payload)) return null;
327
+ const models = [];
328
+ if (Array.isArray(payload.models)) {
329
+ for (const entry of payload.models) {
330
+ if (!isRecord(entry)) continue;
331
+ const providerID = toNonEmptyString(entry.providerID);
332
+ const modelID = toNonEmptyString(entry.modelID);
333
+ if (providerID === null || modelID === null) continue;
334
+ models.push({ providerID, modelID, ...parseUsageTotals(entry) });
335
+ }
336
+ }
337
+ return {
338
+ sessionIDs: Array.isArray(payload.sessionIDs) ? payload.sessionIDs.filter((id) => typeof id === "string") : [],
339
+ totals: parseUsageTotals(payload.totals),
340
+ models
341
+ };
342
+ }
343
+
344
+ // src/helpers.ts
345
+ function isDisplayMode(value) {
346
+ return value === "sidebar" || value === "statusline" || value === "both";
347
+ }
348
+ var DEFAULT_SIDEBAR_MODE = "integrated";
349
+ function parseSidebarMode(value) {
350
+ if (typeof value !== "string") return null;
351
+ const normalized = value.trim().toLowerCase();
352
+ return normalized === "integrated" || normalized === "standalone" ? normalized : null;
353
+ }
354
+ function providerIdFromModel(model) {
355
+ if (typeof model !== "string" || model.length === 0) return void 0;
356
+ const provider = model.split("/")[0];
357
+ return provider !== void 0 && provider.length > 0 ? provider : void 0;
358
+ }
359
+ function resolveProviderId(state, sessionId, fallback) {
360
+ try {
361
+ const fromSession = state?.session?.get?.(sessionId)?.model?.providerID;
362
+ if (typeof fromSession === "string" && fromSession.length > 0) return fromSession;
363
+ } catch {
364
+ }
365
+ try {
366
+ const fromConfig = providerIdFromModel(state?.config?.model);
367
+ if (fromConfig !== void 0) return fromConfig;
368
+ } catch {
369
+ }
370
+ return fallback;
371
+ }
372
+ function surfaceSelectionFromDisplayMode(mode) {
373
+ return { sidebar: mode !== "statusline", statusline: mode !== "sidebar" };
374
+ }
375
+ function parseBooleanFlag(value) {
376
+ if (typeof value === "boolean") return value;
377
+ if (typeof value === "number") {
378
+ if (value === 1) return true;
379
+ if (value === 0) return false;
380
+ return null;
381
+ }
382
+ if (typeof value !== "string") return null;
383
+ const normalized = value.trim().toLowerCase();
384
+ if (normalized === "1" || normalized === "true") return true;
385
+ if (normalized === "0" || normalized === "false") return false;
386
+ return null;
387
+ }
388
+ function isSnapshotEmpty(snapshot) {
389
+ return snapshot.rolling === null && snapshot.weekly === null && snapshot.monthly === null;
390
+ }
391
+ function relevantReset(snapshot) {
392
+ const rows = buildPlanRows(snapshot);
393
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
394
+ const row = rows[i];
395
+ if (row?.reset != null) return { label: row.label, text: row.reset };
396
+ }
397
+ return soonestReset(snapshot);
398
+ }
399
+ function soonestReset(snapshot) {
400
+ const windows = [
401
+ ["5h", snapshot.rolling],
402
+ ["7d", snapshot.weekly],
403
+ ["30d", snapshot.monthly]
404
+ ];
405
+ let soonest = null;
406
+ for (const [name, window] of windows) {
407
+ if (window === null) continue;
408
+ const seconds = window.resetInSec;
409
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) continue;
410
+ if (soonest !== null && seconds >= soonest.seconds) continue;
411
+ soonest = { label: name, seconds };
412
+ }
413
+ if (soonest === null) return null;
414
+ const text = formatResetDuration(soonest.seconds);
415
+ return text === null ? null : { label: soonest.label, text };
416
+ }
417
+ function formatStatusline(snapshot) {
418
+ const rolling = snapshot.rolling === null ? "5h n/a" : `5h ${snapshot.rolling.percent}%`;
419
+ const weekly = snapshot.weekly === null ? "7d n/a" : `7d ${snapshot.weekly.percent}%`;
420
+ const monthly = snapshot.monthly === null ? "30d n/a" : `30d ${snapshot.monthly.percent}%`;
421
+ const windows = `Go ${rolling} | ${weekly} | ${monthly}`;
422
+ const reset = relevantReset(snapshot);
423
+ return reset === null ? windows : `${windows} \xB7 resets in ${reset.text}`;
424
+ }
425
+ var WARNING_PERCENT = 75;
426
+ var ERROR_PERCENT = 90;
427
+ function usageMeterSeverity(window) {
428
+ if (window.limited) return "error";
429
+ return meterSeverityForPercent(window.percent);
430
+ }
431
+ function meterSeverityForPercent(percent) {
432
+ if (!Number.isFinite(percent)) return "muted";
433
+ if (percent >= ERROR_PERCENT) return "error";
434
+ if (percent >= WARNING_PERCENT) return "warning";
435
+ return "muted";
436
+ }
437
+ function meterFillPercent(percent) {
438
+ if (!Number.isFinite(percent)) return 0;
439
+ return Math.min(Math.max(percent, 0), 100);
440
+ }
441
+ function buildPlanRows(snapshot) {
442
+ const windows = [
443
+ ["5h", snapshot.rolling],
444
+ ["7d", snapshot.weekly],
445
+ ["30d", snapshot.monthly]
446
+ ];
447
+ const rows = [];
448
+ for (const [label, window] of windows) {
449
+ if (window === null) continue;
450
+ rows.push({
451
+ label,
452
+ percent: window.percent,
453
+ severity: usageMeterSeverity(window),
454
+ // A countdown only tells the user something once the window is capped;
455
+ // before that the schedule is noise, and the header rows already show it.
456
+ reset: window.limited ? formatResetDuration(window.resetInSec) ?? window.resetText : null
457
+ });
458
+ }
459
+ return rows;
460
+ }
461
+ var COUNT_FORMAT = new Intl.NumberFormat("en-US");
462
+ var CURRENCY_FORMAT = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
463
+ function formatUsageCount(value) {
464
+ return COUNT_FORMAT.format(toUsageCount(value));
465
+ }
466
+ function formatUsageCost(value) {
467
+ return CURRENCY_FORMAT.format(toUsageCount(value));
468
+ }
469
+ function usageTokenCount(tokens) {
470
+ return toUsageCount(tokens.input) + toUsageCount(tokens.output) + toUsageCount(tokens.reasoning) + toUsageCount(tokens.cache.read) + toUsageCount(tokens.cache.write);
471
+ }
472
+ function cacheRatePercent(tokens) {
473
+ const denominator = toUsageCount(tokens.input) + toUsageCount(tokens.cache.read) + toUsageCount(tokens.cache.write);
474
+ if (denominator === 0) return CACHE_RATE_EMPTY;
475
+ return `${(toUsageCount(tokens.cache.read) / denominator * 100).toFixed(CACHE_RATE_DECIMALS)}%`;
476
+ }
477
+ function goSharePercent(modelTokens, totalGoTokens2) {
478
+ const total = toUsageCount(totalGoTokens2);
479
+ if (total === 0) return 0;
480
+ return toUsageCount(modelTokens) / total * 100;
481
+ }
482
+ function totalGoTokens(models) {
483
+ let total = 0;
484
+ for (const model of models) {
485
+ if (model.providerID !== GO_PROVIDER_ID) continue;
486
+ total += usageTokenCount(model.tokens);
487
+ }
488
+ return total;
489
+ }
490
+ function groupModelsByProvider(models, providerNames = /* @__PURE__ */ new Map()) {
491
+ const groups = /* @__PURE__ */ new Map();
492
+ for (const model of models) {
493
+ const group = groups.get(model.providerID) ?? {
494
+ providerID: model.providerID,
495
+ providerName: providerNames.get(model.providerID) ?? model.providerID,
496
+ models: []
497
+ };
498
+ group.models.push(model);
499
+ groups.set(model.providerID, group);
500
+ }
501
+ return [...groups.values()];
502
+ }
503
+ function truncateModelName(value, max = KILO_MODEL_NAME_MAX_CHARS) {
504
+ if (value.length <= max) return value;
505
+ const budget = Math.max(max - 1, 0);
506
+ return `${value.slice(0, budget)}\u2026`;
507
+ }
508
+ function displayModelName(name) {
509
+ return name.trim().replace(/^[^:]+:\s+/, "").replace(/^[^/\s]+\/(?=[^/]+$)/, "").replace(/\s*\([^)]*%\s*off[^)]*\)\s*$/i, "").replace(/^([A-Za-z]{2,})(?=\d)/, "$1 ").replace(/\s+/g, " ");
510
+ }
511
+ function modelDisplayName(catalogName, modelID, max = KILO_MODEL_NAME_MAX_CHARS) {
512
+ const raw = toNonEmptyString(catalogName) ?? toNonEmptyString(modelID) ?? "";
513
+ return truncateModelName(displayModelName(raw), max);
514
+ }
515
+ var PLAN_LABEL_WIDTH = 3;
516
+ var GO_SHARE_LABEL_WIDTH = INTEGRATED_GO_SHARE_LABEL.length;
517
+ function formatPercentCell(percent, width = PERCENT_CELL_WIDTH) {
518
+ const cells = Math.max(Math.floor(width), 1);
519
+ const rounded = Number.isFinite(percent) ? Math.round(meterFillPercent(percent)) : 0;
520
+ return `${`${rounded}%`}`.padStart(cells, " ");
521
+ }
522
+ function buildTokenUsageRows(totals) {
523
+ const values = {
524
+ Input: formatUsageCount(totals.tokens.input),
525
+ Output: formatUsageCount(totals.tokens.output),
526
+ Reasoning: formatUsageCount(totals.tokens.reasoning),
527
+ "Cache read": formatUsageCount(totals.tokens.cache.read),
528
+ "Cache write": formatUsageCount(totals.tokens.cache.write),
529
+ "Cache rate": cacheRatePercent(totals.tokens),
530
+ Cost: formatUsageCost(totals.cost)
531
+ };
532
+ return KILO_TOKEN_USAGE_ROWS.flatMap((label) => {
533
+ const value = values[label];
534
+ return value === void 0 ? [] : [{ label, value }];
535
+ });
536
+ }
537
+ function buildModelTokenRows(tokens) {
538
+ return buildTokenUsageRows({ cost: 0, tokens }).filter((row) => row.label !== "Cost");
539
+ }
540
+
541
+ // src/tui-shared.tsx
542
+ import { For, Show, createEffect, createMemo, createSignal } from "solid-js";
543
+ import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
544
+ var API_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
545
+ var POLL_INTERVAL_MS = 6e4;
546
+ var EVENT_TTL_MS = 15e3;
547
+ var DEBOUNCE_MS = 5e3;
548
+ var FETCH_TIMEOUT_MS = 1e4;
549
+ var LOG_SERVICE = "oc-go-usage-display";
550
+ var KV_DISPLAY_KEY = "display";
551
+ var KV_COLLAPSED_SIDEBAR_KEY = "collapsed_sidebar";
552
+ var KV_COLLAPSED_STATUSLINE_KEY = "collapsed_statusline";
553
+ var KV_COLLAPSED_LEGACY_KEY = "collapsed";
554
+ function reactiveChild(accessor) {
555
+ return accessor;
556
+ }
557
+ function meterColor(theme, severity) {
558
+ if (severity === "error") return theme.current.error;
559
+ if (severity === "warning") return theme.current.warning;
560
+ return theme.current.textMuted;
561
+ }
562
+ async function logUsageError(api, message) {
563
+ try {
564
+ await api.client.app.log({ service: LOG_SERVICE, level: "error", message });
565
+ } catch {
566
+ }
567
+ }
568
+ async function fetchJsonWithTimeout(url, apiKey) {
569
+ const controller = new AbortController();
570
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
571
+ try {
572
+ const response = await fetch(url, {
573
+ signal: controller.signal,
574
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
575
+ });
576
+ if (response.status === 401 || response.status === 403) return { __rejected: true };
577
+ if (!response.ok) return null;
578
+ try {
579
+ return await response.json();
580
+ } catch {
581
+ return null;
582
+ }
583
+ } catch {
584
+ return null;
585
+ } finally {
586
+ clearTimeout(timer);
587
+ }
588
+ }
589
+ async function loadUsageSnapshot(host) {
590
+ if (hostEnv(host, "MOCK") === "1") {
591
+ const overrides = {};
592
+ const percents = parseMockPercents(hostEnv(host, "MOCK_PERCENTS"));
593
+ if (percents !== null) overrides.percents = percents;
594
+ const resets = parseMockResets(hostEnv(host, "MOCK_RESETS"));
595
+ if (resets !== null) overrides.resets = resets;
596
+ const limited = parseMockLimited(hostEnv(host, "MOCK_LIMITED"));
597
+ if (limited !== null) overrides.limited = limited;
598
+ return mockSnapshot(overrides);
599
+ }
600
+ const apiKey = toNonEmptyString(hostEnv(host, "API_KEY")) ?? readAuthJsonApiKey(host);
601
+ if (apiKey === null) return null;
602
+ const payload = await fetchJsonWithTimeout(API_USAGE_URL, apiKey);
603
+ if (payload === null) return null;
604
+ if (isRecord(payload) && payload.__rejected === true) {
605
+ return unavailableSnapshot("API key rejected (401/403)");
606
+ }
607
+ return extractSnapshotFromApiPayload(payload);
608
+ }
609
+ function createUsageStore(api, host) {
610
+ const [snapshot, setSnapshot] = createSignal(null);
611
+ let cachedAt = 0;
612
+ let lastFetchAt = 0;
613
+ let refreshInFlight = false;
614
+ async function refresh(ttlOverride) {
615
+ if (refreshInFlight) return;
616
+ const effectiveTtl = ttlOverride ?? POLL_INTERVAL_MS;
617
+ if (ttlOverride !== void 0) {
618
+ if (Date.now() - lastFetchAt < DEBOUNCE_MS) return;
619
+ if (Date.now() - cachedAt < effectiveTtl && snapshot() !== null) return;
620
+ } else if (Date.now() - cachedAt < POLL_INTERVAL_MS && snapshot() !== null) {
621
+ return;
622
+ }
623
+ refreshInFlight = true;
624
+ try {
625
+ const next = await loadUsageSnapshot(host);
626
+ if (next === null) {
627
+ if (snapshot() === null) {
628
+ await logUsageError(api, "Go usage unavailable (not configured or request failed)");
629
+ }
630
+ return;
631
+ }
632
+ cachedAt = Date.now();
633
+ setSnapshot(next);
634
+ } catch {
635
+ } finally {
636
+ lastFetchAt = Date.now();
637
+ refreshInFlight = false;
638
+ }
639
+ }
640
+ return {
641
+ snapshot,
642
+ refresh: () => refresh(),
643
+ refreshSafely: (ttlOverride) => {
644
+ void refresh(ttlOverride).catch(() => {
645
+ });
646
+ }
647
+ };
648
+ }
649
+ function resolveSurfaceSelection(options, api, host) {
650
+ if (options !== void 0) {
651
+ const sidebarOption = parseBooleanFlag(options.sidebar);
652
+ const statuslineOption = parseBooleanFlag(options.statusline);
653
+ if (sidebarOption !== null || statuslineOption !== null) {
654
+ return { sidebar: sidebarOption ?? true, statusline: statuslineOption ?? true };
655
+ }
656
+ if (isDisplayMode(options.display)) return surfaceSelectionFromDisplayMode(options.display);
657
+ }
658
+ const sidebarEnv = parseBooleanFlag(hostEnv(host, "SIDEBAR"));
659
+ const statuslineEnv = parseBooleanFlag(hostEnv(host, "STATUSLINE"));
660
+ if (sidebarEnv !== null || statuslineEnv !== null) {
661
+ return { sidebar: sidebarEnv ?? true, statusline: statuslineEnv ?? true };
662
+ }
663
+ const displayEnv = toNonEmptyString(hostEnv(host, "DISPLAY"));
664
+ if (displayEnv !== null && isDisplayMode(displayEnv)) {
665
+ return surfaceSelectionFromDisplayMode(displayEnv);
666
+ }
667
+ try {
668
+ const stored = api.kv.get(KV_DISPLAY_KEY, "both");
669
+ if (isDisplayMode(stored)) return surfaceSelectionFromDisplayMode(stored);
670
+ } catch {
671
+ }
672
+ return { sidebar: true, statusline: true };
673
+ }
674
+ function readCollapsedFlag(api, key) {
675
+ try {
676
+ return api.kv.get(key, false) === true;
677
+ } catch {
678
+ return false;
679
+ }
680
+ }
681
+ function migrateLegacyCollapsedFlag(api) {
682
+ let legacyCollapsed = false;
683
+ try {
684
+ legacyCollapsed = api.kv.get(KV_COLLAPSED_LEGACY_KEY, false) === true;
685
+ } catch {
686
+ return;
687
+ }
688
+ if (!legacyCollapsed) return;
689
+ try {
690
+ api.kv.set(KV_COLLAPSED_SIDEBAR_KEY, true);
691
+ api.kv.set(KV_COLLAPSED_STATUSLINE_KEY, true);
692
+ api.kv.set(KV_COLLAPSED_LEGACY_KEY, false);
693
+ } catch {
694
+ }
695
+ }
696
+ function createCollapseState(api) {
697
+ migrateLegacyCollapsedFlag(api);
698
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = createSignal(
699
+ readCollapsedFlag(api, KV_COLLAPSED_SIDEBAR_KEY)
700
+ );
701
+ const [isStatuslineCollapsed, setIsStatuslineCollapsed] = createSignal(
702
+ readCollapsedFlag(api, KV_COLLAPSED_STATUSLINE_KEY)
703
+ );
704
+ return {
705
+ isSidebarCollapsed,
706
+ isStatuslineCollapsed,
707
+ toggleSidebar() {
708
+ try {
709
+ const next = !isSidebarCollapsed();
710
+ setIsSidebarCollapsed(next);
711
+ api.kv.set(KV_COLLAPSED_SIDEBAR_KEY, next);
712
+ } catch {
713
+ }
714
+ },
715
+ toggleStatusline() {
716
+ try {
717
+ const next = !isStatuslineCollapsed();
718
+ setIsStatuslineCollapsed(next);
719
+ api.kv.set(KV_COLLAPSED_STATUSLINE_KEY, next);
720
+ } catch {
721
+ }
722
+ }
723
+ };
724
+ }
725
+ function isGoUsageProvider(providerId) {
726
+ return providerId === GO_PROVIDER_ID;
727
+ }
728
+ function makeProviderResolver(state, fallback) {
729
+ return (sessionId) => resolveProviderId(state, sessionId, fallback());
730
+ }
731
+ function readProviderDisplayNames(source) {
732
+ const names = /* @__PURE__ */ new Map();
733
+ try {
734
+ for (const provider of source.state?.provider ?? []) {
735
+ const id = toNonEmptyString(provider.id);
736
+ if (id === null) continue;
737
+ names.set(id, toNonEmptyString(provider.name) ?? id);
738
+ }
739
+ } catch {
740
+ }
741
+ return names;
742
+ }
743
+ function readModelDisplayName(source, providerID, modelID) {
744
+ try {
745
+ const provider = source.state?.provider?.find((entry) => entry.id === providerID);
746
+ return toNonEmptyString(provider?.models?.[modelID]?.name);
747
+ } catch {
748
+ return null;
749
+ }
750
+ }
751
+ function LabeledValueRow(props) {
752
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "row", justifyContent: "space-between", children: [
753
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: props.row.label }),
754
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: props.row.value })
755
+ ] });
756
+ }
757
+ var MIN_METER_WIDTH = 6;
758
+ function MeterBar(props) {
759
+ const fill = createMemo(() => meterFillPercent(props.percent));
760
+ const track = createMemo(() => 100 - fill());
761
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "row", flexGrow: 1, flexShrink: 1, minWidth: MIN_METER_WIDTH, children: [
762
+ /* @__PURE__ */ jsx(Show, { when: fill() > 0, children: /* @__PURE__ */ jsx(
763
+ "box",
764
+ {
765
+ flexGrow: fill(),
766
+ flexBasis: 0,
767
+ flexShrink: 0,
768
+ backgroundColor: meterColor(props.theme, props.severity)
769
+ }
770
+ ) }),
771
+ /* @__PURE__ */ jsx(Show, { when: track() > 0, children: /* @__PURE__ */ jsx(
772
+ "box",
773
+ {
774
+ flexGrow: track(),
775
+ flexBasis: 0,
776
+ flexShrink: 0,
777
+ backgroundColor: props.theme.current.backgroundElement
778
+ }
779
+ ) })
780
+ ] });
781
+ }
782
+ function GoPlanRow(props) {
783
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
784
+ /* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
785
+ /* @__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 }) }),
786
+ /* @__PURE__ */ jsx(MeterBar, { theme: props.theme, percent: props.row.percent, severity: props.row.severity }),
787
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", flexShrink: 0, children: formatPercentCell(props.row.percent) })
788
+ ] }),
789
+ /* @__PURE__ */ jsx(Show, { when: props.row.reset, children: (reset) => /* @__PURE__ */ jsxs("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: [
790
+ "resets in ",
791
+ reset()
792
+ ] }) })
793
+ ] });
794
+ }
795
+ function GoPlanSection(props) {
796
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
797
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.text, wrapMode: "none", children: /* @__PURE__ */ jsx("b", { children: GO_PLAN_HEADING }) }),
798
+ /* @__PURE__ */ jsx("box", { flexDirection: "column", paddingLeft: 1, children: /* @__PURE__ */ jsx(For, { each: props.rows, children: (row) => /* @__PURE__ */ jsx(GoPlanRow, { theme: props.theme, row }) }) })
799
+ ] });
800
+ }
801
+ function SectionHeader(props) {
802
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "row", gap: 1, flexShrink: 0, onMouseDown: props.onToggle, children: [
803
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.text, wrapMode: "none", flexShrink: 0, children: reactiveChild(() => props.expanded() ? SIDEBAR_EXPANDED_GLYPH : SIDEBAR_COLLAPSED_GLYPH) }),
804
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.text, wrapMode: "none", children: /* @__PURE__ */ jsx("b", { children: reactiveChild(() => {
805
+ const count = props.count();
806
+ return count === null ? props.label : `${props.label} (${count})`;
807
+ }) }) })
808
+ ] });
809
+ }
810
+ function CollapsibleSection(props) {
811
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
812
+ /* @__PURE__ */ jsx(
813
+ SectionHeader,
814
+ {
815
+ theme: props.theme,
816
+ label: props.label,
817
+ count: props.count,
818
+ expanded: props.expanded,
819
+ onToggle: props.onToggle
820
+ }
821
+ ),
822
+ /* @__PURE__ */ jsx("box", { flexDirection: "column", gap: 1, children: reactiveChild(() => props.expanded() ? props.body() : null) })
823
+ ] });
824
+ }
825
+ function GoShareRow(props) {
826
+ const rounded = Math.round(props.percent);
827
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
828
+ /* @__PURE__ */ jsx("box", { width: GO_SHARE_LABEL_WIDTH, marginRight: 1, flexShrink: 0, flexDirection: "row", children: /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: INTEGRATED_GO_SHARE_LABEL }) }),
829
+ /* @__PURE__ */ jsx(MeterBar, { theme: props.theme, percent: rounded, severity: meterSeverityForPercent(rounded) }),
830
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", flexShrink: 0, children: formatPercentCell(rounded) })
831
+ ] });
832
+ }
833
+ function GoUsageBlock(props) {
834
+ createEffect(() => {
835
+ const snapshot = props.snapshot();
836
+ if (snapshot !== null && snapshot.source === "unavailable") {
837
+ void logUsageError(
838
+ props.api,
839
+ snapshot.apiError ? `Go usage unavailable (${snapshot.apiError})` : "Go usage snapshot unavailable"
840
+ );
841
+ }
842
+ });
843
+ const body = createMemo(() => {
844
+ const snapshot = props.snapshot();
845
+ if (snapshot === null) {
846
+ return /* @__PURE__ */ jsx("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: "Go loading\u2026" });
847
+ }
848
+ if (snapshot.source === "unavailable") {
849
+ return /* @__PURE__ */ jsxs("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: [
850
+ "Go n/a (",
851
+ snapshot.apiError ?? "unavailable",
852
+ ")"
853
+ ] });
854
+ }
855
+ const planRows = buildPlanRows(snapshot).map((row) => ({ ...row, reset: null }));
856
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
857
+ /* @__PURE__ */ jsx(Show, { when: props.resetLine === void 0 ? null : props.resetLine(), children: (reset) => /* @__PURE__ */ jsxs("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: [
858
+ reset().label,
859
+ " resets in ",
860
+ reset().text
861
+ ] }) }),
862
+ /* @__PURE__ */ jsx(For, { each: planRows, children: (row) => /* @__PURE__ */ jsx(GoPlanRow, { theme: props.theme, row }) })
863
+ ] });
864
+ });
865
+ return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
866
+ /* @__PURE__ */ jsx("text", { fg: props.theme.current.text, children: /* @__PURE__ */ jsx("b", { children: "Go Usage" }) }),
867
+ reactiveChild(body)
868
+ ] });
869
+ }
870
+ function GoStatusline(props) {
871
+ return /* @__PURE__ */ jsx(Show, { when: props.snapshot(), fallback: null, children: (snapshot) => {
872
+ if (isSnapshotEmpty(snapshot())) return null;
873
+ return /* @__PURE__ */ jsx("text", { children: formatStatusline(snapshot()) });
874
+ } });
875
+ }
876
+
877
+ // src/tui.kilo.tsx
878
+ import { jsx as jsx2, jsxs as jsxs2 } from "@opentui/solid/jsx-runtime";
879
+ var SESSION_TREE_WALK_LIMIT = 32;
880
+ var HOST = "kilo";
881
+ var SLOT_ORDER = KILO_SLOT_ORDER;
882
+ var INTEGRATED_SLOT_ORDER = KILO_INTEGRATED_SLOT_ORDER;
883
+ var KV_SIDEBAR_MODE_KEY = "sidebar_mode";
884
+ function resolveSidebarMode(options, api) {
885
+ if (options !== void 0) {
886
+ const option = parseSidebarMode(options.sidebar_mode);
887
+ if (option !== null) return option;
888
+ }
889
+ const env = parseSidebarMode(hostEnv(HOST, "SIDEBAR_MODE"));
890
+ if (env !== null) return env;
891
+ try {
892
+ const stored = parseSidebarMode(api.kv.get(KV_SIDEBAR_MODE_KEY, null));
893
+ if (stored !== null) return stored;
894
+ } catch {
895
+ }
896
+ return DEFAULT_SIDEBAR_MODE;
897
+ }
898
+ function hostUsagePanelEnabled(api) {
899
+ try {
900
+ const entry = api.plugins?.list?.().find((status) => status.id === KILO_USAGE_PANEL_PLUGIN_ID);
901
+ if (entry === void 0) return null;
902
+ return entry.active || entry.enabled;
903
+ } catch {
904
+ return null;
905
+ }
906
+ }
907
+ async function applyHostUsagePanel(api, mode) {
908
+ const wantEnabled = mode === "standalone";
909
+ try {
910
+ const current = hostUsagePanelEnabled(api);
911
+ if (current === wantEnabled) return;
912
+ const applied = await (wantEnabled ? api.plugins.activate(KILO_USAGE_PANEL_PLUGIN_ID) : api.plugins.deactivate(KILO_USAGE_PANEL_PLUGIN_ID));
913
+ if (applied === true) return;
914
+ await logUsageError(
915
+ api,
916
+ `Could not ${wantEnabled ? "enable" : "disable"} Kilo's ${KILO_USAGE_PANEL_PLUGIN_ID} panel`
917
+ );
918
+ } catch (error) {
919
+ await logUsageError(
920
+ api,
921
+ `Kilo ${KILO_USAGE_PANEL_PLUGIN_ID} panel switch failed: ${errorMessage(error)}`
922
+ );
923
+ }
924
+ }
925
+ async function fetchSessionModelUsage(api, sessionId) {
926
+ if (sessionId.length === 0) return null;
927
+ try {
928
+ const result = await api.client.kilocode.sessionModelUsage({ sessionID: sessionId });
929
+ return parseSessionModelUsage(result.data);
930
+ } catch {
931
+ return null;
932
+ }
933
+ }
934
+ function isInSessionTree(api, sessionId, rootId) {
935
+ if (sessionId === rootId) return true;
936
+ let current = sessionId;
937
+ for (let depth = 0; depth < SESSION_TREE_WALK_LIMIT; depth += 1) {
938
+ let parent;
939
+ try {
940
+ parent = api.state?.session.get(current)?.parentID;
941
+ } catch {
942
+ return false;
943
+ }
944
+ if (parent === void 0 || parent.length === 0) return false;
945
+ if (parent === rootId) return true;
946
+ current = parent;
947
+ }
948
+ return false;
949
+ }
950
+ function trackSubscription(unsubscribes, subscribe) {
951
+ try {
952
+ const unsubscribe = subscribe();
953
+ if (typeof unsubscribe === "function") unsubscribes.push(unsubscribe);
954
+ } catch {
955
+ }
956
+ }
957
+ async function initializeTui(api, options) {
958
+ const surfaces = resolveSurfaceSelection(options, api, HOST);
959
+ const usageStore = createUsageStore(api, HOST);
960
+ const collapse = createCollapseState(api);
961
+ let sidebarMode = resolveSidebarMode(options, api);
962
+ void applyHostUsagePanel(api, sidebarMode);
963
+ const [activeProviderId, setActiveProviderId] = createSignal2(void 0);
964
+ try {
965
+ const modelString = api.state?.config?.model;
966
+ if (typeof modelString === "string" && modelString.length > 0) {
967
+ const providerPart = modelString.split("/")[0];
968
+ if (providerPart !== void 0 && providerPart.length > 0) setActiveProviderId(providerPart);
969
+ }
970
+ } catch {
971
+ }
972
+ const resolveActiveProviderId = makeProviderResolver(api.state, activeProviderId);
973
+ function toggleSidebarMode() {
974
+ const next = sidebarMode === "integrated" ? "standalone" : "integrated";
975
+ sidebarMode = next;
976
+ void applyHostUsagePanel(api, next);
977
+ try {
978
+ api.kv.set(KV_SIDEBAR_MODE_KEY, next);
979
+ } catch {
980
+ }
981
+ }
982
+ function planResetLine() {
983
+ const snapshot = usageStore.snapshot();
984
+ if (snapshot === null || snapshot.source === "unavailable") return null;
985
+ return relevantReset(snapshot);
986
+ }
987
+ function currentPlanRows() {
988
+ const snapshot = usageStore.snapshot();
989
+ if (snapshot === null || snapshot.source === "unavailable") return [];
990
+ return buildPlanRows(snapshot);
991
+ }
992
+ function ProviderUsageGroup(props) {
993
+ const isGo = props.group.providerID === GO_PROVIDER_ID;
994
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", gap: 1, children: [
995
+ /* @__PURE__ */ jsx2("text", { fg: props.theme.current.text, wrapMode: "none", children: props.group.providerName }),
996
+ /* @__PURE__ */ jsx2(Show2, { when: isGo && props.planRows.length > 0, children: /* @__PURE__ */ jsx2(GoPlanSection, { theme: props.theme, rows: props.planRows }) }),
997
+ /* @__PURE__ */ jsxs2("box", { flexDirection: "row", gap: 1, children: [
998
+ /* @__PURE__ */ jsx2("box", { width: 1, flexShrink: 0 }),
999
+ /* @__PURE__ */ jsx2("text", { flexGrow: 1, minWidth: 0, fg: props.theme.current.textMuted, wrapMode: "none", children: "Model" }),
1000
+ /* @__PURE__ */ jsx2(
1001
+ "box",
1002
+ {
1003
+ width: KILO_STEPS_COLUMN_WIDTH,
1004
+ flexDirection: "row",
1005
+ flexShrink: 0,
1006
+ justifyContent: "flex-end",
1007
+ children: /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: "Steps" })
1008
+ }
1009
+ ),
1010
+ /* @__PURE__ */ jsx2(
1011
+ "box",
1012
+ {
1013
+ width: KILO_COST_COLUMN_WIDTH,
1014
+ flexDirection: "row",
1015
+ flexShrink: 0,
1016
+ justifyContent: "flex-end",
1017
+ children: /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: "Cost" })
1018
+ }
1019
+ )
1020
+ ] }),
1021
+ /* @__PURE__ */ jsx2(For2, { each: props.group.models, children: (model) => {
1022
+ const key = `${model.providerID}/${model.modelID}`;
1023
+ return /* @__PURE__ */ jsx2(
1024
+ ModelUsageRow,
1025
+ {
1026
+ theme: props.theme,
1027
+ model,
1028
+ isGo,
1029
+ expanded: () => props.expandedModels().has(key),
1030
+ goShare: () => mockGoShare(HOST) ?? goSharePercent(usageTokenCount(model.tokens), props.goTotal),
1031
+ onToggle: () => props.onToggleModel(key)
1032
+ }
1033
+ );
1034
+ } })
1035
+ ] });
1036
+ }
1037
+ function ModelUsageRow(props) {
1038
+ const detail = createMemo2(
1039
+ () => props.expanded() ? /* @__PURE__ */ jsx2("box", { flexDirection: "column", paddingLeft: 2, children: /* @__PURE__ */ jsx2(For2, { each: buildModelTokenRows(props.model.tokens), children: (row) => /* @__PURE__ */ jsx2(LabeledValueRow, { theme: props.theme, row }) }) }) : null
1040
+ );
1041
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", gap: 1, children: [
1042
+ /* @__PURE__ */ jsxs2("box", { flexDirection: "row", gap: 1, flexShrink: 0, onMouseDown: props.onToggle, children: [
1043
+ /* @__PURE__ */ jsx2("text", { fg: props.theme.current.text, wrapMode: "none", flexShrink: 0, children: reactiveChild(() => props.expanded() ? SIDEBAR_EXPANDED_GLYPH : SIDEBAR_COLLAPSED_GLYPH) }),
1044
+ /* @__PURE__ */ jsx2("box", { flexGrow: 1, minWidth: 0, overflow: "hidden", children: /* @__PURE__ */ jsx2("text", { fg: props.theme.current.text, wrapMode: "none", children: /* @__PURE__ */ jsx2("b", { children: modelDisplayName(
1045
+ readModelDisplayName(api, props.model.providerID, props.model.modelID),
1046
+ props.model.modelID
1047
+ ) }) }) }),
1048
+ /* @__PURE__ */ jsx2(
1049
+ "box",
1050
+ {
1051
+ width: KILO_STEPS_COLUMN_WIDTH,
1052
+ flexDirection: "row",
1053
+ flexShrink: 0,
1054
+ justifyContent: "flex-end",
1055
+ children: /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: formatUsageCount(props.model.steps) })
1056
+ }
1057
+ ),
1058
+ /* @__PURE__ */ jsx2(
1059
+ "box",
1060
+ {
1061
+ width: KILO_COST_COLUMN_WIDTH,
1062
+ flexDirection: "row",
1063
+ flexShrink: 0,
1064
+ justifyContent: "flex-end",
1065
+ children: /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: formatUsageCost(props.model.cost) })
1066
+ }
1067
+ )
1068
+ ] }),
1069
+ /* @__PURE__ */ jsx2(Show2, { when: props.isGo, children: /* @__PURE__ */ jsx2("box", { paddingLeft: 2, children: /* @__PURE__ */ jsx2(GoShareRow, { theme: props.theme, percent: props.goShare() }) }) }),
1070
+ reactiveChild(detail)
1071
+ ] });
1072
+ }
1073
+ function ModelsSection(props) {
1074
+ const [isExpanded, setIsExpanded] = createSignal2(true);
1075
+ const [expandedModels, setExpandedModels] = createSignal2(/* @__PURE__ */ new Set());
1076
+ function toggleModel(key) {
1077
+ setExpandedModels((current) => {
1078
+ const next = new Set(current);
1079
+ if (!next.delete(key)) next.add(key);
1080
+ return next;
1081
+ });
1082
+ }
1083
+ const modelCount = createMemo2(() => props.usage()?.models.length ?? 0);
1084
+ const body = createMemo2(() => {
1085
+ const usage = props.usage();
1086
+ const models = usage?.models ?? [];
1087
+ const providerNames = readProviderDisplayNames(api);
1088
+ const grouped = groupModelsByProvider(models, providerNames);
1089
+ const groups = grouped.length === 0 && props.planRows().length > 0 ? [
1090
+ {
1091
+ providerID: GO_PROVIDER_ID,
1092
+ providerName: providerNames.get(GO_PROVIDER_ID) ?? GO_PROVIDER_ID,
1093
+ models: []
1094
+ }
1095
+ ] : grouped;
1096
+ const goTotal = totalGoTokens(models);
1097
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", gap: 1, paddingTop: 1, children: [
1098
+ /* @__PURE__ */ jsx2(Show2, { when: models.length === 0, children: /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: MODELS_EMPTY_LABEL }) }),
1099
+ /* @__PURE__ */ jsx2(For2, { each: groups, children: (group) => /* @__PURE__ */ jsx2(
1100
+ ProviderUsageGroup,
1101
+ {
1102
+ theme: props.theme,
1103
+ group,
1104
+ planRows: props.planRows(),
1105
+ goTotal,
1106
+ expandedModels,
1107
+ onToggleModel: toggleModel
1108
+ }
1109
+ ) })
1110
+ ] });
1111
+ });
1112
+ return /* @__PURE__ */ jsx2(
1113
+ CollapsibleSection,
1114
+ {
1115
+ theme: props.theme,
1116
+ label: INTEGRATED_MODELS_SECTION_LABEL,
1117
+ count: modelCount,
1118
+ expanded: isExpanded,
1119
+ onToggle: () => setIsExpanded((current) => !current),
1120
+ body
1121
+ }
1122
+ );
1123
+ }
1124
+ function GoIntegratedPanel(props) {
1125
+ const [usage, setUsage] = createSignal2(null);
1126
+ const [loadFailed, setLoadFailed] = createSignal2(false);
1127
+ const [refetchToken, setRefetchToken] = createSignal2(0);
1128
+ const [isTokensExpanded, setIsTokensExpanded] = createSignal2(true);
1129
+ createEffect2(() => {
1130
+ const sessionId = props.sessionId;
1131
+ refetchToken();
1132
+ if (sessionId.length === 0) return;
1133
+ let cancelled = false;
1134
+ void fetchSessionModelUsage(api, sessionId).then((result) => {
1135
+ if (cancelled) return;
1136
+ setUsage(result);
1137
+ if (result === null && !loadFailed()) {
1138
+ void logUsageError(api, "Session model usage unavailable (request failed)");
1139
+ }
1140
+ setLoadFailed(result === null);
1141
+ });
1142
+ onCleanup(() => {
1143
+ cancelled = true;
1144
+ });
1145
+ });
1146
+ createEffect2(() => {
1147
+ const sessionId = props.sessionId;
1148
+ const unsubscribes = [];
1149
+ const inTree = (id) => isInSessionTree(api, id, sessionId);
1150
+ const refetch = () => {
1151
+ setRefetchToken((current) => current + 1);
1152
+ };
1153
+ trackSubscription(
1154
+ unsubscribes,
1155
+ () => api.event.on("message.part.updated", (event) => {
1156
+ if (event.properties.part.type !== "step-finish") return;
1157
+ if (inTree(event.properties.sessionID)) refetch();
1158
+ })
1159
+ );
1160
+ trackSubscription(
1161
+ unsubscribes,
1162
+ () => api.event.on("message.part.removed", (event) => {
1163
+ if (inTree(event.properties.sessionID)) refetch();
1164
+ })
1165
+ );
1166
+ trackSubscription(
1167
+ unsubscribes,
1168
+ () => api.event.on("message.removed", (event) => {
1169
+ if (inTree(event.properties.sessionID)) refetch();
1170
+ })
1171
+ );
1172
+ trackSubscription(
1173
+ unsubscribes,
1174
+ () => api.event.on("session.created", (event) => {
1175
+ if (inTree(event.properties.sessionID)) refetch();
1176
+ })
1177
+ );
1178
+ trackSubscription(
1179
+ unsubscribes,
1180
+ () => api.event.on("session.deleted", (event) => {
1181
+ if (inTree(event.properties.sessionID)) refetch();
1182
+ })
1183
+ );
1184
+ trackSubscription(unsubscribes, () => api.event.on("server.connected", refetch));
1185
+ onCleanup(() => {
1186
+ for (const unsubscribe of unsubscribes) {
1187
+ try {
1188
+ unsubscribe();
1189
+ } catch {
1190
+ }
1191
+ }
1192
+ });
1193
+ });
1194
+ const tokenBody = createMemo2(() => {
1195
+ const data = usage();
1196
+ if (data === null) {
1197
+ return /* @__PURE__ */ jsx2("text", { fg: props.theme.current.textMuted, wrapMode: "none", children: loadFailed() ? INTEGRATED_UNAVAILABLE_LABEL : INTEGRATED_LOADING_LABEL });
1198
+ }
1199
+ return /* @__PURE__ */ jsx2("box", { flexDirection: "column", children: /* @__PURE__ */ jsx2(For2, { each: buildTokenUsageRows(data.totals), children: (row) => /* @__PURE__ */ jsx2(LabeledValueRow, { theme: props.theme, row }) }) });
1200
+ });
1201
+ return (
1202
+ // Integrated mode draws the plan ONCE, as the `Go Plan` meters inside the
1203
+ // OpenCode Go group of the Models table. A `Go Usage` block above it (as
1204
+ // there was) said the same three numbers twice, in two different
1205
+ // layouts, on the same screen.
1206
+ /* @__PURE__ */ jsxs2("box", { flexDirection: "column", gap: 1, children: [
1207
+ /* @__PURE__ */ jsx2(
1208
+ CollapsibleSection,
1209
+ {
1210
+ theme: props.theme,
1211
+ label: INTEGRATED_TOKENS_SECTION_LABEL,
1212
+ count: () => null,
1213
+ expanded: isTokensExpanded,
1214
+ onToggle: () => setIsTokensExpanded((current) => !current),
1215
+ body: tokenBody
1216
+ }
1217
+ ),
1218
+ /* @__PURE__ */ jsx2(ModelsSection, { theme: props.theme, usage, planRows: currentPlanRows })
1219
+ ] })
1220
+ );
1221
+ }
1222
+ function GoSidebarPanel(props) {
1223
+ return /* @__PURE__ */ jsx2(
1224
+ GoUsageBlock,
1225
+ {
1226
+ api,
1227
+ theme: props.theme,
1228
+ snapshot: usageStore.snapshot,
1229
+ resetLine: planResetLine
1230
+ }
1231
+ );
1232
+ }
1233
+ if (surfaces.sidebar) {
1234
+ try {
1235
+ api.slots.register({
1236
+ order: sidebarMode === "integrated" ? INTEGRATED_SLOT_ORDER : SLOT_ORDER,
1237
+ slots: {
1238
+ sidebar_content(ctx, props) {
1239
+ try {
1240
+ if (props.session_id.length === 0) return null;
1241
+ if (!isGoUsageProvider(resolveActiveProviderId(props.session_id))) return null;
1242
+ if (api.route.current.name !== "session") return null;
1243
+ if (collapse.isSidebarCollapsed()) return null;
1244
+ if (sidebarMode === "integrated") {
1245
+ return /* @__PURE__ */ jsx2(GoIntegratedPanel, { theme: ctx.theme, sessionId: props.session_id });
1246
+ }
1247
+ return /* @__PURE__ */ jsx2(GoSidebarPanel, { theme: ctx.theme });
1248
+ } catch {
1249
+ return null;
1250
+ }
1251
+ }
1252
+ }
1253
+ });
1254
+ } catch {
1255
+ }
1256
+ }
1257
+ if (surfaces.statusline) {
1258
+ try {
1259
+ api.slots.register({
1260
+ order: SLOT_ORDER,
1261
+ slots: {
1262
+ session_prompt_right(_ctx, props) {
1263
+ try {
1264
+ if (props.session_id.length === 0) return null;
1265
+ if (!isGoUsageProvider(resolveActiveProviderId(props.session_id))) return null;
1266
+ if (collapse.isStatuslineCollapsed()) return null;
1267
+ return /* @__PURE__ */ jsx2(GoStatusline, { snapshot: usageStore.snapshot });
1268
+ } catch {
1269
+ return null;
1270
+ }
1271
+ }
1272
+ }
1273
+ });
1274
+ } catch {
1275
+ }
1276
+ }
1277
+ let unregisterToggleCommand = () => {
1278
+ };
1279
+ try {
1280
+ const unregister = api.command?.register(() => [
1281
+ {
1282
+ title: "Go usage: toggle sidebar",
1283
+ value: "oc-go-usage-display.toggle-sidebar",
1284
+ category: "Go",
1285
+ onSelect: () => collapse.toggleSidebar()
1286
+ },
1287
+ {
1288
+ title: "Go usage: toggle statusline",
1289
+ value: "oc-go-usage-display.toggle-statusline",
1290
+ category: "Go",
1291
+ onSelect: () => collapse.toggleStatusline()
1292
+ },
1293
+ {
1294
+ title: "Go usage: toggle sidebar mode",
1295
+ value: "oc-go-usage-display.toggle-sidebar-mode",
1296
+ category: "Go",
1297
+ onSelect: () => toggleSidebarMode()
1298
+ }
1299
+ ]);
1300
+ if (typeof unregister === "function") unregisterToggleCommand = unregister;
1301
+ } catch {
1302
+ }
1303
+ let unsubscribeSession = () => {
1304
+ };
1305
+ let unsubscribeMessage = () => {
1306
+ };
1307
+ try {
1308
+ const unsubscribe = api.event.on("session.updated", (event) => {
1309
+ const providerId = event?.properties?.info?.model?.providerID;
1310
+ if (providerId !== void 0) setActiveProviderId(providerId);
1311
+ usageStore.refreshSafely(0);
1312
+ });
1313
+ if (typeof unsubscribe === "function") unsubscribeSession = unsubscribe;
1314
+ } catch {
1315
+ }
1316
+ try {
1317
+ const unsubscribe = api.event.on("message.updated", () => usageStore.refreshSafely(EVENT_TTL_MS));
1318
+ if (typeof unsubscribe === "function") unsubscribeMessage = unsubscribe;
1319
+ } catch {
1320
+ }
1321
+ let pollTimer = null;
1322
+ try {
1323
+ pollTimer = setInterval(() => usageStore.refreshSafely(), POLL_INTERVAL_MS);
1324
+ } catch {
1325
+ }
1326
+ try {
1327
+ api.lifecycle.onDispose(() => {
1328
+ if (pollTimer !== null) {
1329
+ try {
1330
+ clearInterval(pollTimer);
1331
+ } catch {
1332
+ }
1333
+ }
1334
+ for (const teardown of [unsubscribeSession, unsubscribeMessage, unregisterToggleCommand]) {
1335
+ try {
1336
+ teardown();
1337
+ } catch {
1338
+ }
1339
+ }
1340
+ });
1341
+ } catch {
1342
+ if (pollTimer !== null) {
1343
+ try {
1344
+ clearInterval(pollTimer);
1345
+ } catch {
1346
+ }
1347
+ }
1348
+ }
1349
+ await usageStore.refresh();
1350
+ }
1351
+ var goUsageTui = async (api, options) => {
1352
+ try {
1353
+ await initializeTui(api, options);
1354
+ } catch (error) {
1355
+ void logUsageError(
1356
+ api,
1357
+ `Go usage TUI failed to initialize: ${errorMessage(error)}; continuing without display`
1358
+ );
1359
+ }
1360
+ };
1361
+ var tui_kilo_default = { id: "oc-go-usage-display", tui: goUsageTui };
1362
+ export {
1363
+ tui_kilo_default as default
1364
+ };