tokmon 0.19.8 → 0.20.1

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.
@@ -0,0 +1,3497 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PROVIDERS,
4
+ PROVIDER_ORDER,
5
+ TOKMON_WS_METHODS,
6
+ TOKMON_WS_PATH,
7
+ TokmonRpcGroup,
8
+ accountsByProvider,
9
+ buildAccounts,
10
+ coalesceTables,
11
+ col,
12
+ currency,
13
+ cursorModelSpend,
14
+ detectProviders,
15
+ fetchPeak,
16
+ resolveTimezone,
17
+ shortDate,
18
+ systemTimezone,
19
+ time,
20
+ tokens
21
+ } from "./chunk-5BW4H7WW.js";
22
+ import {
23
+ COLOR_PALETTE,
24
+ DEFAULTS,
25
+ configLocation,
26
+ generateAccountId,
27
+ getTrackedAccountRows,
28
+ isValidTimezone,
29
+ loadConfig,
30
+ normalizeConfig,
31
+ pickAccentColor,
32
+ sanitizeTyped,
33
+ saveConfigSync,
34
+ snapshotCacheFile
35
+ } from "./chunk-XQEJ4WQ5.js";
36
+ import {
37
+ glyphs
38
+ } from "./chunk-RF4GGQGM.js";
39
+
40
+ // src/bootstrap-ink.tsx
41
+ import { render } from "ink";
42
+ import { MouseProvider } from "@zenobius/ink-mouse";
43
+
44
+ // src/app.tsx
45
+ import { useState as useState4, useEffect as useEffect4, useCallback, useRef as useRef3, useMemo as useMemo2 } from "react";
46
+ import { Box as Box11, Text as Text11, useInput, useStdout, useApp } from "ink";
47
+ import { useMouse } from "@zenobius/ink-mouse";
48
+
49
+ // src/config-sync.ts
50
+ function deepEqual(a, b) {
51
+ if (a === b) return true;
52
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
53
+ if (Array.isArray(a) || Array.isArray(b)) {
54
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
55
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
56
+ return true;
57
+ }
58
+ const ka = Object.keys(a);
59
+ const kb = Object.keys(b);
60
+ if (ka.length !== kb.length) return false;
61
+ for (const k of ka) {
62
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
63
+ if (!deepEqual(a[k], b[k])) return false;
64
+ }
65
+ return true;
66
+ }
67
+ function reconcileDaemonConfig(previous, daemonConfig, pendingLocalConfig) {
68
+ if (pendingLocalConfig) {
69
+ if (deepEqual(daemonConfig, pendingLocalConfig)) {
70
+ return { config: previous && deepEqual(previous, daemonConfig) ? previous : daemonConfig, pendingLocalConfig: null };
71
+ }
72
+ return { config: previous ?? pendingLocalConfig, pendingLocalConfig };
73
+ }
74
+ if (previous && deepEqual(previous, daemonConfig)) {
75
+ return { config: previous, pendingLocalConfig: null };
76
+ }
77
+ if (previous?.onboarded === true && daemonConfig.onboarded === false) {
78
+ return { config: previous, pendingLocalConfig: null };
79
+ }
80
+ return { config: daemonConfig, pendingLocalConfig: null };
81
+ }
82
+
83
+ // src/client/seed-cache.ts
84
+ import { readFile } from "fs/promises";
85
+ async function loadSeedSnapshot() {
86
+ try {
87
+ const parsed = JSON.parse(await readFile(snapshotCacheFile(), "utf-8"));
88
+ if (!parsed || !Array.isArray(parsed.accounts)) return {};
89
+ const out = {};
90
+ for (const a of parsed.accounts) {
91
+ if (a.dashboard || a.billing) out[a.id] = { dashboard: a.dashboard ?? null, billing: a.billing ?? null };
92
+ }
93
+ return out;
94
+ } catch {
95
+ return {};
96
+ }
97
+ }
98
+
99
+ // src/ui/shared.tsx
100
+ import { appendFileSync } from "fs";
101
+ import { memo, useEffect, useRef, useState } from "react";
102
+ import { Box, Text } from "ink";
103
+ import { useOnMouseClick } from "@zenobius/ink-mouse";
104
+ import { jsx, jsxs } from "react/jsx-runtime";
105
+ function truncateName(s, n) {
106
+ const ell = glyphs().ellipsis;
107
+ return s.length > n ? s.slice(0, n - ell.length) + ell : s;
108
+ }
109
+ function ClickableBox({ onClick, children, ...props }) {
110
+ const ref = useRef(null);
111
+ useOnMouseClick(ref, (clicked) => {
112
+ if (clicked) onClick();
113
+ });
114
+ return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
115
+ }
116
+ var SGR_PRESS = /\x1b\[<(\d+);(\d+);(\d+)M/g;
117
+ var linkHits = /* @__PURE__ */ new Set();
118
+ function dispatchLinkClicks(chunk) {
119
+ if (linkHits.size === 0) return;
120
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
121
+ SGR_PRESS.lastIndex = 0;
122
+ let m;
123
+ while ((m = SGR_PRESS.exec(s)) !== null) {
124
+ const code = Number(m[1]);
125
+ if (code & 64 || code & 32) continue;
126
+ const mx = Number(m[2]) - 1;
127
+ const my = Number(m[3]) - 1;
128
+ if (process.env.TOKMON_LINKDEBUG) {
129
+ try {
130
+ appendFileSync(process.env.TOKMON_LINKDEBUG, `DISPATCH code=${code} mx=${mx} my=${my} hits=${linkHits.size}
131
+ `);
132
+ } catch {
133
+ }
134
+ }
135
+ for (const hit of [...linkHits]) {
136
+ if (hit(mx, my)) break;
137
+ }
138
+ }
139
+ }
140
+ function nodeBox(node) {
141
+ const yn = node?.yogaNode;
142
+ if (!yn) return null;
143
+ const l = yn.getComputedLayout();
144
+ let left = l.left, top = l.top;
145
+ let p = node?.parentNode;
146
+ while (p) {
147
+ const pn = p.yogaNode;
148
+ if (!pn) break;
149
+ const pl = pn.getComputedLayout();
150
+ left += pl.left;
151
+ top += pl.top;
152
+ p = p.parentNode;
153
+ }
154
+ return { left, top, width: l.width, height: l.height };
155
+ }
156
+ function LinkBox({ onClick, children, ...props }) {
157
+ const ref = useRef(null);
158
+ const onClickRef = useRef(onClick);
159
+ onClickRef.current = onClick;
160
+ useEffect(() => {
161
+ const hit = (mx, my) => {
162
+ const box = nodeBox(ref.current);
163
+ if (process.env.TOKMON_LINKDEBUG) {
164
+ try {
165
+ appendFileSync(process.env.TOKMON_LINKDEBUG, `HIT? mx=${mx} my=${my} box=${box ? `${box.left},${box.top} ${box.width}x${box.height}` : "null"}
166
+ `);
167
+ } catch {
168
+ }
169
+ }
170
+ if (!box || box.width <= 0 || box.height <= 0) return false;
171
+ if (mx >= box.left && mx < box.left + box.width && my >= box.top && my < box.top + box.height) {
172
+ onClickRef.current();
173
+ return true;
174
+ }
175
+ return false;
176
+ };
177
+ linkHits.add(hit);
178
+ return () => {
179
+ linkHits.delete(hit);
180
+ };
181
+ }, []);
182
+ return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
183
+ }
184
+ function Spinner({ label }) {
185
+ const frames = glyphs().spinner;
186
+ const [i, setI] = useState(0);
187
+ useEffect(() => {
188
+ const id = setInterval(() => setI((n) => (n + 1) % frames.length), 80);
189
+ return () => clearInterval(id);
190
+ }, []);
191
+ return /* @__PURE__ */ jsxs(Box, { children: [
192
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
193
+ frames[i],
194
+ " "
195
+ ] }),
196
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
197
+ ] });
198
+ }
199
+ var TabBar = memo(function TabBar2({ tabs, active, onSelect }) {
200
+ return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
201
+ " ",
202
+ t,
203
+ " "
204
+ ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
205
+ " ",
206
+ t,
207
+ " "
208
+ ] }) }, t)) });
209
+ });
210
+ var PeakBadge = memo(function PeakBadge2({ peak }) {
211
+ const color = peak.state === "peak" ? "red" : "green";
212
+ return /* @__PURE__ */ jsxs(Box, { children: [
213
+ /* @__PURE__ */ jsxs(Text, { color, children: [
214
+ glyphs().dot,
215
+ " "
216
+ ] }),
217
+ /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
218
+ peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
219
+ " (",
220
+ fmtMinutes(peak.minutesUntilChange),
221
+ ")"
222
+ ] })
223
+ ] });
224
+ });
225
+ function fmtMinutes(mins) {
226
+ if (mins < 60) return `${mins}m`;
227
+ const h = Math.floor(mins / 60);
228
+ const m = mins % 60;
229
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
230
+ }
231
+ function currencySymbol(cur) {
232
+ return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
233
+ }
234
+ function sparkline(values) {
235
+ if (values.length === 0) return "";
236
+ const spark = glyphs().spark;
237
+ const max = Math.max(...values);
238
+ if (max <= 0) return spark[0].repeat(values.length);
239
+ return values.map((v) => {
240
+ if (v <= 0) return spark[0];
241
+ const idx = Math.min(spark.length - 1, 1 + Math.round(v / max * (spark.length - 2)));
242
+ return spark[idx];
243
+ }).join("");
244
+ }
245
+ function Bar({ pct, color, width = 24 }) {
246
+ const filled = Math.max(0, Math.min(width, Math.round(pct / 100 * width)));
247
+ return /* @__PURE__ */ jsxs(Text, { children: [
248
+ /* @__PURE__ */ jsx(Text, { color, children: glyphs().barFull.repeat(filled) }),
249
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: glyphs().barEmpty.repeat(width - filled) })
250
+ ] });
251
+ }
252
+ function metricValueText(m) {
253
+ if (m.format.kind === "dollars") {
254
+ const sym = currencySymbol(m.format.currency);
255
+ const used = `${sym}${m.used.toFixed(2)}`;
256
+ return m.limit != null ? `${used} / ${sym}${m.limit.toFixed(2)}` : `${used}`;
257
+ }
258
+ if (m.format.kind === "count") {
259
+ const suffix = m.format.suffix ? ` ${m.format.suffix}` : "";
260
+ const used = `${Math.round(m.used)}${suffix}`;
261
+ return m.limit != null ? `${used} / ${Math.round(m.limit)}` : used;
262
+ }
263
+ return `${Math.round(m.used)}%`;
264
+ }
265
+
266
+ // src/ui/dashboard.tsx
267
+ import { memo as memo2 } from "react";
268
+ import { Box as Box2, Text as Text2 } from "ink";
269
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
270
+ var GAP = 2;
271
+ var MIN_CARD = 56;
272
+ var MIN_CARD_DENSE = 50;
273
+ var CARD_H = { full: 14, compact: 12, mini: 8 };
274
+ var VARIANT_ORDER = ["full", "compact", "mini"];
275
+ var INDICATOR_ROWS = 1;
276
+ var MAX_SINGLE_CARD = Math.round(MIN_CARD * 1.6);
277
+ function chooseLayout(content, budget, n, single, cols) {
278
+ if (n <= 0) return { ncols: 1, variant: "mini", cardsPerPage: 1, pageCount: 1 };
279
+ const gridHeight = (rows, H2) => rows * H2 + Math.max(0, rows - 1);
280
+ const colCap = single ? 1 : cols >= 3 * MIN_CARD_DENSE + 2 * GAP ? 3 : cols >= 2 * MIN_CARD + GAP ? 2 : 1;
281
+ const maxCols = Math.max(1, Math.min(colCap, n));
282
+ const cardWidthAt = (nc) => nc <= 1 ? content : Math.floor((content - GAP * (nc - 1)) / nc);
283
+ const minWidthAt = (nc) => nc >= 3 ? MIN_CARD_DENSE : MIN_CARD;
284
+ for (const variant of VARIANT_ORDER) {
285
+ for (let nc = maxCols; nc >= 1; nc--) {
286
+ if (nc > 1 && cardWidthAt(nc) < minWidthAt(nc)) continue;
287
+ const rows = Math.ceil(n / nc);
288
+ if (gridHeight(rows, CARD_H[variant]) <= budget) {
289
+ return { ncols: nc, variant, cardsPerPage: n, pageCount: 1 };
290
+ }
291
+ }
292
+ }
293
+ let ncols = 1;
294
+ for (let nc = maxCols; nc >= 1; nc--) {
295
+ if (nc === 1 || cardWidthAt(nc) >= minWidthAt(nc)) {
296
+ ncols = nc;
297
+ break;
298
+ }
299
+ }
300
+ const H = CARD_H.mini;
301
+ const fitBudget = budget - INDICATOR_ROWS;
302
+ const rowsThatFit = Math.max(1, Math.floor((fitBudget + 1) / (H + 1)));
303
+ const cardsPerPage = Math.max(1, rowsThatFit * ncols);
304
+ const pageCount = Math.max(1, Math.ceil(n / cardsPerPage));
305
+ return { ncols, variant: "mini", cardsPerPage, pageCount };
306
+ }
307
+ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget, focusId, layout, page = 0 }) {
308
+ if (groups.length === 0) {
309
+ return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
310
+ "No providers enabled ",
311
+ glyphs().emDash,
312
+ " press s to pick providers."
313
+ ] });
314
+ }
315
+ let shown = groups;
316
+ if (layout === "single" && focusId === null) shown = groups.slice(0, 1);
317
+ const single = focusId !== null || layout === "single";
318
+ const content = Math.max(MIN_CARD, cols - 4);
319
+ const { ncols, variant, cardsPerPage, pageCount } = chooseLayout(content, budget, shown.length, single, cols);
320
+ let cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
321
+ if (ncols === 1 && cardW > MAX_SINGLE_CARD) cardW = MAX_SINGLE_CARD;
322
+ const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
323
+ const visible = pageCount > 1 ? shown.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : shown;
324
+ return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
325
+ /* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant }) }, g.provider)) }),
326
+ pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
327
+ " ",
328
+ glyphs().middot,
329
+ " page ",
330
+ pg + 1,
331
+ "/",
332
+ pageCount,
333
+ " ",
334
+ glyphs().middot,
335
+ " scroll ",
336
+ glyphs().arrowU,
337
+ glyphs().arrowD
338
+ ] })
339
+ ] });
340
+ });
341
+ function ProviderCard({ provider, accounts, stats, width, variant }) {
342
+ const meta = PROVIDERS[provider];
343
+ const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
344
+ const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
345
+ const agg = meta.hasUsage && dashboards.length > 0 ? aggregate(dashboards) : null;
346
+ const plan = items.map((i) => i.s?.billing?.plan).find(Boolean) ?? null;
347
+ const activity = items.map((i) => i.s?.billing?.activity).find(Boolean) ?? null;
348
+ const inner = width - 4;
349
+ const barW = Math.max(10, Math.min(46, inner - 20));
350
+ const hasSpark = !!agg && agg.series.some((v) => v > 0);
351
+ const showBars = variant !== "mini";
352
+ const showSpark = variant === "full";
353
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, borderStyle: glyphs().border, borderColor: meta.color, paddingX: 1, children: [
354
+ /* @__PURE__ */ jsxs2(Box2, { children: [
355
+ /* @__PURE__ */ jsxs2(Text2, { color: meta.color, children: [
356
+ glyphs().dot,
357
+ " "
358
+ ] }),
359
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: meta.color, children: meta.name }),
360
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
361
+ plan && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: plan })
362
+ ] }),
363
+ meta.hasUsage && (agg ? /* @__PURE__ */ jsxs2(Fragment, { children: [
364
+ /* @__PURE__ */ jsx2(Box2, { height: 1 }),
365
+ /* @__PURE__ */ jsx2(SummaryRow, { label: "Today", s: agg.today }),
366
+ /* @__PURE__ */ jsx2(SummaryRow, { label: "This Week", s: agg.week }),
367
+ /* @__PURE__ */ jsx2(SummaryRow, { label: "This Month", s: agg.month }),
368
+ /* @__PURE__ */ jsx2(KpiLine, { agg })
369
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
370
+ /* @__PURE__ */ jsx2(Box2, { height: 1 }),
371
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
372
+ "Fetching usage",
373
+ glyphs().ellipsis
374
+ ] })
375
+ ] })),
376
+ meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
377
+ meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
378
+ /* @__PURE__ */ jsx2(LimitsBlock, { items, barW })
379
+ ] }),
380
+ meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items }),
381
+ hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
382
+ /* @__PURE__ */ jsx2(Rule, { inner }),
383
+ /* @__PURE__ */ jsx2(SparkFooter, { series: agg.series, month: agg.month.cost, color: meta.color })
384
+ ] }),
385
+ !meta.hasUsage && activity && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
386
+ /* @__PURE__ */ jsx2(Rule, { inner }),
387
+ /* @__PURE__ */ jsxs2(Box2, { children: [
388
+ /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
389
+ /* @__PURE__ */ jsx2(Text2, { color: meta.color, children: sparkline(activity.series) }),
390
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activity.summary }) })
391
+ ] })
392
+ ] })
393
+ ] });
394
+ }
395
+ function CompactBilling({ items }) {
396
+ const billing = items.map((i) => i.s?.billing).find(Boolean);
397
+ if (!billing) return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
398
+ "Fetching",
399
+ glyphs().ellipsis
400
+ ] });
401
+ if (billing.error) return /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error });
402
+ const m = billing.metrics[0];
403
+ if (!m) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" });
404
+ return /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) });
405
+ }
406
+ function Rule({ inner }) {
407
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: glyphs().rule.repeat(Math.max(0, inner)) });
408
+ }
409
+ function SummaryRow({ label, s }) {
410
+ const cachedPct = s.tokens > 0 ? Math.round(s.cacheRead / s.tokens * 100) : 0;
411
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
412
+ /* @__PURE__ */ jsx2(Box2, { width: 11, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: label }) }),
413
+ /* @__PURE__ */ jsx2(Box2, { width: 11, flexShrink: 0, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", wrap: "truncate", children: currency(s.cost) }) }),
414
+ /* @__PURE__ */ jsx2(Box2, { width: 13, flexShrink: 0, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate", children: [
415
+ tokens(s.tokens),
416
+ " tok"
417
+ ] }) }),
418
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: cachedPct > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate", children: [
419
+ cachedPct,
420
+ "% cached"
421
+ ] }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
422
+ ] });
423
+ }
424
+ function KpiLine({ agg }) {
425
+ const hasBurn = agg.burnRate > 0;
426
+ const hasSaved = agg.month.cacheSavings > 0;
427
+ if (!hasBurn && !hasSaved) return null;
428
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
429
+ hasBurn && /* @__PURE__ */ jsxs2(Fragment, { children: [
430
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Burn " }),
431
+ /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
432
+ currency(agg.burnRate),
433
+ "/hr"
434
+ ] })
435
+ ] }),
436
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
437
+ hasSaved && /* @__PURE__ */ jsxs2(Fragment, { children: [
438
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Cache saved " }),
439
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
440
+ currency(agg.month.cacheSavings),
441
+ "/mo"
442
+ ] })
443
+ ] })
444
+ ] });
445
+ }
446
+ function LimitsBlock({ items, barW }) {
447
+ const showName = items.length > 1;
448
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
449
+ const billing = s?.billing;
450
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
451
+ showName && /* @__PURE__ */ jsxs2(Box2, { children: [
452
+ /* @__PURE__ */ jsxs2(Text2, { color: account.color, children: [
453
+ glyphs().dot,
454
+ " "
455
+ ] }),
456
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.name, 22) })
457
+ ] }),
458
+ !billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
459
+ "Fetching",
460
+ glyphs().ellipsis
461
+ ] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW }, `${m.label}${i}`))
462
+ ] }, account.id);
463
+ }) });
464
+ }
465
+ function MetricRow({ m, color, barW }) {
466
+ if (m.format.kind === "percent") {
467
+ const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
468
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
469
+ /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
470
+ /* @__PURE__ */ jsx2(Bar, { pct: m.used, color: barColor, width: barW }),
471
+ /* @__PURE__ */ jsx2(Box2, { width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
472
+ Math.round(m.used),
473
+ "%"
474
+ ] }) }),
475
+ /* @__PURE__ */ jsx2(Box2, { width: 8, justifyContent: "flex-end", children: m.resetsAt ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: m.resetsAt }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
476
+ ] });
477
+ }
478
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
479
+ /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
480
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) })
481
+ ] });
482
+ }
483
+ function SparkFooter({ series, month, color }) {
484
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
485
+ /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
486
+ /* @__PURE__ */ jsx2(Text2, { color, children: sparkline(series) }),
487
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
488
+ currency(month),
489
+ " mo"
490
+ ] }) })
491
+ ] });
492
+ }
493
+ function aggregate(list) {
494
+ const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
495
+ const z = { today: zero(), week: zero(), month: zero(), burnRate: 0, series: [] };
496
+ for (const d of list) {
497
+ for (const k of ["today", "week", "month"]) {
498
+ z[k].cost += d[k].cost;
499
+ z[k].tokens += d[k].tokens;
500
+ z[k].cacheRead += d[k].cacheRead;
501
+ z[k].cacheSavings += d[k].cacheSavings;
502
+ }
503
+ z.burnRate += d.burnRate;
504
+ d.series.forEach((v, i) => {
505
+ z.series[i] = (z.series[i] ?? 0) + v;
506
+ });
507
+ }
508
+ return z;
509
+ }
510
+ var TotalsRow = memo2(function TotalsRow2({ groups, stats, cols }) {
511
+ const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
512
+ const t = zero(), w = zero(), m = zero();
513
+ for (const g of groups) {
514
+ if (!PROVIDERS[g.provider].hasUsage) continue;
515
+ for (const a of g.accounts) {
516
+ const d = stats.get(a.id)?.dashboard;
517
+ if (!d) continue;
518
+ t.cost += d.today.cost;
519
+ t.tokens += d.today.tokens;
520
+ w.cost += d.week.cost;
521
+ w.tokens += d.week.tokens;
522
+ m.cost += d.month.cost;
523
+ m.tokens += d.month.tokens;
524
+ }
525
+ }
526
+ const inner = cols - 4;
527
+ const dot = glyphs().middot;
528
+ const full = `${glyphs().dotAll} Today ${currency(t.cost)} (${tokens(t.tokens)} tok) ${dot} Week ${currency(w.cost)} (${tokens(w.tokens)} tok) ${dot} Month ${currency(m.cost)} (${tokens(m.tokens)} tok)`;
529
+ const noTok = `${glyphs().dotAll} Today ${currency(t.cost)} ${dot} Week ${currency(w.cost)} ${dot} Month ${currency(m.cost)}`;
530
+ const tight = `${glyphs().dotAll} ${currency(t.cost)} ${dot} ${currency(w.cost)} ${dot} ${currency(m.cost)}`;
531
+ const text = full.length <= inner ? full : noTok.length <= inner ? noTok : tight;
532
+ return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: text }) });
533
+ });
534
+
535
+ // src/ui/table.tsx
536
+ import { memo as memo4 } from "react";
537
+ import { Box as Box4, Text as Text4 } from "ink";
538
+
539
+ // src/ui/settings.tsx
540
+ import { memo as memo3 } from "react";
541
+ import { Box as Box3, Text as Text3 } from "ink";
542
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
543
+ var GENERAL_ROWS = 6;
544
+ var SETTINGS_TABS = ["general", "providers", "accounts"];
545
+ var SETTINGS_TAB_LABELS = {
546
+ general: "General",
547
+ providers: "Providers",
548
+ accounts: "Accounts"
549
+ };
550
+ var FORM_FIELDS = ["provider", "name", "homeDir", "color"];
551
+ var SettingsView = memo3(function SettingsView2({
552
+ config,
553
+ cursor,
554
+ activeTab,
555
+ tzEdit,
556
+ tzCaret,
557
+ tzError,
558
+ resolvedTz,
559
+ accountForm,
560
+ activeAccountId,
561
+ trackedAccounts,
562
+ accountIdentities
563
+ }) {
564
+ if (accountForm) return /* @__PURE__ */ jsx3(AccountFormView, { form: accountForm, accounts: config.accounts });
565
+ const editingTz = tzEdit !== null;
566
+ const tzDisplay = config.timezone === null ? `System (${resolvedTz})` : config.timezone;
567
+ const tabFocused = cursor < 0;
568
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
569
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Settings" }),
570
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: configLocation() }),
571
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
572
+ /* @__PURE__ */ jsx3(SettingsTabBar, { active: activeTab, focused: tabFocused }),
573
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
574
+ activeTab === "general" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
575
+ /* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "General" }),
576
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 0, label: "Refresh interval", children: [
577
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
578
+ glyphs().caretL,
579
+ " "
580
+ ] }),
581
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "yellow", children: [
582
+ config.interval,
583
+ "s"
584
+ ] }),
585
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
586
+ " ",
587
+ glyphs().caretR
588
+ ] })
589
+ ] }),
590
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 1, label: "Billing poll", children: [
591
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
592
+ glyphs().caretL,
593
+ " "
594
+ ] }),
595
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "yellow", children: [
596
+ config.billingInterval,
597
+ "m"
598
+ ] }),
599
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
600
+ " ",
601
+ glyphs().caretR
602
+ ] })
603
+ ] }),
604
+ /* @__PURE__ */ jsx3(Row, { cursor, idx: 2, label: "Clear screen", children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: config.clearScreen ? "green" : "red", children: config.clearScreen ? "on" : "off" }) }),
605
+ /* @__PURE__ */ jsx3(Row, { cursor, idx: 3, label: "Timezone", children: editingTz ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
606
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[" }),
607
+ /* @__PURE__ */ jsx3(CaretText, { value: tzEdit ?? "", caret: tzCaret, color: "cyan" }),
608
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "]" })
609
+ ] }) : /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: tzDisplay }) }),
610
+ cursor === 3 && tzError && /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
611
+ " ",
612
+ tzError
613
+ ] }),
614
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 4, label: "Dashboard", children: [
615
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
616
+ glyphs().caretL,
617
+ " "
618
+ ] }),
619
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: config.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
620
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
621
+ " ",
622
+ glyphs().caretR
623
+ ] })
624
+ ] }),
625
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 5, label: "Default focus", children: [
626
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
627
+ glyphs().caretL,
628
+ " "
629
+ ] }),
630
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: config.defaultFocus === "all" ? "All" : "Last account" }),
631
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
632
+ " ",
633
+ glyphs().caretR
634
+ ] })
635
+ ] })
636
+ ] }),
637
+ activeTab === "providers" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
638
+ /* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Providers" }),
639
+ PROVIDER_ORDER.map((pid, i) => {
640
+ const selected = cursor === i;
641
+ const enabled = !config.disabledProviders.includes(pid);
642
+ const p = PROVIDERS[pid];
643
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
644
+ /* @__PURE__ */ jsxs3(Text3, { color: selected ? "green" : void 0, children: [
645
+ selected ? glyphs().caretR : " ",
646
+ " "
647
+ ] }),
648
+ /* @__PURE__ */ jsx3(Text3, { bold: enabled, color: enabled ? p.color : void 0, dimColor: !enabled, children: enabled ? `[${glyphs().check}]` : "[ ]" }),
649
+ /* @__PURE__ */ jsxs3(Text3, { color: p.color, children: [
650
+ " ",
651
+ glyphs().dot,
652
+ " "
653
+ ] }),
654
+ /* @__PURE__ */ jsx3(Box3, { width: 9, children: /* @__PURE__ */ jsx3(Text3, { bold: selected, children: p.name }) }),
655
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: enabled ? "tracking" : "off" })
656
+ ] }, pid);
657
+ })
658
+ ] }),
659
+ activeTab === "accounts" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
660
+ /* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Accounts" }),
661
+ trackedAccounts.length === 0 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
662
+ " none tracked ",
663
+ glyphs().emDash,
664
+ " enable a provider or add an account"
665
+ ] }),
666
+ trackedAccounts.map((acc, i) => {
667
+ const selected = cursor === i;
668
+ const isActive = acc.id === activeAccountId;
669
+ const provider = PROVIDERS[acc.providerId];
670
+ const identity = accountIdentities.get(acc.id);
671
+ const identityLabel = identity?.email || identity?.displayName || acc.name;
672
+ const plan = identity?.plan ?? null;
673
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
674
+ /* @__PURE__ */ jsxs3(Text3, { color: selected ? "green" : void 0, children: [
675
+ selected ? glyphs().caretR : " ",
676
+ " "
677
+ ] }),
678
+ /* @__PURE__ */ jsxs3(Text3, { color: acc.color || provider.color, children: [
679
+ isActive ? glyphs().dot : glyphs().radioOff,
680
+ " "
681
+ ] }),
682
+ /* @__PURE__ */ jsx3(Box3, { width: 28, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: truncateName(identityLabel, 27) }) }),
683
+ /* @__PURE__ */ jsx3(Box3, { width: 9, children: /* @__PURE__ */ jsx3(Text3, { color: provider.color, children: provider.name }) }),
684
+ /* @__PURE__ */ jsx3(Box3, { width: 18, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: plan ? truncateName(plan, 17) : "" }) }),
685
+ /* @__PURE__ */ jsx3(Box3, { width: 12, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: acc.source === "auto" ? "auto tracking" : "configured" }) }),
686
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: truncateName(acc.homeDir, 24) })
687
+ ] }, `${acc.source}:${acc.id}`);
688
+ }),
689
+ /* @__PURE__ */ jsxs3(Box3, { children: [
690
+ /* @__PURE__ */ jsxs3(Text3, { color: cursor === trackedAccounts.length ? "green" : void 0, children: [
691
+ cursor === trackedAccounts.length ? glyphs().caretR : " ",
692
+ " "
693
+ ] }),
694
+ /* @__PURE__ */ jsx3(Text3, { color: "greenBright", children: "+ " }),
695
+ /* @__PURE__ */ jsx3(Text3, { children: "Add account" })
696
+ ] })
697
+ ] }),
698
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
699
+ tabFocused ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
700
+ glyphs().arrowL,
701
+ glyphs().arrowR,
702
+ "/tab switch section ",
703
+ glyphs().middot,
704
+ " ",
705
+ glyphs().arrowD,
706
+ " rows ",
707
+ glyphs().middot,
708
+ " s/Esc close"
709
+ ] }) : editingTz ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
710
+ "type IANA name (e.g. Europe/London) ",
711
+ glyphs().middot,
712
+ " empty = System ",
713
+ glyphs().middot,
714
+ " Enter save ",
715
+ glyphs().middot,
716
+ " Esc cancel"
717
+ ] }) : activeTab === "providers" ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
718
+ glyphs().arrowU,
719
+ glyphs().arrowD,
720
+ " select ",
721
+ glyphs().middot,
722
+ " space toggle provider ",
723
+ glyphs().middot,
724
+ " s/Esc close"
725
+ ] }) : activeTab === "accounts" && cursor >= 0 && cursor < trackedAccounts.length ? trackedAccounts[cursor]?.source === "auto" ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
726
+ glyphs().arrowU,
727
+ glyphs().arrowD,
728
+ " select ",
729
+ glyphs().middot,
730
+ " Enter configure ",
731
+ glyphs().middot,
732
+ " space activate ",
733
+ glyphs().middot,
734
+ " s/Esc close"
735
+ ] }) : /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
736
+ glyphs().arrowU,
737
+ glyphs().arrowD,
738
+ " select ",
739
+ glyphs().middot,
740
+ " ",
741
+ glyphs().shift,
742
+ glyphs().arrowU,
743
+ glyphs().arrowD,
744
+ " reorder ",
745
+ glyphs().middot,
746
+ " Enter edit ",
747
+ glyphs().middot,
748
+ " space activate ",
749
+ glyphs().middot,
750
+ " d delete ",
751
+ glyphs().middot,
752
+ " s/Esc close"
753
+ ] }) : activeTab === "accounts" && cursor === trackedAccounts.length ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
754
+ glyphs().arrowU,
755
+ glyphs().arrowD,
756
+ " select ",
757
+ glyphs().middot,
758
+ " Enter add account ",
759
+ glyphs().middot,
760
+ " s/Esc close"
761
+ ] }) : /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
762
+ glyphs().arrowU,
763
+ glyphs().arrowD,
764
+ " select ",
765
+ glyphs().arrowL,
766
+ glyphs().arrowR,
767
+ " adjust Enter edit tab switch section s/Esc close"
768
+ ] })
769
+ ] });
770
+ });
771
+ function SettingsTabBar({ active, focused }) {
772
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
773
+ /* @__PURE__ */ jsxs3(Text3, { color: focused ? "green" : void 0, children: [
774
+ focused ? glyphs().caretR : " ",
775
+ " "
776
+ ] }),
777
+ SETTINGS_TABS.map((tab, i) => {
778
+ const selected = tab === active;
779
+ return /* @__PURE__ */ jsxs3(Box3, { marginRight: 1, children: [
780
+ selected ? /* @__PURE__ */ jsxs3(Text3, { bold: true, inverse: true, children: [
781
+ " ",
782
+ SETTINGS_TAB_LABELS[tab],
783
+ " "
784
+ ] }) : /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
785
+ " ",
786
+ SETTINGS_TAB_LABELS[tab],
787
+ " "
788
+ ] }),
789
+ i < SETTINGS_TABS.length - 1 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " })
790
+ ] }, tab);
791
+ })
792
+ ] });
793
+ }
794
+ function CaretText({ value, caret, color }) {
795
+ const c = Math.max(0, Math.min(caret, value.length));
796
+ if (c >= value.length) {
797
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
798
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color, children: value }),
799
+ /* @__PURE__ */ jsx3(Text3, { color, children: glyphs().vbar })
800
+ ] });
801
+ }
802
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
803
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color, children: value.slice(0, c) }),
804
+ /* @__PURE__ */ jsx3(Text3, { inverse: true, color, children: value[c] }),
805
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color, children: value.slice(c + 1) })
806
+ ] });
807
+ }
808
+ function Row({ cursor, idx, label, children }) {
809
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
810
+ /* @__PURE__ */ jsxs3(Text3, { color: cursor === idx ? "green" : void 0, children: [
811
+ cursor === idx ? glyphs().caretR : " ",
812
+ " "
813
+ ] }),
814
+ /* @__PURE__ */ jsx3(Box3, { width: 20, children: /* @__PURE__ */ jsx3(Text3, { children: label }) }),
815
+ children
816
+ ] });
817
+ }
818
+ function AccountFormView({ form, accounts }) {
819
+ const previewId = form.mode === "add" ? generateAccountId(form.name || "account", accounts) : form.editingId ?? "";
820
+ const accent = form.color;
821
+ const stepIndex = { provider: 1, name: 2, homeDir: 3, color: 4 };
822
+ const step = stepIndex[form.field];
823
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
824
+ /* @__PURE__ */ jsxs3(Box3, { children: [
825
+ /* @__PURE__ */ jsx3(Text3, { color: accent, bold: true, children: glyphs().vbar }),
826
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
827
+ " ",
828
+ form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
829
+ ] }),
830
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
831
+ " step ",
832
+ step,
833
+ " of 4"
834
+ ] })
835
+ ] }),
836
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Stepper, { active: form.field, accent }) }),
837
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", borderStyle: glyphs().border, borderColor: accent, paddingX: 2, paddingY: 1, children: [
838
+ /* @__PURE__ */ jsx3(ProviderField, { value: form.providerId, focused: form.field === "provider" }),
839
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
840
+ /* @__PURE__ */ jsx3(
841
+ FormField,
842
+ {
843
+ label: "Name",
844
+ hint: "display name for this account",
845
+ value: form.name,
846
+ focused: form.field === "name",
847
+ caret: form.caret,
848
+ accent,
849
+ placeholder: "e.g. Work, Personal"
850
+ }
851
+ ),
852
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
853
+ /* @__PURE__ */ jsx3(
854
+ FormField,
855
+ {
856
+ label: "Home directory",
857
+ hint: `path containing the tool's data dir ${glyphs().middot} ~ for default`,
858
+ value: form.homeDir,
859
+ focused: form.field === "homeDir",
860
+ caret: form.caret,
861
+ accent,
862
+ placeholder: "~/work",
863
+ mono: true
864
+ }
865
+ ),
866
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
867
+ /* @__PURE__ */ jsx3(ColorField, { value: form.color, focused: form.field === "color" }),
868
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
869
+ /* @__PURE__ */ jsxs3(Box3, { children: [
870
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
871
+ "id ",
872
+ glyphs().boxMark,
873
+ " "
874
+ ] }),
875
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: accent, children: previewId || "account" }),
876
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
877
+ " ",
878
+ glyphs().boxMark,
879
+ " auto-generated from name"
880
+ ] })
881
+ ] })
882
+ ] }),
883
+ form.error && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
884
+ glyphs().warn,
885
+ " ",
886
+ form.error
887
+ ] }) }),
888
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
889
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
890
+ "tab/",
891
+ glyphs().arrowU,
892
+ glyphs().arrowD,
893
+ " "
894
+ ] }),
895
+ /* @__PURE__ */ jsx3(Text3, { children: "switch field" }),
896
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
897
+ " ",
898
+ glyphs().middot,
899
+ " "
900
+ ] }),
901
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "enter " }),
902
+ /* @__PURE__ */ jsx3(Text3, { children: form.field === "color" ? "save" : "next" }),
903
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
904
+ " ",
905
+ glyphs().middot,
906
+ " "
907
+ ] }),
908
+ form.field === "color" || form.field === "provider" ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
909
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
910
+ glyphs().arrowL,
911
+ glyphs().arrowR,
912
+ " "
913
+ ] }),
914
+ /* @__PURE__ */ jsx3(Text3, { children: form.field === "provider" ? "pick provider" : "pick color" }),
915
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
916
+ " ",
917
+ glyphs().middot,
918
+ " "
919
+ ] })
920
+ ] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
921
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
922
+ glyphs().arrowL,
923
+ glyphs().arrowR,
924
+ " "
925
+ ] }),
926
+ /* @__PURE__ */ jsx3(Text3, { children: "move caret" }),
927
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
928
+ " ",
929
+ glyphs().middot,
930
+ " "
931
+ ] })
932
+ ] }),
933
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "ctrl+s " }),
934
+ /* @__PURE__ */ jsx3(Text3, { children: "save" }),
935
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
936
+ " ",
937
+ glyphs().middot,
938
+ " "
939
+ ] }),
940
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "esc " }),
941
+ /* @__PURE__ */ jsx3(Text3, { children: "cancel" })
942
+ ] })
943
+ ] });
944
+ }
945
+ function Stepper({ active, accent }) {
946
+ const steps = [
947
+ { id: "provider", label: "Provider" },
948
+ { id: "name", label: "Name" },
949
+ { id: "homeDir", label: "Home" },
950
+ { id: "color", label: "Color" }
951
+ ];
952
+ const activeIdx = steps.findIndex((s) => s.id === active);
953
+ return /* @__PURE__ */ jsx3(Box3, { children: steps.map((s, i) => {
954
+ const done = i < activeIdx;
955
+ const cur = i === activeIdx;
956
+ const dot = done ? glyphs().dot : cur ? glyphs().dotSel : glyphs().radioOff;
957
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
958
+ /* @__PURE__ */ jsxs3(Text3, { color: cur || done ? accent : void 0, dimColor: !cur && !done, children: [
959
+ dot,
960
+ " "
961
+ ] }),
962
+ /* @__PURE__ */ jsx3(Text3, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
963
+ i < steps.length - 1 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
964
+ " ",
965
+ glyphs().rule,
966
+ " "
967
+ ] })
968
+ ] }, s.id);
969
+ }) });
970
+ }
971
+ function ProviderField({ value, focused }) {
972
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
973
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: focused ? PROVIDERS[value].color : void 0, bold: focused, dimColor: !focused, children: [
974
+ focused ? glyphs().caretR : " ",
975
+ " Provider"
976
+ ] }) }),
977
+ /* @__PURE__ */ jsxs3(Box3, { children: [
978
+ /* @__PURE__ */ jsxs3(Text3, { children: [
979
+ " ",
980
+ focused ? glyphs().vbar : " ",
981
+ " "
982
+ ] }),
983
+ PROVIDER_ORDER.map((pid) => {
984
+ const selected = pid === value;
985
+ const p = PROVIDERS[pid];
986
+ return /* @__PURE__ */ jsx3(Box3, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: p.color, children: [
987
+ "[",
988
+ p.name,
989
+ "]"
990
+ ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: p.name }) }, pid);
991
+ })
992
+ ] }),
993
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " which tool this account tracks" }) })
994
+ ] });
995
+ }
996
+ function FormField({ label, hint, value, focused, caret, accent, placeholder, mono }) {
997
+ const isPlaceholder = value === "";
998
+ const display = isPlaceholder ? placeholder : value;
999
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1000
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
1001
+ focused ? glyphs().caretR : " ",
1002
+ " ",
1003
+ label
1004
+ ] }) }),
1005
+ /* @__PURE__ */ jsxs3(Box3, { children: [
1006
+ /* @__PURE__ */ jsxs3(Text3, { color: focused ? accent : void 0, children: [
1007
+ " ",
1008
+ focused ? glyphs().vbar : " ",
1009
+ " "
1010
+ ] }),
1011
+ focused ? isPlaceholder ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1012
+ /* @__PURE__ */ jsx3(Text3, { color: accent, children: glyphs().vbar }),
1013
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, italic: mono, children: placeholder })
1014
+ ] }) : /* @__PURE__ */ jsx3(CaretText, { value, caret: caret ?? value.length, color: accent }) : /* @__PURE__ */ jsx3(Text3, { dimColor: isPlaceholder, italic: mono && isPlaceholder, children: display })
1015
+ ] }),
1016
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1017
+ " ",
1018
+ hint
1019
+ ] }) })
1020
+ ] });
1021
+ }
1022
+ function ColorField({ value, focused }) {
1023
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1024
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
1025
+ focused ? glyphs().caretR : " ",
1026
+ " Accent color"
1027
+ ] }) }),
1028
+ /* @__PURE__ */ jsxs3(Box3, { children: [
1029
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1030
+ " ",
1031
+ focused ? glyphs().vbar : " ",
1032
+ " "
1033
+ ] }),
1034
+ COLOR_PALETTE.map((c) => /* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: c === value ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: c, children: [
1035
+ "[",
1036
+ glyphs().dot,
1037
+ "]"
1038
+ ] }) : /* @__PURE__ */ jsxs3(Text3, { color: c, dimColor: !focused, children: [
1039
+ " ",
1040
+ glyphs().dot
1041
+ ] }) }, c))
1042
+ ] }),
1043
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
1044
+ ] });
1045
+ }
1046
+
1047
+ // src/ui/table.tsx
1048
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1049
+ var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1050
+ var TableProviderBar = memo4(function TableProviderBar2({ providers, active, onSelect }) {
1051
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
1052
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "provider " }),
1053
+ providers.map((p) => {
1054
+ const meta = PROVIDERS[p];
1055
+ return /* @__PURE__ */ jsx4(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p === active ? /* @__PURE__ */ jsxs4(Text4, { bold: true, color: meta.color, inverse: true, children: [
1056
+ " ",
1057
+ meta.name,
1058
+ " "
1059
+ ] }) : /* @__PURE__ */ jsxs4(Text4, { color: meta.color, dimColor: true, children: [
1060
+ " ",
1061
+ meta.name,
1062
+ " "
1063
+ ] }) }, p);
1064
+ }),
1065
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " p/P switch" })
1066
+ ] });
1067
+ });
1068
+ var ControlBar = memo4(function ControlBar2({ views, period, sort, search, searchCaret, searching, showPeriod }) {
1069
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
1070
+ showPeriod && /* @__PURE__ */ jsxs4(Fragment3, { children: [
1071
+ views.map((v, i) => /* @__PURE__ */ jsx4(Box4, { marginRight: 2, children: i === period ? /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
1072
+ "[",
1073
+ v,
1074
+ "]"
1075
+ ] }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: v }) }, v)),
1076
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " " })
1077
+ ] }),
1078
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "sort " }),
1079
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "magenta", children: sort }),
1080
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1081
+ " o cycle ",
1082
+ glyphs().middot,
1083
+ " "
1084
+ ] }),
1085
+ searching ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1086
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "/" }),
1087
+ /* @__PURE__ */ jsx4(CaretText, { value: search, caret: searchCaret, color: "cyan" })
1088
+ ] }) : search ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1089
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "filter " }),
1090
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "green", children: search }),
1091
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1092
+ " (/ edit ",
1093
+ glyphs().middot,
1094
+ " esc clear)"
1095
+ ] })
1096
+ ] }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "/ filter" })
1097
+ ] });
1098
+ });
1099
+ var TokenTable = memo4(function TokenTable2({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1100
+ if (rows.length === 0) return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No usage in this period (or filtered out)." });
1101
+ const wide = cols > 90;
1102
+ const base = wide ? { label: 12, input: 10, output: 10, cc: 14, cr: 12, total: 11, cost: 13 } : { label: 8, input: 7, output: 7, cc: 7, cr: 8, total: 0, cost: 11 };
1103
+ const fixed = base.label + base.input + base.output + base.cc + base.cr + base.total + base.cost;
1104
+ const available = cols - fixed - 6;
1105
+ const W = { ...base, models: Math.max(wide ? 22 : 14, available) };
1106
+ const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
1107
+ const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
1108
+ for (const r of rows) {
1109
+ totals.input += r.input;
1110
+ totals.output += r.output;
1111
+ totals.cacheCreate += r.cacheCreate;
1112
+ totals.cacheRead += r.cacheRead;
1113
+ totals.cost += r.cost;
1114
+ }
1115
+ const clampedCursor = Math.min(cursor, rows.length - 1);
1116
+ const scrollStart = Math.max(0, Math.min(clampedCursor - Math.floor(maxRows / 2), rows.length - maxRows));
1117
+ const visible = rows.slice(scrollStart, scrollStart + maxRows);
1118
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1119
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1120
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
1121
+ " ",
1122
+ col("Date", W.label, "left")
1123
+ ] }),
1124
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Models", W.models, "left") }),
1125
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Input", W.input) }),
1126
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Output", W.output) }),
1127
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col(wide ? "Cache Create" : "CchCrt", W.cc) }),
1128
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col(wide ? "Cache Read" : "CchRd", W.cr) }),
1129
+ W.total > 0 && /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Total", W.total) }),
1130
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Cost", W.cost) })
1131
+ ] }),
1132
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
1133
+ visible.map((r, vi) => {
1134
+ const idx = scrollStart + vi;
1135
+ const selected = idx === clampedCursor;
1136
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1137
+ /* @__PURE__ */ jsx4(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs4(Text4, { inverse: selected, children: [
1138
+ /* @__PURE__ */ jsxs4(Text4, { color: selected ? void 0 : "cyan", children: [
1139
+ selected ? `${glyphs().caretR} ` : " ",
1140
+ col(fmtLabel(r.label), W.label, "left")
1141
+ ] }),
1142
+ /* @__PURE__ */ jsx4(Text4, { dimColor: !selected, children: col(r.models.join(", "), W.models, "left") }),
1143
+ /* @__PURE__ */ jsx4(Text4, { children: col(tokens(r.input), W.input) }),
1144
+ /* @__PURE__ */ jsx4(Text4, { children: col(tokens(r.output), W.output) }),
1145
+ /* @__PURE__ */ jsx4(Text4, { children: col(tokens(r.cacheCreate), W.cc) }),
1146
+ /* @__PURE__ */ jsx4(Text4, { children: col(tokens(r.cacheRead), W.cr) }),
1147
+ W.total > 0 && /* @__PURE__ */ jsx4(Text4, { children: col(tokens(r.total), W.total) }),
1148
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.cost), W.cost) })
1149
+ ] }) }),
1150
+ idx === expanded && /* @__PURE__ */ jsx4(RowDetail, { row: r, indent: W.label + 2 })
1151
+ ] }, r.label);
1152
+ }),
1153
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
1154
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1155
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "greenBright", children: [
1156
+ " ",
1157
+ col("Total", W.label, "left")
1158
+ ] }),
1159
+ /* @__PURE__ */ jsx4(Text4, { children: col("", W.models, "left") }),
1160
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
1161
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
1162
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
1163
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
1164
+ W.total > 0 && /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: col(tokens(totals.input + totals.output + totals.cacheCreate + totals.cacheRead), W.total) }),
1165
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
1166
+ ] }),
1167
+ /* @__PURE__ */ jsx4(Box4, { height: 1 }),
1168
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1169
+ glyphs().arrowU,
1170
+ glyphs().arrowD,
1171
+ " navigate ",
1172
+ glyphs().middot,
1173
+ " Enter detail ",
1174
+ glyphs().middot,
1175
+ " o sort ",
1176
+ glyphs().middot,
1177
+ " g/G top/bottom ",
1178
+ glyphs().middot,
1179
+ " ",
1180
+ clampedCursor + 1,
1181
+ "/",
1182
+ rows.length
1183
+ ] })
1184
+ ] });
1185
+ });
1186
+ function RowDetail({ row, indent }) {
1187
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingLeft: indent, children: row.breakdown.map((m, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
1188
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1189
+ i === row.breakdown.length - 1 ? glyphs().treeEnd : glyphs().treeMid,
1190
+ " "
1191
+ ] }),
1192
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col(m.name, 16, "left") }),
1193
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1194
+ col(tokens(m.input), 8),
1195
+ " in "
1196
+ ] }),
1197
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1198
+ col(tokens(m.output), 8),
1199
+ " out "
1200
+ ] }),
1201
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1202
+ col(tokens(m.cacheCreate), 8),
1203
+ " cc "
1204
+ ] }),
1205
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1206
+ col(tokens(m.cacheRead), 9),
1207
+ " cr "
1208
+ ] }),
1209
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: currency(m.cost) })
1210
+ ] }, m.name)) });
1211
+ }
1212
+ var CursorSpendTable = memo4(function CursorSpendTable2({ rows, cursor, maxRows, onRowClick }) {
1213
+ if (rows.length === 0) return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No Cursor spend recorded locally." });
1214
+ const total = rows.reduce((a, r) => a + r.usd, 0);
1215
+ const totalAmt = rows.reduce((a, r) => a + r.requests, 0);
1216
+ const clamped = Math.min(cursor, rows.length - 1);
1217
+ const scrollStart = Math.max(0, Math.min(clamped - Math.floor(maxRows / 2), rows.length - maxRows));
1218
+ const visible = rows.slice(scrollStart, scrollStart + maxRows);
1219
+ const W = { model: 34, cost: 12, amount: 12, share: 8 };
1220
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1221
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1222
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
1223
+ " ",
1224
+ col("Model", W.model, "left")
1225
+ ] }),
1226
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Cost", W.cost) }),
1227
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Amount", W.amount) }),
1228
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Share", W.share) })
1229
+ ] }),
1230
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1231
+ visible.map((r, vi) => {
1232
+ const idx = scrollStart + vi;
1233
+ const selected = idx === clamped;
1234
+ const share = total > 0 ? r.usd / total * 100 : 0;
1235
+ return /* @__PURE__ */ jsx4(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs4(Text4, { inverse: selected, children: [
1236
+ /* @__PURE__ */ jsxs4(Text4, { color: selected ? void 0 : "magenta", children: [
1237
+ selected ? `${glyphs().caretR} ` : " ",
1238
+ col(r.name, W.model, "left")
1239
+ ] }),
1240
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.usd), W.cost) }),
1241
+ /* @__PURE__ */ jsx4(Text4, { children: col(tokens(r.requests), W.amount) }),
1242
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: col(share.toFixed(1) + "%", W.share) })
1243
+ ] }) }, r.name);
1244
+ }),
1245
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1246
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1247
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "greenBright", children: [
1248
+ " ",
1249
+ col("Total", W.model, "left")
1250
+ ] }),
1251
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellowBright", children: col(currency(total), W.cost) }),
1252
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: col(tokens(totalAmt), W.amount) }),
1253
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: col("100%", W.share) })
1254
+ ] }),
1255
+ /* @__PURE__ */ jsx4(Box4, { height: 1 }),
1256
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1257
+ "local spend by model (composerData) ",
1258
+ glyphs().middot,
1259
+ " est. API-equivalent ",
1260
+ glyphs().middot,
1261
+ " ",
1262
+ clamped + 1,
1263
+ "/",
1264
+ rows.length
1265
+ ] })
1266
+ ] });
1267
+ });
1268
+ function fmtLabel(label) {
1269
+ if (label.length === 7 && label[4] === "-") {
1270
+ return `${MONTHS[Number(label.slice(5, 7))]} '${label.slice(2, 4)}`;
1271
+ }
1272
+ return shortDate(label);
1273
+ }
1274
+
1275
+ // src/ui/onboarding.tsx
1276
+ import { Box as Box5, Text as Text5 } from "ink";
1277
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1278
+ function Onboarding({ items, cursor, onToggle, onConfirm, heading = "Welcome to tokmon", subheading = "Pick the tools you want to track. You can change this anytime in settings." }) {
1279
+ const anyEnabled = items.some((it) => it.enabled);
1280
+ const startIdx = items.length;
1281
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
1282
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: "greenBright", children: heading }) }),
1283
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: subheading }),
1284
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
1285
+ items.map((it, i) => {
1286
+ const selected = cursor === i;
1287
+ const box = it.enabled ? `[${glyphs().check}]` : "[ ]";
1288
+ return /* @__PURE__ */ jsxs5(ClickableBox, { onClick: () => onToggle(i), children: [
1289
+ /* @__PURE__ */ jsxs5(Text5, { color: selected ? "green" : void 0, children: [
1290
+ selected ? glyphs().caretR : " ",
1291
+ " "
1292
+ ] }),
1293
+ /* @__PURE__ */ jsx5(Text5, { bold: it.enabled, color: it.enabled ? it.color : void 0, dimColor: !it.enabled, children: box }),
1294
+ /* @__PURE__ */ jsxs5(Text5, { color: it.color, children: [
1295
+ " ",
1296
+ glyphs().dot,
1297
+ " "
1298
+ ] }),
1299
+ /* @__PURE__ */ jsx5(Box5, { width: 13, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { bold: selected, dimColor: !it.detected && !it.enabled, wrap: "truncate", children: it.name }) }),
1300
+ it.detected ? /* @__PURE__ */ jsx5(Text5, { color: "green", dimColor: true, children: "installed" }) : it.enabled ? /* @__PURE__ */ jsx5(Text5, { color: "yellow", dimColor: true, children: "manual" }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "not found" })
1301
+ ] }, it.id);
1302
+ }),
1303
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
1304
+ /* @__PURE__ */ jsxs5(ClickableBox, { onClick: onConfirm, children: [
1305
+ /* @__PURE__ */ jsxs5(Text5, { color: cursor === startIdx ? "green" : void 0, children: [
1306
+ cursor === startIdx ? glyphs().caretR : " ",
1307
+ " "
1308
+ ] }),
1309
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? `${glyphs().play} Start tokmon` : `${glyphs().play} Start (nothing selected)` })
1310
+ ] }),
1311
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
1312
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1313
+ glyphs().arrowU,
1314
+ glyphs().arrowD,
1315
+ " move ",
1316
+ glyphs().middot,
1317
+ " space toggle ",
1318
+ glyphs().middot,
1319
+ " enter start ",
1320
+ glyphs().middot,
1321
+ " q quit"
1322
+ ] })
1323
+ ] });
1324
+ }
1325
+
1326
+ // src/ui/loading.tsx
1327
+ import { useState as useState2, useEffect as useEffect2 } from "react";
1328
+ import { Box as Box6, Text as Text6 } from "ink";
1329
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1330
+ function accountReady(s, providerId) {
1331
+ if (!s) return false;
1332
+ const p = PROVIDERS[providerId];
1333
+ if (p.hasBilling && (s.billingState === "error" || s.billing?.error)) return true;
1334
+ if (p.hasUsage && s.summaryState !== "ready" && s.summaryState !== "error") return false;
1335
+ if (p.hasBilling && s.billingState !== "ready" && s.billingState !== "error") return false;
1336
+ return true;
1337
+ }
1338
+ function statsReadyInput(s) {
1339
+ if (!s) return void 0;
1340
+ return {
1341
+ summaryState: s.dashboard ? "ready" : "pending",
1342
+ billingState: s.billing?.error ? "error" : s.billing ? "ready" : "pending",
1343
+ billing: s.billing
1344
+ };
1345
+ }
1346
+ function groupTodayCost(items) {
1347
+ return items.reduce((sum, s) => {
1348
+ const d = s?.dashboard;
1349
+ return sum + (d?.today.cost ?? 0);
1350
+ }, 0);
1351
+ }
1352
+ function headlineFor(group, items) {
1353
+ const meta = PROVIDERS[group.provider];
1354
+ if (meta.hasUsage) return `${currency(groupTodayCost(items))} today`;
1355
+ const billing = items.map((s) => s?.billing).find(Boolean);
1356
+ if (!billing) return "no data";
1357
+ if (billing.error) return billing.error;
1358
+ const m = billing.metrics[0];
1359
+ if (m) return metricValueText(m);
1360
+ return billing.plan ?? "no data";
1361
+ }
1362
+ var STAGGER_FRAMES = 2;
1363
+ function LoadingView({ groups, stats, cols, rows, readyInput }) {
1364
+ const resolveReady = readyInput ?? ((id) => statsReadyInput(stats.get(id)));
1365
+ const sp = glyphs().spinner;
1366
+ const [frame, setFrame] = useState2(0);
1367
+ useEffect2(() => {
1368
+ const id = setInterval(() => setFrame((f) => f + 1), 80);
1369
+ return () => clearInterval(id);
1370
+ }, []);
1371
+ const nameW = Math.min(13, groups.reduce((w, g) => Math.max(w, PROVIDERS[g.provider].name.length), 0));
1372
+ const readyCount = groups.filter((g) => g.accounts.every((a) => accountReady(resolveReady(a.id), g.provider))).length;
1373
+ const maxRows = Math.max(1, rows - 7);
1374
+ const visible = groups.slice(0, maxRows);
1375
+ const hidden = groups.length - visible.length;
1376
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1377
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "greenBright", children: [
1378
+ glyphs().dotSel,
1379
+ " tokmon"
1380
+ ] }),
1381
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1382
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1383
+ "Detecting installed tools",
1384
+ glyphs().ellipsis
1385
+ ] }),
1386
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1387
+ " ",
1388
+ groups.length,
1389
+ " found"
1390
+ ] })
1391
+ ] }),
1392
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1393
+ visible.map((g, i) => {
1394
+ const meta = PROVIDERS[g.provider];
1395
+ const items = g.accounts.map((a) => stats.get(a.id));
1396
+ const ready = g.accounts.every((a) => accountReady(resolveReady(a.id), g.provider));
1397
+ const errored = items.some((s) => !!s?.billing?.error);
1398
+ const revealed = frame >= i * STAGGER_FRAMES;
1399
+ const name = truncateName(meta.name, nameW);
1400
+ const namePad = " ".repeat(Math.max(0, nameW - name.length));
1401
+ if (!revealed) {
1402
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
1403
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1404
+ glyphs().dot,
1405
+ " "
1406
+ ] }),
1407
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1408
+ name,
1409
+ namePad
1410
+ ] })
1411
+ ] }, g.provider);
1412
+ }
1413
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
1414
+ /* @__PURE__ */ jsxs6(Text6, { color: meta.color, children: [
1415
+ glyphs().dot,
1416
+ " "
1417
+ ] }),
1418
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: meta.color, children: name }),
1419
+ /* @__PURE__ */ jsx6(Text6, { children: namePad }),
1420
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
1421
+ errored ? /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
1422
+ glyphs().warn,
1423
+ " "
1424
+ ] }) : ready ? /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
1425
+ glyphs().check,
1426
+ " "
1427
+ ] }) : /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
1428
+ sp[frame % sp.length],
1429
+ " "
1430
+ ] }),
1431
+ errored ? /* @__PURE__ */ jsx6(Text6, { color: "red", children: headlineFor(g, items) }) : ready ? /* @__PURE__ */ jsx6(Text6, { children: headlineFor(g, items) }) : /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1432
+ "loading",
1433
+ glyphs().ellipsis
1434
+ ] })
1435
+ ] }, g.provider);
1436
+ }),
1437
+ hidden > 0 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1438
+ "+",
1439
+ hidden,
1440
+ " more"
1441
+ ] })
1442
+ ] }),
1443
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1444
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1445
+ "loading ",
1446
+ readyCount,
1447
+ " / ",
1448
+ groups.length
1449
+ ] }),
1450
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1451
+ " ",
1452
+ glyphs().middot,
1453
+ " W opens the web dashboard once loaded"
1454
+ ] })
1455
+ ] })
1456
+ ] });
1457
+ }
1458
+
1459
+ // src/ui/app-layout.logic.ts
1460
+ function deriveSlots(accounts) {
1461
+ return accounts.length > 1 ? [{ id: null, name: "All", color: "whiteBright" }, ...accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }))] : accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }));
1462
+ }
1463
+ function findActiveSlot(slots, activeAccountId) {
1464
+ if (activeAccountId === null) return { activeSlotIdx: 0, focusId: slots[0]?.id ?? null };
1465
+ const i = slots.findIndex((s) => s.id === activeAccountId);
1466
+ const activeSlotIdx = i < 0 ? 0 : i;
1467
+ return { activeSlotIdx, focusId: slots[activeSlotIdx]?.id ?? null };
1468
+ }
1469
+ function computeChrome(slots, cols, rows) {
1470
+ const hasStrip = slots.length > 1;
1471
+ const stripChipW = (s) => 2 + 2 + truncateName(s.name, 16).length + 2;
1472
+ const stripChars = slots.reduce((sum, s) => sum + stripChipW(s), 0);
1473
+ const stripLines = hasStrip ? Math.max(1, Math.ceil(stripChars / Math.max(1, cols - 4 - 7))) : 0;
1474
+ const headerRows = cols < 70 ? 2 : 1;
1475
+ const CHROME = 2 + headerRows + 3 + (hasStrip ? 1 + stripLines : 0) + 2 + 2;
1476
+ const gridBudget = Math.max(1, rows - CHROME);
1477
+ return { hasStrip, stripChipW, stripChars, stripLines, headerRows, CHROME, gridBudget };
1478
+ }
1479
+
1480
+ // src/ui/resizing.tsx
1481
+ import { Box as Box7, Text as Text7 } from "ink";
1482
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1483
+ function ResizingView({ cols, rows }) {
1484
+ return /* @__PURE__ */ jsx7(Box7, { width: cols, height: rows, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1485
+ glyphs().dotSel,
1486
+ " resizing\u2026 ",
1487
+ /* @__PURE__ */ jsx7(Text7, { color: "greenBright", children: cols }),
1488
+ "\xD7",
1489
+ /* @__PURE__ */ jsx7(Text7, { color: "greenBright", children: rows })
1490
+ ] }) });
1491
+ }
1492
+
1493
+ // src/ui/account-strip.tsx
1494
+ import { memo as memo5 } from "react";
1495
+ import { Box as Box8, Text as Text8 } from "ink";
1496
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1497
+ var AccountStrip = memo5(function AccountStrip2({ slots, activeIdx, onSelect }) {
1498
+ return /* @__PURE__ */ jsx8(Box8, { flexWrap: "wrap", children: slots.map((s, i) => {
1499
+ const active = i === activeIdx;
1500
+ const dot = s.id === null ? glyphs().dotAll : glyphs().dot;
1501
+ const label = truncateName(s.name, 16);
1502
+ return /* @__PURE__ */ jsxs8(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
1503
+ /* @__PURE__ */ jsx8(Text8, { dimColor: !active, children: i }),
1504
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
1505
+ /* @__PURE__ */ jsx8(Text8, { color: s.color, bold: active, dimColor: !active, children: dot }),
1506
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
1507
+ active ? /* @__PURE__ */ jsx8(Text8, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: label })
1508
+ ] }, s.id ?? "__all__");
1509
+ }) });
1510
+ });
1511
+
1512
+ // src/ui/footer.tsx
1513
+ import { memo as memo6 } from "react";
1514
+ import { Box as Box9, Text as Text9, Transform } from "ink";
1515
+
1516
+ // src/ui/terminal.ts
1517
+ import { spawn } from "child_process";
1518
+ import { appendFileSync as appendFileSync2 } from "fs";
1519
+ var IS_TTY = process.stdin.isTTY === true;
1520
+ var REPO_URL = "https://github.com/DavidIlie/tokmon";
1521
+ var SITE_URL = "https://davidilie.com";
1522
+ var IS_APPLE_TERMINAL = process.env.TERM_PROGRAM === "Apple_Terminal";
1523
+ function detectHyperlinks(env, isTTY) {
1524
+ const force = env.FORCE_HYPERLINK;
1525
+ if (force != null && force !== "") return force !== "0" && force.toLowerCase() !== "false";
1526
+ if (!isTTY || env.TERM === "dumb" || env.NO_HYPERLINK) return false;
1527
+ if (env.WT_SESSION || env.ConEmuANSI === "ON" || env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
1528
+ if (env.KONSOLE_VERSION || env.TERMINAL_EMULATOR === "JetBrains-JediTerm") return true;
1529
+ if (env.VTE_VERSION && Number(env.VTE_VERSION) >= 5e3) return true;
1530
+ const tp = env.TERM_PROGRAM;
1531
+ if (tp) {
1532
+ const [maj, min] = (env.TERM_PROGRAM_VERSION ?? "").split(".").map((n) => Number(n) || 0);
1533
+ if (tp === "iTerm.app") return maj > 3 || maj === 3 && min >= 1;
1534
+ if (tp === "vscode" || tp === "WezTerm" || tp === "ghostty" || tp === "Hyper" || tp === "Tabby" || tp === "rio") return true;
1535
+ }
1536
+ return false;
1537
+ }
1538
+ var HYPERLINKS = detectHyperlinks(process.env, process.stdout.isTTY === true);
1539
+ function openUrl(url) {
1540
+ if (process.env.TOKMON_OPENLOG) {
1541
+ try {
1542
+ appendFileSync2(process.env.TOKMON_OPENLOG, url + "\n");
1543
+ } catch {
1544
+ }
1545
+ return;
1546
+ }
1547
+ try {
1548
+ if (process.platform === "darwin") spawn("open", [url], { stdio: "ignore", detached: true }).unref();
1549
+ else if (process.platform === "win32") spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
1550
+ else spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
1551
+ } catch {
1552
+ }
1553
+ }
1554
+ function osc8(text, url) {
1555
+ if (!HYPERLINKS) return text;
1556
+ return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
1557
+ }
1558
+
1559
+ // src/ui/footer.tsx
1560
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1561
+ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1562
+ const inner = cols - 4;
1563
+ const BASE = "by David Ilie (davidilie.com) \xB7 O=repo W=web s=settings q=quit".length;
1564
+ const optHint = (glyphs().shift === "\u21E7" ? "\u2325" : "opt") + "-click links ";
1565
+ const OPT = IS_APPLE_TERMINAL ? optHint.length : 0;
1566
+ const JUMP = "0-9=jump a/A=cycle ".length;
1567
+ const PAGE = "scroll=page ".length;
1568
+ const showOpt = IS_APPLE_TERMINAL && inner >= BASE + OPT;
1569
+ const showJump = hasAccounts && inner >= BASE + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
1570
+ const showPage = paginated && inner >= BASE + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
1571
+ return /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexWrap: "nowrap", children: [
1572
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "by " }),
1573
+ /* @__PURE__ */ jsx9(LinkBox, { onClick: () => openUrl(REPO_URL), children: /* @__PURE__ */ jsx9(Transform, { transform: (s) => osc8(s, REPO_URL), children: /* @__PURE__ */ jsx9(Text9, { underline: true, children: "David Ilie" }) }) }),
1574
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " (" }),
1575
+ /* @__PURE__ */ jsx9(LinkBox, { onClick: () => openUrl(SITE_URL), children: /* @__PURE__ */ jsx9(Transform, { transform: (s) => osc8(s, SITE_URL), children: /* @__PURE__ */ jsx9(Text9, { color: "cyan", underline: true, children: "davidilie.com" }) }) }),
1576
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1577
+ ") ",
1578
+ glyphs().middot,
1579
+ " O=repo W=web s=settings "
1580
+ ] }),
1581
+ showOpt && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: optHint }),
1582
+ showJump && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "0-9=jump a/A=cycle " }),
1583
+ showPage && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "scroll=page " }),
1584
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q=quit" })
1585
+ ] });
1586
+ });
1587
+
1588
+ // src/ui/tiny-fallback.tsx
1589
+ import { Box as Box10, Text as Text10 } from "ink";
1590
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1591
+ function TinyFallback({ groups, stats, rows, cols }) {
1592
+ const maxLines = Math.max(1, rows - 4);
1593
+ const visible = groups.slice(0, maxLines);
1594
+ const hidden = groups.length - visible.length;
1595
+ const w = Math.max(8, cols - 2);
1596
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
1597
+ /* @__PURE__ */ jsxs10(Text10, { bold: true, color: "greenBright", children: [
1598
+ glyphs().dotSel,
1599
+ " tokmon"
1600
+ ] }),
1601
+ groups.length === 0 ? /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
1602
+ "No providers ",
1603
+ glyphs().emDash,
1604
+ " s=settings"
1605
+ ] }) : visible.map((g) => /* @__PURE__ */ jsx10(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
1606
+ hidden > 0 && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
1607
+ "+",
1608
+ hidden,
1609
+ " more (enlarge terminal)"
1610
+ ] }),
1611
+ /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1612
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "s=settings q=quit" })
1613
+ ] });
1614
+ }
1615
+ function TinyRow({ provider, accounts, stats, width }) {
1616
+ const meta = PROVIDERS[provider];
1617
+ const dashboards = accounts.map((a) => stats.get(a.id)?.dashboard).filter(Boolean);
1618
+ const billings = accounts.map((a) => stats.get(a.id)?.billing).filter(Boolean);
1619
+ const todayCost = dashboards.reduce((sum, d) => sum + (d?.today.cost ?? 0), 0);
1620
+ const pctMetric = billings.flatMap((b) => b?.metrics ?? []).find((m) => m.format.kind === "percent");
1621
+ const detail = meta.hasUsage ? `${currency(todayCost)} today` : pctMetric ? `${Math.round(pctMetric.used)}%` : "billing";
1622
+ const name = truncateName(meta.name, Math.max(4, width - 18));
1623
+ return /* @__PURE__ */ jsxs10(Box10, { width, children: [
1624
+ /* @__PURE__ */ jsxs10(Text10, { color: meta.color, children: [
1625
+ glyphs().dot,
1626
+ " "
1627
+ ] }),
1628
+ /* @__PURE__ */ jsx10(Text10, { bold: true, color: meta.color, children: name }),
1629
+ /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1630
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: detail })
1631
+ ] });
1632
+ }
1633
+
1634
+ // src/client/use-daemon.ts
1635
+ import { useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState3 } from "react";
1636
+
1637
+ // src/client/daemon-rpc-client.ts
1638
+ import { Cause, Context, Duration, Effect, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
1639
+ import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
1640
+ import * as Socket from "effect/unstable/socket/Socket";
1641
+ var TokmonRpcClient = class extends Context.Service()(
1642
+ "tokmon/client/DaemonRpcClient/TokmonRpcClient"
1643
+ ) {
1644
+ };
1645
+ var dynamicImport = new Function("specifier", "return import(specifier)");
1646
+ function toWsUrl(baseUrl, token) {
1647
+ const base = typeof window === "undefined" ? void 0 : window.location.origin;
1648
+ const url = new URL(baseUrl, base);
1649
+ if (url.protocol === "http:") url.protocol = "ws:";
1650
+ else if (url.protocol === "https:") url.protocol = "wss:";
1651
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1652
+ throw new Error(`unsupported daemon RPC protocol: ${url.protocol}`);
1653
+ }
1654
+ url.pathname = TOKMON_WS_PATH;
1655
+ if (token) url.searchParams.set("wsToken", token);
1656
+ return url.toString();
1657
+ }
1658
+ function shouldUseNodeTransport(transport) {
1659
+ if (transport === "node") return true;
1660
+ if (transport === "browser") return false;
1661
+ return typeof window === "undefined";
1662
+ }
1663
+ async function socketLayerFor(url, transport) {
1664
+ if (shouldUseNodeTransport(transport)) {
1665
+ const NodeSocket = await dynamicImport("@effect/platform-node/NodeSocket");
1666
+ return NodeSocket.layerWebSocket(url);
1667
+ }
1668
+ return Socket.layerWebSocket(url).pipe(
1669
+ Layer.provide(Socket.layerWebSocketConstructorGlobal)
1670
+ );
1671
+ }
1672
+ function retryPolicy(options) {
1673
+ const attempts = options.reconnectAttempts ?? 5;
1674
+ const baseDelay = options.reconnectBaseDelayMs ?? 250;
1675
+ return Schedule.addDelay(
1676
+ Schedule.recurs(attempts),
1677
+ (retryCount) => Effect.succeed(Duration.millis(Math.min(baseDelay * (retryCount + 1), 2500)))
1678
+ );
1679
+ }
1680
+ function createDaemonRpcClient(baseUrl, options = {}) {
1681
+ const url = toWsUrl(baseUrl, options.wsToken);
1682
+ const fibers = /* @__PURE__ */ new Set();
1683
+ let session = null;
1684
+ let sessionPromise = null;
1685
+ let closed = false;
1686
+ const setConn = (state, error) => {
1687
+ options.onConn?.(state, error);
1688
+ };
1689
+ const makeProtocolLayer = async () => {
1690
+ const socketLayer = await socketLayerFor(url, options.transport);
1691
+ const connectionHooksLayer = Layer.succeed(
1692
+ RpcClient.ConnectionHooks,
1693
+ RpcClient.ConnectionHooks.of({
1694
+ onConnect: Effect.sync(() => {
1695
+ setConn("live");
1696
+ }),
1697
+ onDisconnect: Effect.sync(() => {
1698
+ if (!closed) setConn("reconnecting");
1699
+ })
1700
+ })
1701
+ );
1702
+ return Layer.effect(
1703
+ RpcClient.Protocol,
1704
+ RpcClient.makeProtocolSocket({
1705
+ retryPolicy: retryPolicy(options),
1706
+ retryTransientErrors: true
1707
+ })
1708
+ ).pipe(
1709
+ Layer.provide(Layer.mergeAll(socketLayer, RpcSerialization.layerJson, connectionHooksLayer))
1710
+ );
1711
+ };
1712
+ const ensureSession = async () => {
1713
+ if (closed) throw new Error("daemon RPC client is closed");
1714
+ if (session) return session;
1715
+ if (sessionPromise) return sessionPromise;
1716
+ setConn("connecting");
1717
+ sessionPromise = (async () => {
1718
+ let runtime;
1719
+ try {
1720
+ const protocolLayer = await makeProtocolLayer();
1721
+ const clientLayer = Layer.effect(
1722
+ TokmonRpcClient,
1723
+ RpcClient.make(TokmonRpcGroup)
1724
+ ).pipe(
1725
+ Layer.provide(protocolLayer)
1726
+ );
1727
+ runtime = ManagedRuntime.make(clientLayer);
1728
+ await runtime.runPromise(TokmonRpcClient.asEffect());
1729
+ if (closed) {
1730
+ await runtime.dispose();
1731
+ throw new Error("daemon RPC client is closed");
1732
+ }
1733
+ session = { runtime };
1734
+ return session;
1735
+ } catch (error) {
1736
+ sessionPromise = null;
1737
+ await runtime?.dispose().catch(() => {
1738
+ });
1739
+ if (!closed) setConn("error", error);
1740
+ throw error;
1741
+ }
1742
+ })();
1743
+ return sessionPromise;
1744
+ };
1745
+ const run = async (effectFor) => {
1746
+ const active = await ensureSession();
1747
+ try {
1748
+ return await active.runtime.runPromise(
1749
+ TokmonRpcClient.use((client) => effectFor(client))
1750
+ );
1751
+ } catch (error) {
1752
+ if (!closed) setConn("error", error);
1753
+ throw error;
1754
+ }
1755
+ };
1756
+ const subscribe = (streamFor, onValue) => {
1757
+ if (closed) return () => {
1758
+ };
1759
+ let fiber = null;
1760
+ let unsubscribed = false;
1761
+ void (async () => {
1762
+ try {
1763
+ const active = await ensureSession();
1764
+ if (closed || unsubscribed) return;
1765
+ fiber = active.runtime.runFork(
1766
+ TokmonRpcClient.use(
1767
+ (client) => streamFor(client).pipe(
1768
+ Stream.runForEach(
1769
+ (value) => Effect.sync(() => {
1770
+ try {
1771
+ onValue(value);
1772
+ } catch {
1773
+ }
1774
+ })
1775
+ )
1776
+ )
1777
+ ).pipe(Effect.catchCause(
1778
+ (cause) => Effect.sync(() => {
1779
+ if (!closed && !unsubscribed) setConn("error", Cause.squash(cause));
1780
+ })
1781
+ ))
1782
+ );
1783
+ fibers.add(fiber);
1784
+ fiber.addObserver(() => {
1785
+ if (fiber) fibers.delete(fiber);
1786
+ });
1787
+ } catch (error) {
1788
+ if (!closed && !unsubscribed) setConn("error", error);
1789
+ }
1790
+ })();
1791
+ return () => {
1792
+ unsubscribed = true;
1793
+ if (!fiber) return;
1794
+ fibers.delete(fiber);
1795
+ void (session?.runtime.runPromise(Fiber.interrupt(fiber)) ?? Effect.runPromise(Fiber.interrupt(fiber))).catch(() => {
1796
+ });
1797
+ };
1798
+ };
1799
+ return {
1800
+ getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})),
1801
+ setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)),
1802
+ refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
1803
+ browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
1804
+ subscribeSnapshot: (onSnapshot) => subscribe((client) => client[TOKMON_WS_METHODS.snapshot]({}), onSnapshot),
1805
+ subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}), onConfig),
1806
+ async close() {
1807
+ if (closed) return;
1808
+ closed = true;
1809
+ setConn("closed");
1810
+ const active = [...fibers];
1811
+ fibers.clear();
1812
+ const activeSession = session ?? await sessionPromise?.catch(() => null) ?? null;
1813
+ await Promise.all(active.map(
1814
+ (fiber) => (activeSession?.runtime.runPromise(Fiber.interrupt(fiber)) ?? Effect.runPromise(Fiber.interrupt(fiber))).catch(() => {
1815
+ })
1816
+ ));
1817
+ session = null;
1818
+ sessionPromise = null;
1819
+ if (activeSession) {
1820
+ await activeSession.runtime.dispose().catch(() => {
1821
+ });
1822
+ }
1823
+ }
1824
+ };
1825
+ }
1826
+
1827
+ // src/client/use-daemon.ts
1828
+ function useDaemon(baseUrl, wsToken) {
1829
+ const [snapshot, setSnapshot] = useState3(null);
1830
+ const [conn, setConn] = useState3("connecting");
1831
+ const [config, setConfigState] = useState3(null);
1832
+ const client = useMemo(() => {
1833
+ if (!baseUrl || !wsToken) return null;
1834
+ return createDaemonRpcClient(baseUrl, {
1835
+ transport: "node",
1836
+ wsToken,
1837
+ onConn: (state) => {
1838
+ if (state !== "closed") setConn(state);
1839
+ }
1840
+ });
1841
+ }, [baseUrl, wsToken]);
1842
+ const clientRef = useRef2(client);
1843
+ clientRef.current = client;
1844
+ useEffect3(() => {
1845
+ if (!client) return;
1846
+ const unsubSnapshot = client.subscribeSnapshot(setSnapshot);
1847
+ const unsubConfig = client.subscribeConfig(setConfigState);
1848
+ return () => {
1849
+ unsubSnapshot();
1850
+ unsubConfig();
1851
+ void client.close();
1852
+ };
1853
+ }, [client]);
1854
+ const requireClient = () => {
1855
+ if (!clientRef.current) throw new Error("daemon RPC client is unavailable");
1856
+ return clientRef.current;
1857
+ };
1858
+ return {
1859
+ snapshot,
1860
+ conn,
1861
+ config,
1862
+ setConfig: (next) => requireClient().setConfig(next),
1863
+ refresh: (scope) => requireClient().refresh(scope),
1864
+ browse: (path) => requireClient().browseFs(path)
1865
+ };
1866
+ }
1867
+
1868
+ // src/client/snapshot-adapter.ts
1869
+ function indexById(snapshot) {
1870
+ const m = /* @__PURE__ */ new Map();
1871
+ if (snapshot) for (const a of snapshot.accounts) m.set(a.id, a);
1872
+ return m;
1873
+ }
1874
+ function toStatsMap(snapshot, accounts) {
1875
+ const byId = indexById(snapshot);
1876
+ const out = /* @__PURE__ */ new Map();
1877
+ for (const account of accounts) {
1878
+ const wa = byId.get(account.id);
1879
+ out.set(account.id, {
1880
+ account,
1881
+ dashboard: wa?.dashboard ?? null,
1882
+ billing: wa?.billing ?? null
1883
+ });
1884
+ }
1885
+ return out;
1886
+ }
1887
+ function toCursorRows(snapshot, accountId) {
1888
+ if (!snapshot || !accountId) return null;
1889
+ const wa = snapshot.accounts.find((a) => a.id === accountId);
1890
+ if (!wa) return null;
1891
+ if (wa.billingState === "pending") return null;
1892
+ const spend = wa.billing?.modelSpend;
1893
+ if (!spend) return [];
1894
+ return spend.map((m) => ({ name: m.name, usd: m.usd, requests: m.requests }));
1895
+ }
1896
+ function pickTable(snapshot, accountIds) {
1897
+ if (!snapshot) return null;
1898
+ const byId = indexById(snapshot);
1899
+ const tables = [];
1900
+ for (const id of accountIds) {
1901
+ const t = byId.get(id)?.table;
1902
+ if (t) tables.push(t);
1903
+ }
1904
+ return coalesceTables(tables);
1905
+ }
1906
+
1907
+ // src/app.logic.ts
1908
+ var TABS = ["Dashboard", "Table"];
1909
+ var VIEWS = ["Daily", "Weekly", "Monthly"];
1910
+ var SORTS = [
1911
+ { label: "date", dir: "up" },
1912
+ { label: "date", dir: "down" },
1913
+ { label: "cost", dir: "up" },
1914
+ { label: "cost", dir: "down" }
1915
+ ];
1916
+ var CURSOR_SORTS = [
1917
+ { label: "cost", dir: "down" },
1918
+ { label: "amount", dir: "down" },
1919
+ { label: "model", dir: null }
1920
+ ];
1921
+ var acctKey = (a) => `${a.id}:${a.homeDir ?? ""}`;
1922
+ var clampCaret = (caret, len) => Math.max(0, Math.min(caret, len));
1923
+ function spliceInsert(value, caret, text) {
1924
+ const c = clampCaret(caret, value.length);
1925
+ return { value: value.slice(0, c) + text + value.slice(c), caret: c + text.length };
1926
+ }
1927
+ function spliceBackspace(value, caret) {
1928
+ const c = clampCaret(caret, value.length);
1929
+ if (c === 0) return { value, caret: 0 };
1930
+ return { value: value.slice(0, c - 1) + value.slice(c), caret: c - 1 };
1931
+ }
1932
+ function applyStartup(c, cliInterval) {
1933
+ if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
1934
+ if (c.defaultFocus === "all") c = { ...c, activeAccountId: null };
1935
+ return c;
1936
+ }
1937
+ function upsert(prev, account, patch) {
1938
+ const next = new Map(prev);
1939
+ const cur = next.get(account.id) ?? { account, dashboard: null, billing: null };
1940
+ next.set(account.id, { ...cur, account, ...patch });
1941
+ return next;
1942
+ }
1943
+ async function fetchScopeTable(scope, tz) {
1944
+ const tables = await Promise.all(scope.map(async (acc) => {
1945
+ const provider = PROVIDERS[acc.providerId];
1946
+ if (!provider.fetchTable) return null;
1947
+ try {
1948
+ return await provider.fetchTable(acc, tz);
1949
+ } catch {
1950
+ return null;
1951
+ }
1952
+ }));
1953
+ const valid = tables.filter((t) => t !== null);
1954
+ return coalesceTables(valid);
1955
+ }
1956
+ function sortLabel(entry) {
1957
+ if (entry.dir === "up") return `${entry.label} ${glyphs().arrowU}`;
1958
+ if (entry.dir === "down") return `${entry.label} ${glyphs().arrowD}`;
1959
+ return entry.label;
1960
+ }
1961
+ function sortRows(rows, sortIdx) {
1962
+ const sorted = [...rows];
1963
+ switch (sortIdx % SORTS.length) {
1964
+ case 0:
1965
+ return sorted.sort((a, b) => a.label.localeCompare(b.label));
1966
+ case 1:
1967
+ return sorted.sort((a, b) => b.label.localeCompare(a.label));
1968
+ case 2:
1969
+ return sorted.sort((a, b) => a.cost - b.cost);
1970
+ case 3:
1971
+ return sorted.sort((a, b) => b.cost - a.cost);
1972
+ default:
1973
+ return sorted;
1974
+ }
1975
+ }
1976
+ function filterTokenRows(rows, q) {
1977
+ if (!q) return rows;
1978
+ const s = q.toLowerCase();
1979
+ return rows.filter((r) => r.label.toLowerCase().includes(s) || r.models.some((m) => m.toLowerCase().includes(s)));
1980
+ }
1981
+ function filterCursorRows(rows, q) {
1982
+ if (!q) return rows;
1983
+ const s = q.toLowerCase();
1984
+ return rows.filter((r) => r.name.toLowerCase().includes(s));
1985
+ }
1986
+ function sortCursorRows(rows, sortIdx) {
1987
+ const out = [...rows];
1988
+ switch (sortIdx % CURSOR_SORTS.length) {
1989
+ case 1:
1990
+ return out.sort((a, b) => b.requests - a.requests);
1991
+ case 2:
1992
+ return out.sort((a, b) => a.name.localeCompare(b.name));
1993
+ default:
1994
+ return out.sort((a, b) => b.usd - a.usd);
1995
+ }
1996
+ }
1997
+
1998
+ // src/ui/keybindings.ts
1999
+ function handleKey(input, key, ctx) {
2000
+ const {
2001
+ showPicker,
2002
+ pickerProviders,
2003
+ onboardCursor,
2004
+ setOnboardCursor,
2005
+ toggleOnboard,
2006
+ confirmOnboarding,
2007
+ exit,
2008
+ showSettings,
2009
+ accountForm,
2010
+ setAccountForm,
2011
+ commitAccountForm,
2012
+ cycleFormField,
2013
+ cycleProvider,
2014
+ cycleColor,
2015
+ isPrintable,
2016
+ insertText,
2017
+ tzEdit,
2018
+ setTzEdit,
2019
+ setTzError,
2020
+ updateConfig,
2021
+ setTzCaret,
2022
+ tzValueRef,
2023
+ tzCaretRef,
2024
+ tab,
2025
+ searchMode,
2026
+ setSearchMode,
2027
+ search,
2028
+ setSearch,
2029
+ setSearchCaret,
2030
+ searchValueRef,
2031
+ searchCaretRef,
2032
+ showLoader,
2033
+ configReady,
2034
+ toggleWeb,
2035
+ settingsCursor,
2036
+ settingsTab,
2037
+ setSettingsTab,
2038
+ setShowSettings,
2039
+ cfg,
2040
+ trackedAccountRows,
2041
+ moveAccount,
2042
+ setSettingsCursor,
2043
+ toggleProvider,
2044
+ openEditAccount,
2045
+ openConfigureAccount,
2046
+ deleteAccount,
2047
+ openAddAccount,
2048
+ cycleAccount,
2049
+ setTab,
2050
+ resetView,
2051
+ slots,
2052
+ dashPaginated,
2053
+ dashPageCount,
2054
+ setDashPage,
2055
+ cycleTableProvider,
2056
+ setExpanded,
2057
+ setSort,
2058
+ SORTS_FOR,
2059
+ tableIsCursor,
2060
+ setView,
2061
+ cursor,
2062
+ rowCountRef,
2063
+ rows,
2064
+ setCursor,
2065
+ clampRow
2066
+ } = ctx;
2067
+ if (showPicker) {
2068
+ if (input === "q") {
2069
+ exit();
2070
+ return;
2071
+ }
2072
+ const startIdx = pickerProviders.length;
2073
+ if (key.upArrow) {
2074
+ setOnboardCursor((c) => Math.max(0, c - 1));
2075
+ return;
2076
+ }
2077
+ if (key.downArrow) {
2078
+ setOnboardCursor((c) => Math.min(startIdx, c + 1));
2079
+ return;
2080
+ }
2081
+ if (input === " ") {
2082
+ toggleOnboard(onboardCursor);
2083
+ return;
2084
+ }
2085
+ if (key.return) {
2086
+ if (onboardCursor === startIdx) confirmOnboarding();
2087
+ else toggleOnboard(onboardCursor);
2088
+ return;
2089
+ }
2090
+ return;
2091
+ }
2092
+ if (showSettings && accountForm) {
2093
+ if (key.escape) {
2094
+ setAccountForm(null);
2095
+ return;
2096
+ }
2097
+ if (key.ctrl && input === "s") {
2098
+ commitAccountForm();
2099
+ return;
2100
+ }
2101
+ if (key.tab) {
2102
+ cycleFormField(key.shift ? -1 : 1);
2103
+ return;
2104
+ }
2105
+ if (key.upArrow) {
2106
+ cycleFormField(-1);
2107
+ return;
2108
+ }
2109
+ if (key.downArrow) {
2110
+ cycleFormField(1);
2111
+ return;
2112
+ }
2113
+ if (accountForm.field === "provider") {
2114
+ if (key.leftArrow) {
2115
+ cycleProvider(-1);
2116
+ return;
2117
+ }
2118
+ if (key.rightArrow) {
2119
+ cycleProvider(1);
2120
+ return;
2121
+ }
2122
+ if (key.return) {
2123
+ setAccountForm((f) => f && { ...f, field: "name", caret: f.name.length });
2124
+ return;
2125
+ }
2126
+ return;
2127
+ }
2128
+ if (accountForm.field === "color") {
2129
+ if (key.leftArrow) {
2130
+ cycleColor(-1);
2131
+ return;
2132
+ }
2133
+ if (key.rightArrow) {
2134
+ cycleColor(1);
2135
+ return;
2136
+ }
2137
+ if (key.return) {
2138
+ commitAccountForm();
2139
+ return;
2140
+ }
2141
+ return;
2142
+ }
2143
+ const tf = accountForm.field;
2144
+ if (key.leftArrow) {
2145
+ setAccountForm((f) => f && { ...f, caret: clampCaret(f.caret - 1, f[tf].length) });
2146
+ return;
2147
+ }
2148
+ if (key.rightArrow) {
2149
+ setAccountForm((f) => f && { ...f, caret: clampCaret(f.caret + 1, f[tf].length) });
2150
+ return;
2151
+ }
2152
+ if (key.ctrl && input === "a") {
2153
+ setAccountForm((f) => f && { ...f, caret: 0 });
2154
+ return;
2155
+ }
2156
+ if (key.ctrl && input === "e") {
2157
+ setAccountForm((f) => f && { ...f, caret: f[tf].length });
2158
+ return;
2159
+ }
2160
+ if (key.return) {
2161
+ if (tf === "name" && accountForm.name.trim() === "") {
2162
+ setAccountForm((f) => f && { ...f, error: "Name required", caret: f.name.length });
2163
+ return;
2164
+ }
2165
+ setAccountForm((f) => f && { ...f, field: tf === "name" ? "homeDir" : "color", caret: tf === "name" ? f.homeDir.length : f.caret });
2166
+ return;
2167
+ }
2168
+ if (key.backspace || key.delete) {
2169
+ setAccountForm((f) => {
2170
+ if (!f || f.field !== "name" && f.field !== "homeDir") return f;
2171
+ const r = spliceBackspace(f[f.field], f.caret);
2172
+ return { ...f, [f.field]: r.value, caret: r.caret, error: null };
2173
+ });
2174
+ return;
2175
+ }
2176
+ if (isPrintable(input, key)) {
2177
+ const clean = sanitizeTyped(input);
2178
+ if (clean) insertText(clean);
2179
+ }
2180
+ return;
2181
+ }
2182
+ if (showSettings && tzEdit !== null) {
2183
+ if (key.escape) {
2184
+ setTzEdit(null);
2185
+ setTzError(null);
2186
+ return;
2187
+ }
2188
+ if (key.return) {
2189
+ const val = tzEdit.trim();
2190
+ if (val === "" || val.toLowerCase() === "system") {
2191
+ updateConfig((c) => ({ ...c, timezone: null }));
2192
+ setTzEdit(null);
2193
+ setTzError(null);
2194
+ } else if (isValidTimezone(val)) {
2195
+ updateConfig((c) => ({ ...c, timezone: val }));
2196
+ setTzEdit(null);
2197
+ setTzError(null);
2198
+ } else {
2199
+ setTzError(`Invalid: ${val}`);
2200
+ }
2201
+ return;
2202
+ }
2203
+ if (key.leftArrow) {
2204
+ setTzCaret((c) => clampCaret(c - 1, tzEdit.length));
2205
+ return;
2206
+ }
2207
+ if (key.rightArrow) {
2208
+ setTzCaret((c) => clampCaret(c + 1, tzEdit.length));
2209
+ return;
2210
+ }
2211
+ if (key.ctrl && input === "a") {
2212
+ setTzCaret(0);
2213
+ return;
2214
+ }
2215
+ if (key.ctrl && input === "e") {
2216
+ setTzCaret(tzEdit.length);
2217
+ return;
2218
+ }
2219
+ if (key.backspace || key.delete) {
2220
+ const r = spliceBackspace(tzValueRef.current, tzCaretRef.current);
2221
+ tzValueRef.current = r.value;
2222
+ tzCaretRef.current = r.caret;
2223
+ setTzEdit(r.value);
2224
+ setTzCaret(r.caret);
2225
+ setTzError(null);
2226
+ return;
2227
+ }
2228
+ if (isPrintable(input, key)) {
2229
+ const clean = sanitizeTyped(input);
2230
+ if (clean) insertText(clean);
2231
+ }
2232
+ return;
2233
+ }
2234
+ if (tab === 1 && searchMode) {
2235
+ if (key.return || key.escape) {
2236
+ setSearchMode(false);
2237
+ if (key.escape) {
2238
+ setSearch("");
2239
+ setSearchCaret(0);
2240
+ }
2241
+ return;
2242
+ }
2243
+ if (key.leftArrow) {
2244
+ setSearchCaret((c) => clampCaret(c - 1, search.length));
2245
+ return;
2246
+ }
2247
+ if (key.rightArrow) {
2248
+ setSearchCaret((c) => clampCaret(c + 1, search.length));
2249
+ return;
2250
+ }
2251
+ if (key.ctrl && input === "a") {
2252
+ setSearchCaret(0);
2253
+ return;
2254
+ }
2255
+ if (key.ctrl && input === "e") {
2256
+ setSearchCaret(search.length);
2257
+ return;
2258
+ }
2259
+ if (key.backspace || key.delete) {
2260
+ const r = spliceBackspace(searchValueRef.current, searchCaretRef.current);
2261
+ searchValueRef.current = r.value;
2262
+ searchCaretRef.current = r.caret;
2263
+ setSearch(r.value);
2264
+ setSearchCaret(r.caret);
2265
+ return;
2266
+ }
2267
+ if (isPrintable(input, key)) {
2268
+ const clean = sanitizeTyped(input);
2269
+ if (clean) insertText(clean);
2270
+ }
2271
+ return;
2272
+ }
2273
+ if (input === "q") {
2274
+ exit();
2275
+ return;
2276
+ }
2277
+ if (input === "O") {
2278
+ openUrl(REPO_URL);
2279
+ return;
2280
+ }
2281
+ if (input === "W" || input === "w" && tab !== 1 && !showSettings) {
2282
+ if (showLoader || !configReady) return;
2283
+ void toggleWeb();
2284
+ return;
2285
+ }
2286
+ if (showSettings) {
2287
+ if (key.escape || input === "s") {
2288
+ setShowSettings(false);
2289
+ return;
2290
+ }
2291
+ const switchSettingsTab = (dir) => {
2292
+ const idx = SETTINGS_TABS.indexOf(settingsTab);
2293
+ setSettingsTab(SETTINGS_TABS[(idx + dir + SETTINGS_TABS.length) % SETTINGS_TABS.length]);
2294
+ setSettingsCursor(-1);
2295
+ setTzEdit(null);
2296
+ setTzError(null);
2297
+ };
2298
+ const rowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
2299
+ if (key.tab) {
2300
+ switchSettingsTab(key.shift ? -1 : 1);
2301
+ return;
2302
+ }
2303
+ if (input === "[") {
2304
+ switchSettingsTab(-1);
2305
+ return;
2306
+ }
2307
+ if (input === "]") {
2308
+ switchSettingsTab(1);
2309
+ return;
2310
+ }
2311
+ if (settingsCursor < 0) {
2312
+ if (key.leftArrow) {
2313
+ switchSettingsTab(-1);
2314
+ return;
2315
+ }
2316
+ if (key.rightArrow) {
2317
+ switchSettingsTab(1);
2318
+ return;
2319
+ }
2320
+ if (key.downArrow || key.return) {
2321
+ setSettingsCursor(0);
2322
+ return;
2323
+ }
2324
+ if (key.upArrow) {
2325
+ setSettingsCursor(Math.max(0, rowCount - 1));
2326
+ return;
2327
+ }
2328
+ return;
2329
+ }
2330
+ const selectedAccountRow = settingsTab === "accounts" && settingsCursor >= 0 && settingsCursor < trackedAccountRows.length ? trackedAccountRows[settingsCursor] : null;
2331
+ if (selectedAccountRow?.source === "configured" && selectedAccountRow.explicitIndex !== void 0 && key.shift && (key.upArrow || key.downArrow)) {
2332
+ moveAccount(selectedAccountRow.explicitIndex, key.upArrow ? -1 : 1);
2333
+ return;
2334
+ }
2335
+ if (key.upArrow) {
2336
+ setSettingsCursor((c) => Math.max(-1, c - 1));
2337
+ return;
2338
+ }
2339
+ if (key.downArrow) {
2340
+ setSettingsCursor((c) => Math.min(rowCount - 1, c + 1));
2341
+ return;
2342
+ }
2343
+ if (settingsTab === "general") {
2344
+ if (settingsCursor === 0) {
2345
+ if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
2346
+ if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
2347
+ return;
2348
+ }
2349
+ if (settingsCursor === 1) {
2350
+ if (key.leftArrow) updateConfig((c) => ({ ...c, billingInterval: Math.max(1, c.billingInterval - 1) }));
2351
+ if (key.rightArrow) updateConfig((c) => ({ ...c, billingInterval: c.billingInterval + 1 }));
2352
+ return;
2353
+ }
2354
+ if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
2355
+ updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
2356
+ return;
2357
+ }
2358
+ if (settingsCursor === 3) {
2359
+ if (key.return) {
2360
+ const init = cfg.timezone ?? "";
2361
+ setTzEdit(init);
2362
+ setTzCaret(init.length);
2363
+ setTzError(null);
2364
+ }
2365
+ if (key.leftArrow || key.rightArrow) updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
2366
+ return;
2367
+ }
2368
+ if (settingsCursor === 4 && (key.leftArrow || key.rightArrow || key.return)) {
2369
+ updateConfig((c) => ({ ...c, dashboardLayout: c.dashboardLayout === "grid" ? "single" : "grid" }));
2370
+ return;
2371
+ }
2372
+ if (settingsCursor === 5 && (key.leftArrow || key.rightArrow || key.return)) {
2373
+ updateConfig((c) => ({ ...c, defaultFocus: c.defaultFocus === "all" ? "last" : "all" }));
2374
+ return;
2375
+ }
2376
+ return;
2377
+ }
2378
+ if (settingsTab === "providers") {
2379
+ if (settingsCursor >= 0 && settingsCursor < PROVIDER_ORDER.length && (input === " " || key.return || key.leftArrow || key.rightArrow)) {
2380
+ toggleProvider(PROVIDER_ORDER[settingsCursor]);
2381
+ }
2382
+ return;
2383
+ }
2384
+ if (settingsTab === "accounts" && settingsCursor >= 0 && settingsCursor < trackedAccountRows.length) {
2385
+ const row = trackedAccountRows[settingsCursor];
2386
+ if (key.return) {
2387
+ if (row.source === "configured") {
2388
+ const acc = cfg.accounts.find((a) => a.id === row.explicitId);
2389
+ if (acc) openEditAccount(acc);
2390
+ } else {
2391
+ openConfigureAccount(row);
2392
+ }
2393
+ return;
2394
+ }
2395
+ if (row.source === "configured" && row.explicitId && (input === "d" || input === "x")) {
2396
+ deleteAccount(row.explicitId);
2397
+ return;
2398
+ }
2399
+ if (input === " ") {
2400
+ updateConfig((c) => ({ ...c, activeAccountId: row.id }));
2401
+ return;
2402
+ }
2403
+ return;
2404
+ }
2405
+ if (settingsTab === "accounts" && settingsCursor === trackedAccountRows.length && key.return) {
2406
+ openAddAccount();
2407
+ }
2408
+ return;
2409
+ }
2410
+ if (input === "s") {
2411
+ setSettingsTab("general");
2412
+ setShowSettings(true);
2413
+ setSettingsCursor(-1);
2414
+ return;
2415
+ }
2416
+ if (input === "a") {
2417
+ cycleAccount(1);
2418
+ return;
2419
+ }
2420
+ if (input === "A") {
2421
+ cycleAccount(-1);
2422
+ return;
2423
+ }
2424
+ if (key.tab) {
2425
+ setTab((t) => (t + 1) % TABS.length);
2426
+ resetView();
2427
+ return;
2428
+ }
2429
+ if (input && /^[0-9]$/.test(input) && slots.length > 1) {
2430
+ const target = slots[parseInt(input, 10)];
2431
+ if (target) {
2432
+ updateConfig((c) => ({ ...c, activeAccountId: target.id }));
2433
+ resetView();
2434
+ }
2435
+ return;
2436
+ }
2437
+ if (tab === 0) {
2438
+ if (dashPaginated) {
2439
+ if (input === "]" || key.downArrow || key.pageDown) {
2440
+ setDashPage((p) => Math.min(dashPageCount - 1, p + 1));
2441
+ return;
2442
+ }
2443
+ if (input === "[" || key.upArrow || key.pageUp) {
2444
+ setDashPage((p) => Math.max(0, p - 1));
2445
+ return;
2446
+ }
2447
+ }
2448
+ if (key.upArrow || key.downArrow || key.pageUp || key.pageDown || input === "G" || input === "g") return;
2449
+ }
2450
+ if (tab === 1) {
2451
+ if (input === "p") {
2452
+ cycleTableProvider(1);
2453
+ return;
2454
+ }
2455
+ if (input === "P") {
2456
+ cycleTableProvider(-1);
2457
+ return;
2458
+ }
2459
+ if (input === "/") {
2460
+ setSearchMode(true);
2461
+ setSearchCaret(search.length);
2462
+ return;
2463
+ }
2464
+ if (key.escape) {
2465
+ if (search) {
2466
+ setSearch("");
2467
+ setSearchCaret(0);
2468
+ } else setExpanded(-1);
2469
+ return;
2470
+ }
2471
+ if (input === "o") {
2472
+ setSort((s) => (s + 1) % SORTS_FOR.length);
2473
+ resetView();
2474
+ return;
2475
+ }
2476
+ if (!tableIsCursor) {
2477
+ if (input === "d") {
2478
+ setView(0);
2479
+ resetView();
2480
+ return;
2481
+ }
2482
+ if (input === "w") {
2483
+ setView(1);
2484
+ resetView();
2485
+ return;
2486
+ }
2487
+ if (input === "m") {
2488
+ setView(2);
2489
+ resetView();
2490
+ return;
2491
+ }
2492
+ if (key.leftArrow) {
2493
+ setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
2494
+ resetView();
2495
+ return;
2496
+ }
2497
+ if (key.rightArrow) {
2498
+ setView((v) => (v + 1) % VIEWS.length);
2499
+ resetView();
2500
+ return;
2501
+ }
2502
+ if (key.return) {
2503
+ setExpanded((e) => e === cursor ? -1 : cursor);
2504
+ return;
2505
+ }
2506
+ }
2507
+ } else {
2508
+ if (key.leftArrow || key.rightArrow) {
2509
+ setTab((t) => (t + 1) % TABS.length);
2510
+ resetView();
2511
+ return;
2512
+ }
2513
+ }
2514
+ if (tab === 1 && !tableIsCursor) {
2515
+ if (key.upArrow) {
2516
+ setCursor((c) => Math.max(0, c - 1));
2517
+ return;
2518
+ }
2519
+ if (key.downArrow) {
2520
+ setCursor((c) => clampRow(c + 1));
2521
+ return;
2522
+ }
2523
+ if (key.pageDown || input === "G") {
2524
+ setCursor((c) => clampRow(input === "G" ? rowCountRef.current - 1 : c + Math.max(1, rows - 12)));
2525
+ return;
2526
+ }
2527
+ if (key.pageUp || input === "g") {
2528
+ setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
2529
+ return;
2530
+ }
2531
+ }
2532
+ }
2533
+
2534
+ // src/app.tsx
2535
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2536
+ var DEBOUNCE_MS = 300;
2537
+ var LOADER_GRACE_MS = 600;
2538
+ var LOADER_MAX_MS = 8e3;
2539
+ var LOADER_MIN_VISIBLE_MS = 700;
2540
+ function useTerminalSize(settleMs = 90) {
2541
+ const { stdout } = useStdout();
2542
+ const read = () => ({ cols: stdout?.columns || 80, rows: stdout?.rows || 24 });
2543
+ const [size, setSize] = useState4(read);
2544
+ const [live, setLive] = useState4(read);
2545
+ const [resizing, setResizing] = useState4(false);
2546
+ useEffect4(() => {
2547
+ if (!stdout) return;
2548
+ let t;
2549
+ const now = () => ({ cols: stdout.columns || 80, rows: stdout.rows || 24 });
2550
+ const settle = () => {
2551
+ setSize(now());
2552
+ setResizing(false);
2553
+ };
2554
+ const onResize = () => {
2555
+ setLive(now());
2556
+ setResizing(true);
2557
+ if (t) clearTimeout(t);
2558
+ t = setTimeout(settle, settleMs);
2559
+ };
2560
+ stdout.on("resize", onResize);
2561
+ return () => {
2562
+ if (t) clearTimeout(t);
2563
+ stdout.off("resize", onResize);
2564
+ };
2565
+ }, [stdout, settleMs]);
2566
+ return { cols: size.cols, rows: size.rows, resizing, live };
2567
+ }
2568
+ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = null, mode = "degraded" }) {
2569
+ const connected = mode === "connected" && baseUrl !== null && wsToken !== null;
2570
+ const degraded = !connected;
2571
+ const daemon = useDaemon(connected ? baseUrl : null, connected ? wsToken : null);
2572
+ const [config, setConfig] = useState4(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
2573
+ const [detected, setDetected] = useState4([]);
2574
+ const [statsLocal, setStats] = useState4(/* @__PURE__ */ new Map());
2575
+ const [peakLocal, setPeak] = useState4(null);
2576
+ const [tableLocal, setTable] = useState4(null);
2577
+ const [tableLoading, setTableLoading] = useState4(false);
2578
+ const [error, setError] = useState4(null);
2579
+ const [updatedLocal, setUpdated] = useState4(/* @__PURE__ */ new Date());
2580
+ const [tab, setTab] = useState4(0);
2581
+ const [view, setView] = useState4(0);
2582
+ const [cursor, setCursor] = useState4(0);
2583
+ const [expanded, setExpanded] = useState4(-1);
2584
+ const [sort, setSort] = useState4(1);
2585
+ const [tableProvider, setTableProvider] = useState4(null);
2586
+ const [search, setSearch] = useState4("");
2587
+ const [searchMode, setSearchMode] = useState4(false);
2588
+ const [cursorRowsLocal, setCursorRows] = useState4(null);
2589
+ const [showSettings, setShowSettings] = useState4(false);
2590
+ const [settingsTab, setSettingsTab] = useState4("general");
2591
+ const [settingsCursor, setSettingsCursor] = useState4(0);
2592
+ const [tzEdit, setTzEdit] = useState4(null);
2593
+ const [tzError, setTzError] = useState4(null);
2594
+ const [tzCaret, setTzCaret] = useState4(0);
2595
+ const [searchCaret, setSearchCaret] = useState4(0);
2596
+ const [accountForm, setAccountForm] = useState4(null);
2597
+ const [onboardSel, setOnboardSel] = useState4(null);
2598
+ const [onboardCursor, setOnboardCursor] = useState4(0);
2599
+ const [dashPage, setDashPage] = useState4(0);
2600
+ const [debouncePassed, setDebouncePassed] = useState4(false);
2601
+ const [graceHold, setGraceHold] = useState4(false);
2602
+ const [loaderShownAt, setLoaderShownAt] = useState4(null);
2603
+ const loaderDone = useRef3(false);
2604
+ const [loaderDoneState, setLoaderDoneFlag] = useState4(false);
2605
+ const setLoaderDone = useCallback((v) => {
2606
+ loaderDone.current = v;
2607
+ setLoaderDoneFlag(v);
2608
+ }, []);
2609
+ const prevShowPicker = useRef3(false);
2610
+ const { exit } = useApp();
2611
+ const { cols, rows, resizing, live } = useTerminalSize();
2612
+ const webRef = useRef3(null);
2613
+ const webStartingRef = useRef3(false);
2614
+ useEffect4(() => () => {
2615
+ void webRef.current?.stop();
2616
+ }, []);
2617
+ const cfg = config ?? DEFAULTS;
2618
+ const interval = cliInterval ?? cfg.interval * 1e3;
2619
+ const billingMs = cfg.billingInterval * 6e4;
2620
+ const tz = resolveTimezone(cfg.timezone);
2621
+ const configReady = config !== null;
2622
+ const accounts = useMemo2(() => buildAccounts(cfg, detected), [cfg, detected]);
2623
+ const trackedAccountRows = useMemo2(() => getTrackedAccountRows(cfg, detected, accounts), [cfg, detected, accounts]);
2624
+ const settingsRowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
2625
+ const accountsRef = useRef3([]);
2626
+ accountsRef.current = accounts;
2627
+ const rowCountRef = useRef3(0);
2628
+ const tabRef = useRef3(0);
2629
+ tabRef.current = tab;
2630
+ const dashPageCountRef = useRef3(1);
2631
+ const seededRef = useRef3(false);
2632
+ const pasteBufRef = useRef3(null);
2633
+ const pasteCarryRef = useRef3("");
2634
+ const pendingLocalConfigRef = useRef3(null);
2635
+ const insertPasteRef = useRef3(() => {
2636
+ });
2637
+ const tzValueRef = useRef3("");
2638
+ const tzCaretRef = useRef3(0);
2639
+ const searchValueRef = useRef3("");
2640
+ const searchCaretRef = useRef3(0);
2641
+ const accountsKey = useMemo2(() => accounts.map(acctKey).join("|"), [accounts]);
2642
+ const snapshot = daemon.snapshot;
2643
+ const stats = useMemo2(
2644
+ () => connected ? toStatsMap(snapshot, accounts) : statsLocal,
2645
+ [connected, snapshot, accounts, statsLocal]
2646
+ );
2647
+ const accountIdentities = useMemo2(() => {
2648
+ const out = /* @__PURE__ */ new Map();
2649
+ for (const [id, stat] of stats) {
2650
+ const billing = stat.billing;
2651
+ if (!billing) continue;
2652
+ out.set(id, {
2653
+ email: billing.email ?? null,
2654
+ displayName: billing.displayName ?? null,
2655
+ plan: billing.plan ?? null
2656
+ });
2657
+ }
2658
+ return out;
2659
+ }, [stats]);
2660
+ const showPeak = accounts.some((a) => a.providerId === "claude");
2661
+ const peak = connected ? showPeak ? snapshot?.peak ?? null : null : peakLocal;
2662
+ const updated = useMemo2(
2663
+ () => connected ? new Date(snapshot?.generatedAt ?? Date.now()) : updatedLocal,
2664
+ [connected, snapshot, updatedLocal]
2665
+ );
2666
+ const intervalLabel = connected && snapshot?.intervalMs ? Math.round(snapshot.intervalMs / 1e3) : cfg.interval;
2667
+ const readyInputFor = useCallback((id) => {
2668
+ if (connected) {
2669
+ const wa = snapshot?.accounts.find((a) => a.id === id);
2670
+ if (!wa) return void 0;
2671
+ return { summaryState: wa.summaryState, billingState: wa.billingState, billing: wa.billing };
2672
+ }
2673
+ return statsReadyInput(statsLocal.get(id));
2674
+ }, [connected, snapshot, statsLocal]);
2675
+ const slots = useMemo2(() => deriveSlots(accounts), [accounts]);
2676
+ const { activeSlotIdx, focusId } = useMemo2(
2677
+ () => findActiveSlot(slots, cfg.activeAccountId),
2678
+ [slots, cfg.activeAccountId]
2679
+ );
2680
+ const visibleAccounts = useMemo2(
2681
+ () => focusId === null ? accounts : accounts.filter((a) => a.id === focusId),
2682
+ [accounts, focusId]
2683
+ );
2684
+ const allGroups = useMemo2(() => accountsByProvider(accounts), [accounts]);
2685
+ const groups = useMemo2(
2686
+ () => focusId === null ? allGroups : accountsByProvider(visibleAccounts),
2687
+ [allGroups, visibleAccounts, focusId]
2688
+ );
2689
+ const tableProvs = useMemo2(() => allGroups.map((g) => g.provider), [allGroups]);
2690
+ const TOO_SMALL = cols < 40 || rows < 12;
2691
+ const allReady = accounts.length > 0 && accounts.every((a) => accountReady(readyInputFor(a.id), a.providerId));
2692
+ const { gridBudget } = useMemo2(() => computeChrome(slots, cols, rows), [slots, cols, rows]);
2693
+ const dashLayout = useMemo2(() => chooseLayout(
2694
+ Math.max(56, cols - 4),
2695
+ gridBudget,
2696
+ groups.length,
2697
+ focusId !== null || cfg.dashboardLayout === "single",
2698
+ cols
2699
+ ), [cols, gridBudget, groups.length, focusId, cfg.dashboardLayout]);
2700
+ const dashPageCount = dashLayout.pageCount;
2701
+ const dashPaginated = dashPageCount > 1;
2702
+ dashPageCountRef.current = dashPageCount;
2703
+ tzValueRef.current = tzEdit ?? "";
2704
+ tzCaretRef.current = clampCaret(tzCaret, (tzEdit ?? "").length);
2705
+ searchValueRef.current = search;
2706
+ searchCaretRef.current = clampCaret(searchCaret, search.length);
2707
+ const isPrintable = (input, key) => !!input && !key.ctrl && !key.meta && !isPasteInput(input);
2708
+ const insertText = (text) => {
2709
+ if (showSettings && accountForm && (accountForm.field === "name" || accountForm.field === "homeDir")) {
2710
+ setAccountForm((f) => {
2711
+ if (!f || f.field !== "name" && f.field !== "homeDir") return f;
2712
+ const r = spliceInsert(f[f.field], f.caret, text);
2713
+ return { ...f, [f.field]: r.value, caret: r.caret, error: null };
2714
+ });
2715
+ } else if (showSettings && tzEdit !== null) {
2716
+ const r = spliceInsert(tzValueRef.current, tzCaretRef.current, text);
2717
+ tzValueRef.current = r.value;
2718
+ tzCaretRef.current = r.caret;
2719
+ setTzEdit(r.value);
2720
+ setTzCaret(r.caret);
2721
+ setTzError(null);
2722
+ } else if (tab === 1 && searchMode) {
2723
+ const r = spliceInsert(searchValueRef.current, searchCaretRef.current, text);
2724
+ searchValueRef.current = r.value;
2725
+ searchCaretRef.current = r.caret;
2726
+ setSearch(r.value);
2727
+ setSearchCaret(r.caret);
2728
+ }
2729
+ };
2730
+ insertPasteRef.current = insertText;
2731
+ const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
2732
+ const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
2733
+ const tableAccounts = useMemo2(
2734
+ () => effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [],
2735
+ [accounts, effTableProvider]
2736
+ );
2737
+ const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
2738
+ const tableAccountIds = useMemo2(() => tableAccounts.map((a) => a.id), [tableAccounts]);
2739
+ const table = useMemo2(
2740
+ () => connected ? pickTable(snapshot, tableAccountIds) : tableLocal,
2741
+ [connected, snapshot, tableAccountIds, tableLocal]
2742
+ );
2743
+ const cursorRows = useMemo2(
2744
+ () => connected ? toCursorRows(snapshot, tableAccounts[0]?.id) : cursorRowsLocal,
2745
+ [connected, snapshot, tableAccounts, cursorRowsLocal]
2746
+ );
2747
+ const needsOnboarding = configReady && !cfg.onboarded;
2748
+ const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
2749
+ const showPicker = needsOnboarding || newProviders.length > 0;
2750
+ const minVisibleHold = loaderShownAt !== null && Date.now() - loaderShownAt < LOADER_MIN_VISIBLE_MS;
2751
+ const showLoader = configReady && !showPicker && !showSettings && !TOO_SMALL && accounts.length > 0 && (!allReady || graceHold || minVisibleHold) && (debouncePassed || loaderShownAt !== null) && !loaderDoneState;
2752
+ const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
2753
+ const onboardEnabled = onboardSel ?? detected;
2754
+ const onboardItems = pickerProviders.map((pid) => ({
2755
+ id: pid,
2756
+ name: PROVIDERS[pid].name,
2757
+ color: PROVIDERS[pid].color,
2758
+ detected: detected.includes(pid),
2759
+ enabled: onboardEnabled.includes(pid)
2760
+ }));
2761
+ useEffect4(() => {
2762
+ const wasPicker = prevShowPicker.current;
2763
+ prevShowPicker.current = showPicker;
2764
+ if (wasPicker && !showPicker) {
2765
+ setLoaderDone(false);
2766
+ setDebouncePassed(false);
2767
+ setGraceHold(false);
2768
+ setLoaderShownAt(null);
2769
+ }
2770
+ }, [showPicker]);
2771
+ useEffect4(() => {
2772
+ if (showLoader && loaderShownAt === null) setLoaderShownAt(Date.now());
2773
+ }, [showLoader, loaderShownAt]);
2774
+ useEffect4(() => {
2775
+ if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
2776
+ detectProviders().then(setDetected);
2777
+ }, []);
2778
+ useEffect4(() => {
2779
+ if (!degraded) return;
2780
+ if (seededRef.current || !configReady || showPicker || accounts.length === 0) return;
2781
+ seededRef.current = true;
2782
+ loadSeedSnapshot().then((snap) => {
2783
+ setStats((prev) => {
2784
+ if (prev.size > 0) return prev;
2785
+ const next = new Map(prev);
2786
+ for (const acc of accountsRef.current) {
2787
+ const s = snap[acc.id];
2788
+ if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
2789
+ }
2790
+ return next;
2791
+ });
2792
+ });
2793
+ }, [degraded, configReady, showPicker, accountsKey]);
2794
+ useEffect4(() => {
2795
+ if (!configReady || showPicker || accounts.length === 0) return;
2796
+ if (allReady || loaderDone.current) return;
2797
+ const debounce = setTimeout(() => setDebouncePassed(true), DEBOUNCE_MS);
2798
+ const deadline = setTimeout(() => {
2799
+ setLoaderDone(true);
2800
+ setDebouncePassed(false);
2801
+ }, LOADER_MAX_MS);
2802
+ return () => {
2803
+ clearTimeout(debounce);
2804
+ clearTimeout(deadline);
2805
+ };
2806
+ }, [configReady, showPicker, accountsKey]);
2807
+ useEffect4(() => {
2808
+ if (!allReady || loaderDone.current) return;
2809
+ if (loaderShownAt === null) {
2810
+ setLoaderDone(true);
2811
+ return;
2812
+ }
2813
+ setGraceHold(true);
2814
+ const minRemaining = Math.max(0, LOADER_MIN_VISIBLE_MS - (Date.now() - loaderShownAt));
2815
+ const hold = Math.max(LOADER_GRACE_MS, minRemaining);
2816
+ const t = setTimeout(() => {
2817
+ setLoaderDone(true);
2818
+ setGraceHold(false);
2819
+ }, hold);
2820
+ return () => clearTimeout(t);
2821
+ }, [allReady, loaderShownAt]);
2822
+ useEffect4(() => {
2823
+ if (!degraded || !configReady || showPicker) return;
2824
+ let active = true;
2825
+ let timer;
2826
+ const load = async () => {
2827
+ try {
2828
+ await Promise.all(accountsRef.current.map(async (acc) => {
2829
+ const provider = PROVIDERS[acc.providerId];
2830
+ if (!provider.hasUsage || !provider.fetchSummary) return;
2831
+ try {
2832
+ const dashboard = await provider.fetchSummary(acc, tz);
2833
+ if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
2834
+ } catch {
2835
+ }
2836
+ }));
2837
+ if (active) {
2838
+ setError(null);
2839
+ setUpdated(/* @__PURE__ */ new Date());
2840
+ }
2841
+ } finally {
2842
+ if (active) timer = setTimeout(load, interval);
2843
+ }
2844
+ };
2845
+ load();
2846
+ return () => {
2847
+ active = false;
2848
+ clearTimeout(timer);
2849
+ };
2850
+ }, [degraded, interval, tz, configReady, showPicker, accountsKey]);
2851
+ useEffect4(() => {
2852
+ if (!degraded || !configReady || showPicker) return;
2853
+ let active = true;
2854
+ let timer;
2855
+ const load = async () => {
2856
+ try {
2857
+ const peakP = accountsRef.current.some((a) => a.providerId === "claude") ? fetchPeak() : Promise.resolve(null);
2858
+ await Promise.all(accountsRef.current.map(async (acc) => {
2859
+ const provider = PROVIDERS[acc.providerId];
2860
+ if (!provider.hasBilling || !provider.fetchBilling) return;
2861
+ try {
2862
+ const billing = await provider.fetchBilling(acc);
2863
+ if (active) setStats((prev) => upsert(prev, acc, { billing }));
2864
+ } catch {
2865
+ }
2866
+ }));
2867
+ const p = await peakP;
2868
+ if (active && p) setPeak(p);
2869
+ } finally {
2870
+ if (active) timer = setTimeout(load, billingMs);
2871
+ }
2872
+ };
2873
+ load();
2874
+ return () => {
2875
+ active = false;
2876
+ clearTimeout(timer);
2877
+ };
2878
+ }, [degraded, billingMs, configReady, showPicker, accountsKey]);
2879
+ const tableKey = useMemo2(
2880
+ () => `${effTableProvider}|${tableAccounts.map(acctKey).join(",")}|${tz}`,
2881
+ [effTableProvider, tableAccounts, tz]
2882
+ );
2883
+ useEffect4(() => {
2884
+ setTable(null);
2885
+ setCursorRows(null);
2886
+ setCursor(0);
2887
+ setExpanded(-1);
2888
+ setSort(tableIsCursor ? 0 : 1);
2889
+ setTableLoading(false);
2890
+ }, [tableKey]);
2891
+ useEffect4(() => {
2892
+ if (!degraded || tab !== 1 || !effTableProvider) return;
2893
+ let active = true;
2894
+ let timer;
2895
+ const fetchOnce = async () => {
2896
+ try {
2897
+ if (tableIsCursor) {
2898
+ const s = await cursorModelSpend(tableAccounts[0]?.homeDir);
2899
+ if (active) setCursorRows(s?.models ?? []);
2900
+ } else {
2901
+ const r = await fetchScopeTable(tableAccounts, tz);
2902
+ if (active) setTable(r);
2903
+ }
2904
+ } catch {
2905
+ }
2906
+ };
2907
+ const run = async () => {
2908
+ setTableLoading(true);
2909
+ await fetchOnce();
2910
+ if (!active) return;
2911
+ setTableLoading(false);
2912
+ const loop = async () => {
2913
+ await fetchOnce();
2914
+ if (active) timer = setTimeout(loop, Math.max(interval, 1e4));
2915
+ };
2916
+ timer = setTimeout(loop, Math.max(interval, 1e4));
2917
+ };
2918
+ run();
2919
+ return () => {
2920
+ active = false;
2921
+ clearTimeout(timer);
2922
+ };
2923
+ }, [degraded, tab, tableKey, interval]);
2924
+ useEffect4(() => {
2925
+ setCursor(0);
2926
+ setExpanded(-1);
2927
+ }, [search]);
2928
+ useEffect4(() => {
2929
+ setDashPage((p) => Math.min(p, dashPageCount - 1));
2930
+ }, [dashPageCount]);
2931
+ useEffect4(() => {
2932
+ setSettingsCursor((c) => Math.max(-1, Math.min(c, settingsRowCount - 1)));
2933
+ }, [settingsRowCount]);
2934
+ const resetView = useCallback(() => {
2935
+ setCursor(0);
2936
+ setExpanded(-1);
2937
+ }, []);
2938
+ const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
2939
+ const PASTE_START = "\x1B[200~";
2940
+ const PASTE_END = "\x1B[201~";
2941
+ const PASTE_MAX = 1 << 20;
2942
+ const handlePasteData = useCallback((chunk) => {
2943
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
2944
+ if (pasteBufRef.current !== null) {
2945
+ const combined2 = pasteBufRef.current + s;
2946
+ const end2 = combined2.indexOf(PASTE_END);
2947
+ if (end2 === -1) {
2948
+ if (combined2.length >= PASTE_MAX) {
2949
+ const clean3 = sanitizeTyped(combined2);
2950
+ pasteBufRef.current = null;
2951
+ if (clean3) insertPasteRef.current(clean3);
2952
+ return true;
2953
+ }
2954
+ pasteBufRef.current = combined2;
2955
+ return true;
2956
+ }
2957
+ const clean2 = sanitizeTyped(combined2.slice(0, end2));
2958
+ pasteBufRef.current = null;
2959
+ if (clean2) insertPasteRef.current(clean2);
2960
+ return end2 + PASTE_END.length >= combined2.length;
2961
+ }
2962
+ const combined = pasteCarryRef.current + s;
2963
+ const start = combined.indexOf(PASTE_START);
2964
+ if (start === -1) {
2965
+ const keep = Math.min(combined.length, PASTE_START.length - 1);
2966
+ pasteCarryRef.current = combined.slice(combined.length - keep);
2967
+ return false;
2968
+ }
2969
+ pasteCarryRef.current = "";
2970
+ const rest = combined.slice(start + PASTE_START.length);
2971
+ const end = rest.indexOf(PASTE_END);
2972
+ if (end === -1) {
2973
+ pasteBufRef.current = rest;
2974
+ return true;
2975
+ }
2976
+ const clean = sanitizeTyped(rest.slice(0, end));
2977
+ if (clean) insertPasteRef.current(clean);
2978
+ return true;
2979
+ }, []);
2980
+ const isPasteInput = useCallback((input) => {
2981
+ if (pasteBufRef.current !== null) return true;
2982
+ return input.includes("[200~") || input.includes("[201~");
2983
+ }, []);
2984
+ const mouse = useMouse();
2985
+ useEffect4(() => {
2986
+ if (!IS_TTY) return;
2987
+ mouse.enable();
2988
+ if (process.stdout.isTTY) {
2989
+ try {
2990
+ process.stdout.write("\x1B[?1003l\x1B[?1002l\x1B[?1015l");
2991
+ } catch {
2992
+ }
2993
+ }
2994
+ const onScroll = (_pos, dir) => {
2995
+ const up = dir === "scrollup";
2996
+ const t = tabRef.current;
2997
+ if (t === 1) {
2998
+ setCursor((c) => up ? Math.max(0, c - 3) : clampRow(c + 3));
2999
+ } else if (t === 0 && dashPageCountRef.current > 1) {
3000
+ setDashPage((p) => up ? Math.max(0, p - 1) : Math.min(dashPageCountRef.current - 1, p + 1));
3001
+ }
3002
+ };
3003
+ mouse.events.on("scroll", onScroll);
3004
+ const onData = (d) => {
3005
+ if (!handlePasteData(d)) dispatchLinkClicks(d);
3006
+ };
3007
+ process.stdin.on("data", onData);
3008
+ return () => {
3009
+ mouse.events.off("scroll", onScroll);
3010
+ process.stdin.off("data", onData);
3011
+ };
3012
+ }, []);
3013
+ const updateConfig = useCallback((fn) => {
3014
+ setConfig((prev) => {
3015
+ const next = normalizeConfig(fn(prev ?? DEFAULTS));
3016
+ pendingLocalConfigRef.current = connected ? next : null;
3017
+ saveConfigSync(next);
3018
+ if (connected) {
3019
+ void daemon.setConfig(next).then((saved) => {
3020
+ if (pendingLocalConfigRef.current && reconcileDaemonConfig(next, saved, pendingLocalConfigRef.current).pendingLocalConfig === null) {
3021
+ pendingLocalConfigRef.current = null;
3022
+ }
3023
+ }).catch(() => {
3024
+ });
3025
+ }
3026
+ return next;
3027
+ });
3028
+ }, [connected, daemon]);
3029
+ const daemonConfig = daemon.config;
3030
+ useEffect4(() => {
3031
+ if (!connected || !daemonConfig) return;
3032
+ setConfig((prev) => {
3033
+ const reconciled = reconcileDaemonConfig(prev, daemonConfig, pendingLocalConfigRef.current);
3034
+ pendingLocalConfigRef.current = reconciled.pendingLocalConfig;
3035
+ return reconciled.config;
3036
+ });
3037
+ }, [connected, daemonConfig]);
3038
+ function toggleOnboard(i) {
3039
+ if (i < 0 || i >= pickerProviders.length) return;
3040
+ const pid = pickerProviders[i];
3041
+ setOnboardSel((prev) => {
3042
+ const base = prev ?? detected;
3043
+ return base.includes(pid) ? base.filter((p) => p !== pid) : [...base, pid];
3044
+ });
3045
+ }
3046
+ function toggleProvider(pid) {
3047
+ updateConfig((c) => ({
3048
+ ...c,
3049
+ knownProviders: c.knownProviders.includes(pid) ? c.knownProviders : [...c.knownProviders, pid],
3050
+ disabledProviders: c.disabledProviders.includes(pid) ? c.disabledProviders.filter((p) => p !== pid) : [...c.disabledProviders, pid]
3051
+ }));
3052
+ }
3053
+ function confirmOnboarding() {
3054
+ const enabled = onboardEnabled;
3055
+ updateConfig((c) => {
3056
+ if (!c.onboarded) {
3057
+ return {
3058
+ ...c,
3059
+ disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
3060
+ knownProviders: [...PROVIDER_ORDER],
3061
+ onboarded: true
3062
+ };
3063
+ }
3064
+ const newlyDisabled = pickerProviders.filter((p) => !enabled.includes(p));
3065
+ return {
3066
+ ...c,
3067
+ disabledProviders: [.../* @__PURE__ */ new Set([...c.disabledProviders, ...newlyDisabled])],
3068
+ knownProviders: [.../* @__PURE__ */ new Set([...c.knownProviders, ...pickerProviders])]
3069
+ };
3070
+ });
3071
+ setOnboardSel(null);
3072
+ setOnboardCursor(0);
3073
+ }
3074
+ const cycleAccount = useCallback((dir) => {
3075
+ if (slots.length <= 1) return;
3076
+ const next = (activeSlotIdx + dir + slots.length) % slots.length;
3077
+ updateConfig((c) => ({ ...c, activeAccountId: slots[next].id }));
3078
+ resetView();
3079
+ }, [slots, activeSlotIdx, updateConfig, resetView]);
3080
+ const cycleTableProvider = useCallback((dir) => {
3081
+ if (tableProvs.length <= 1) return;
3082
+ const cur = effTableProvider ? tableProvs.indexOf(effTableProvider) : 0;
3083
+ const nextProv = tableProvs[(cur + dir + tableProvs.length) % tableProvs.length];
3084
+ setTableProvider(nextProv);
3085
+ const nextIsCursor = !!nextProv && !PROVIDERS[nextProv].hasUsage;
3086
+ setSort(nextIsCursor ? 0 : 1);
3087
+ setCursor(0);
3088
+ setExpanded(-1);
3089
+ setSearch("");
3090
+ setSearchCaret(0);
3091
+ setSearchMode(false);
3092
+ }, [tableProvs, effTableProvider]);
3093
+ function openAddAccount(defaults) {
3094
+ const providerId = defaults?.providerId ?? (detected[0] ?? "claude");
3095
+ setAccountForm({
3096
+ mode: "add",
3097
+ field: "provider",
3098
+ providerId,
3099
+ name: defaults?.name ?? "",
3100
+ homeDir: defaults?.homeDir ?? "~",
3101
+ color: defaults?.color ?? pickAccentColor(cfg.accounts),
3102
+ caret: defaults?.name?.length ?? 0,
3103
+ editingId: null,
3104
+ error: null
3105
+ });
3106
+ }
3107
+ function openConfigureAccount(row) {
3108
+ openAddAccount(row);
3109
+ }
3110
+ function openEditAccount(acc) {
3111
+ setAccountForm({
3112
+ mode: "edit",
3113
+ field: "provider",
3114
+ providerId: acc.providerId,
3115
+ name: acc.name,
3116
+ homeDir: acc.homeDir,
3117
+ color: acc.color || PROVIDERS[acc.providerId].color,
3118
+ caret: acc.name.length,
3119
+ editingId: acc.id,
3120
+ error: null
3121
+ });
3122
+ }
3123
+ function commitAccountForm() {
3124
+ if (!accountForm) return;
3125
+ const name = accountForm.name.trim();
3126
+ const homeDir = accountForm.homeDir.trim() || "~";
3127
+ if (!name) {
3128
+ setAccountForm({ ...accountForm, error: "Name required", field: "name", caret: accountForm.name.length });
3129
+ return;
3130
+ }
3131
+ updateConfig((c) => {
3132
+ if (accountForm.mode === "add") {
3133
+ const id = generateAccountId(name, c.accounts);
3134
+ const account = { id, providerId: accountForm.providerId, name, homeDir, color: accountForm.color };
3135
+ return { ...c, accounts: [...c.accounts, account] };
3136
+ }
3137
+ return {
3138
+ ...c,
3139
+ accounts: c.accounts.map((a) => a.id === accountForm.editingId ? { ...a, providerId: accountForm.providerId, name, homeDir, color: accountForm.color } : a)
3140
+ };
3141
+ });
3142
+ setAccountForm(null);
3143
+ }
3144
+ const cycleFormField = useCallback((dir) => {
3145
+ setAccountForm((f) => {
3146
+ if (!f) return f;
3147
+ const i = FORM_FIELDS.indexOf(f.field);
3148
+ const next = FORM_FIELDS[(i + dir + FORM_FIELDS.length) % FORM_FIELDS.length];
3149
+ const caret = next === "name" ? f.name.length : next === "homeDir" ? f.homeDir.length : f.caret;
3150
+ return { ...f, field: next, caret };
3151
+ });
3152
+ }, []);
3153
+ const cycleProvider = useCallback((dir) => {
3154
+ setAccountForm((f) => {
3155
+ if (!f) return f;
3156
+ const i = PROVIDER_ORDER.indexOf(f.providerId);
3157
+ return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
3158
+ });
3159
+ }, []);
3160
+ const cycleColor = useCallback((dir) => {
3161
+ setAccountForm((f) => {
3162
+ if (!f) return f;
3163
+ const i = COLOR_PALETTE.indexOf(f.color);
3164
+ const idx = i < 0 ? 0 : i;
3165
+ return { ...f, color: COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length] };
3166
+ });
3167
+ }, []);
3168
+ function deleteAccount(id) {
3169
+ updateConfig((c) => ({
3170
+ ...c,
3171
+ accounts: c.accounts.filter((a) => a.id !== id),
3172
+ activeAccountId: c.activeAccountId === id ? null : c.activeAccountId
3173
+ }));
3174
+ }
3175
+ function moveAccount(idx, dir) {
3176
+ updateConfig((c) => {
3177
+ const next = [...c.accounts];
3178
+ const target = idx + dir;
3179
+ if (target < 0 || target >= next.length) return c;
3180
+ [next[idx], next[target]] = [next[target], next[idx]];
3181
+ return { ...c, accounts: next };
3182
+ });
3183
+ setSettingsCursor((c) => Math.max(0, Math.min(trackedAccountRows.length - 1, c + dir)));
3184
+ }
3185
+ async function toggleWeb() {
3186
+ if (connected) {
3187
+ if (baseUrl) openUrl(baseUrl);
3188
+ return;
3189
+ }
3190
+ if (webRef.current) {
3191
+ openUrl(webRef.current.url);
3192
+ return;
3193
+ }
3194
+ if (webStartingRef.current) return;
3195
+ webStartingRef.current = true;
3196
+ try {
3197
+ const { startWebServer } = await import("./server-RL2JDFQY.js");
3198
+ const ctrl = await startWebServer({ config: cfg, log: false });
3199
+ webRef.current = ctrl;
3200
+ openUrl(ctrl.url);
3201
+ } catch {
3202
+ } finally {
3203
+ webStartingRef.current = false;
3204
+ }
3205
+ }
3206
+ const onTabSelect = useCallback((i) => {
3207
+ setTab(i);
3208
+ resetView();
3209
+ }, [resetView]);
3210
+ const onStripSelect = useCallback((i) => {
3211
+ updateConfig((c) => ({ ...c, activeAccountId: slots[i].id }));
3212
+ resetView();
3213
+ }, [slots, updateConfig, resetView]);
3214
+ const onProviderSelect = useCallback((p) => {
3215
+ setTableProvider(p);
3216
+ setCursor(0);
3217
+ setExpanded(-1);
3218
+ setSearch("");
3219
+ setSearchCaret(0);
3220
+ setSearchMode(false);
3221
+ }, []);
3222
+ const onRowClickToken = useCallback((idx) => {
3223
+ if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
3224
+ else setCursor(idx);
3225
+ }, [cursor]);
3226
+ const onRowClickCursor = useCallback((idx) => setCursor(idx), []);
3227
+ const tokenRows = useMemo2(
3228
+ () => tab === 1 && !tableIsCursor ? sortRows(filterTokenRows(table ? [table.daily, table.weekly, table.monthly][view] : [], search), sort) : [],
3229
+ [tab, tableIsCursor, table, view, search, sort]
3230
+ );
3231
+ const cursorTableRows = useMemo2(
3232
+ () => tab === 1 && tableIsCursor ? sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort) : [],
3233
+ [tab, tableIsCursor, cursorRows, search, sort]
3234
+ );
3235
+ useInput((input, key) => handleKey(input, key, {
3236
+ showPicker,
3237
+ pickerProviders,
3238
+ onboardCursor,
3239
+ setOnboardCursor,
3240
+ toggleOnboard,
3241
+ confirmOnboarding,
3242
+ exit,
3243
+ showSettings,
3244
+ accountForm,
3245
+ setAccountForm,
3246
+ commitAccountForm,
3247
+ cycleFormField,
3248
+ cycleProvider,
3249
+ cycleColor,
3250
+ isPrintable,
3251
+ insertText,
3252
+ tzEdit,
3253
+ setTzEdit,
3254
+ setTzError,
3255
+ updateConfig,
3256
+ setTzCaret,
3257
+ tzValueRef,
3258
+ tzCaretRef,
3259
+ tab,
3260
+ searchMode,
3261
+ setSearchMode,
3262
+ search,
3263
+ setSearch,
3264
+ setSearchCaret,
3265
+ searchValueRef,
3266
+ searchCaretRef,
3267
+ showLoader,
3268
+ configReady,
3269
+ toggleWeb,
3270
+ settingsCursor,
3271
+ settingsTab,
3272
+ setSettingsTab,
3273
+ setShowSettings,
3274
+ cfg,
3275
+ trackedAccountRows,
3276
+ moveAccount,
3277
+ setSettingsCursor,
3278
+ toggleProvider,
3279
+ openEditAccount,
3280
+ openConfigureAccount,
3281
+ deleteAccount,
3282
+ openAddAccount,
3283
+ cycleAccount,
3284
+ setTab,
3285
+ resetView,
3286
+ slots,
3287
+ dashPaginated,
3288
+ dashPageCount,
3289
+ setDashPage,
3290
+ cycleTableProvider,
3291
+ setExpanded,
3292
+ setSort,
3293
+ SORTS_FOR,
3294
+ tableIsCursor,
3295
+ setView,
3296
+ cursor,
3297
+ rowCountRef,
3298
+ rows,
3299
+ setCursor,
3300
+ clampRow
3301
+ }), { isActive: IS_TTY });
3302
+ if (error) return /* @__PURE__ */ jsx11(Box11, { padding: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: error }) });
3303
+ if (!config) return /* @__PURE__ */ jsx11(Box11, { padding: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Loading..." }) });
3304
+ if (resizing) return /* @__PURE__ */ jsx11(ResizingView, { cols: live.cols, rows: live.rows });
3305
+ if (showPicker) {
3306
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx11(
3307
+ Onboarding,
3308
+ {
3309
+ items: onboardItems,
3310
+ cursor: onboardCursor,
3311
+ onToggle: toggleOnboard,
3312
+ onConfirm: confirmOnboarding,
3313
+ heading: needsOnboarding ? "Welcome to tokmon" : "New providers detected",
3314
+ subheading: needsOnboarding ? "Pick the tools you want to track. You can change this anytime in settings." : "tokmon found these installed since you last set up. Pick which to track."
3315
+ }
3316
+ ) });
3317
+ }
3318
+ if (showLoader) {
3319
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: /* @__PURE__ */ jsx11(LoadingView, { groups: allGroups, stats, cols, rows, readyInput: readyInputFor }) });
3320
+ }
3321
+ if (TOO_SMALL && !showSettings) {
3322
+ return /* @__PURE__ */ jsx11(TinyFallback, { groups, stats, rows, cols });
3323
+ }
3324
+ rowCountRef.current = tableIsCursor ? cursorTableRows.length : tokenRows.length;
3325
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
3326
+ /* @__PURE__ */ jsxs11(Box11, { justifyContent: "space-between", children: [
3327
+ /* @__PURE__ */ jsxs11(Box11, { children: [
3328
+ /* @__PURE__ */ jsxs11(Text11, { bold: true, color: "greenBright", children: [
3329
+ glyphs().dotSel,
3330
+ " tokmon"
3331
+ ] }),
3332
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3333
+ " ",
3334
+ glyphs().middot,
3335
+ " every ",
3336
+ intervalLabel,
3337
+ "s"
3338
+ ] })
3339
+ ] }),
3340
+ /* @__PURE__ */ jsxs11(Box11, { children: [
3341
+ peak && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3342
+ /* @__PURE__ */ jsx11(PeakBadge, { peak }),
3343
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3344
+ " ",
3345
+ glyphs().middot,
3346
+ " "
3347
+ ] })
3348
+ ] }),
3349
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: time(updated, tz) })
3350
+ ] })
3351
+ ] }),
3352
+ degraded && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3353
+ glyphs().warn,
3354
+ " degraded ",
3355
+ glyphs().middot,
3356
+ " background service unavailable, running in-process"
3357
+ ] }),
3358
+ connected && daemon.conn !== "live" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3359
+ glyphs().warn,
3360
+ " reconnecting ",
3361
+ glyphs().middot,
3362
+ " showing last known data"
3363
+ ] }),
3364
+ showSettings ? /* @__PURE__ */ jsx11(
3365
+ SettingsView,
3366
+ {
3367
+ config: cfg,
3368
+ cursor: settingsCursor,
3369
+ activeTab: settingsTab,
3370
+ tzEdit,
3371
+ tzCaret,
3372
+ tzError,
3373
+ resolvedTz: tz,
3374
+ accountForm,
3375
+ activeAccountId: cfg.activeAccountId,
3376
+ trackedAccounts: trackedAccountRows,
3377
+ accountIdentities
3378
+ }
3379
+ ) : /* @__PURE__ */ jsxs11(Fragment4, { children: [
3380
+ /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, marginBottom: 1, children: [
3381
+ /* @__PURE__ */ jsx11(TabBar, { tabs: TABS, active: tab, onSelect: onTabSelect }),
3382
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3383
+ " Tab/",
3384
+ glyphs().arrowL,
3385
+ glyphs().arrowR
3386
+ ] })
3387
+ ] }),
3388
+ tab === 0 && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3389
+ /* @__PURE__ */ jsx11(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage }),
3390
+ slots.length > 1 && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
3391
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "focus " }),
3392
+ /* @__PURE__ */ jsx11(
3393
+ AccountStrip,
3394
+ {
3395
+ slots,
3396
+ activeIdx: activeSlotIdx,
3397
+ onSelect: onStripSelect
3398
+ }
3399
+ )
3400
+ ] }),
3401
+ /* @__PURE__ */ jsx11(TotalsRow, { groups, stats, cols })
3402
+ ] }),
3403
+ tab === 1 && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3404
+ tableProvs.length > 0 && /* @__PURE__ */ jsx11(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: onProviderSelect }),
3405
+ /* @__PURE__ */ jsx11(Box11, { height: 1 }),
3406
+ /* @__PURE__ */ jsx11(
3407
+ ControlBar,
3408
+ {
3409
+ views: VIEWS,
3410
+ period: view,
3411
+ sort: sortLabel(SORTS_FOR[sort % SORTS_FOR.length]),
3412
+ search,
3413
+ searchCaret,
3414
+ searching: searchMode,
3415
+ showPeriod: !tableIsCursor
3416
+ }
3417
+ ),
3418
+ /* @__PURE__ */ jsx11(Box11, { height: 1 }),
3419
+ !effTableProvider ? /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3420
+ "No providers enabled ",
3421
+ glyphs().emDash,
3422
+ " press s to pick providers."
3423
+ ] }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx11(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx11(
3424
+ CursorSpendTable,
3425
+ {
3426
+ rows: cursorTableRows,
3427
+ cursor,
3428
+ maxRows: Math.max(1, rows - 16),
3429
+ onRowClick: onRowClickCursor
3430
+ }
3431
+ ) : /* @__PURE__ */ jsx11(
3432
+ TokenTable,
3433
+ {
3434
+ rows: tokenRows,
3435
+ cursor,
3436
+ expanded,
3437
+ maxRows: Math.max(1, rows - 16),
3438
+ cols,
3439
+ onRowClick: onRowClickToken
3440
+ }
3441
+ )
3442
+ ] })
3443
+ ] }),
3444
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx11(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols })
3445
+ ] });
3446
+ }
3447
+
3448
+ // src/bootstrap-ink.tsx
3449
+ import { jsx as jsx12 } from "react/jsx-runtime";
3450
+ function enterAltScreen() {
3451
+ process.stdout.write("\x1B[?1049h\x1B[H");
3452
+ }
3453
+ function leaveAltScreen() {
3454
+ try {
3455
+ process.stdout.write("\x1B[?1049l");
3456
+ } catch {
3457
+ }
3458
+ }
3459
+ function setupInputModes() {
3460
+ process.stdout.write("\x1B[?2004h\x1B[?1004l");
3461
+ }
3462
+ function restoreInputModes() {
3463
+ try {
3464
+ process.stdout.write("\x1B[?2004l");
3465
+ } catch {
3466
+ }
3467
+ }
3468
+ async function bootstrapInk({ interval, config, daemon, mode }) {
3469
+ const isTTY = process.stdout.isTTY === true;
3470
+ const altScreen = config.clearScreen && isTTY;
3471
+ if (altScreen) enterAltScreen();
3472
+ if (isTTY) setupInputModes();
3473
+ process.once("exit", () => {
3474
+ if (isTTY) restoreInputModes();
3475
+ if (altScreen) leaveAltScreen();
3476
+ });
3477
+ const { waitUntilExit } = render(
3478
+ /* @__PURE__ */ jsx12(MouseProvider, { children: /* @__PURE__ */ jsx12(
3479
+ App,
3480
+ {
3481
+ interval,
3482
+ initialConfig: config,
3483
+ baseUrl: daemon.baseUrl,
3484
+ wsToken: daemon.wsToken,
3485
+ mode
3486
+ }
3487
+ ) })
3488
+ );
3489
+ await waitUntilExit();
3490
+ daemon.stop();
3491
+ if (isTTY) restoreInputModes();
3492
+ if (altScreen) leaveAltScreen();
3493
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
3494
+ }
3495
+ export {
3496
+ bootstrapInk
3497
+ };