tokmon 0.20.6 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bootstrap-ink-WWSVQO6D.js → bootstrap-ink-P3PH2EV4.js} +621 -435
- package/dist/{chunk-YWJT3OG4.js → chunk-2PIYMU6W.js} +808 -516
- package/dist/{chunk-XQEJ4WQ5.js → chunk-7MFZMI5C.js} +2 -2
- package/dist/{chunk-E6YT66ST.js → chunk-AT2OT3AT.js} +72 -161
- package/dist/cli.js +4 -4
- package/dist/{config-C6Z65JUP.js → config-UPNBGIJO.js} +1 -1
- package/dist/{daemon-ZUWJQEKA.js → daemon-CMY7V37B.js} +3 -3
- package/dist/server-GEGTLRUW.js +9 -0
- package/dist/web/assets/breakdown-DyImok0q.js +4 -0
- package/dist/web/assets/index-CQGWJ0OP.js +173 -0
- package/dist/web/assets/{index-CZOCHnad.css → index-D9ioiqrk.css} +1 -1
- package/dist/web/assets/timeline-mhVcXkJg.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/server-TSTBMVTJ.js +0 -9
- package/dist/web/assets/breakdown-DFnPYZtA.js +0 -4
- package/dist/web/assets/chart-C8J22kR3.js +0 -69
- package/dist/web/assets/index-B9eW55YB.js +0 -105
- package/dist/web/assets/timeline-BjoaOdbh.js +0 -2
|
@@ -13,12 +13,13 @@ import {
|
|
|
13
13
|
cursorModelSpend,
|
|
14
14
|
detectProviders,
|
|
15
15
|
fetchPeak,
|
|
16
|
+
modelColor,
|
|
16
17
|
resolveTimezone,
|
|
17
18
|
shortDate,
|
|
18
19
|
systemTimezone,
|
|
19
20
|
time,
|
|
20
21
|
tokens
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-2PIYMU6W.js";
|
|
22
23
|
import {
|
|
23
24
|
COLOR_PALETTE,
|
|
24
25
|
DEFAULTS,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
sanitizeTyped,
|
|
33
34
|
saveConfigSync,
|
|
34
35
|
snapshotCacheFile
|
|
35
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-7MFZMI5C.js";
|
|
36
37
|
import {
|
|
37
38
|
glyphs
|
|
38
39
|
} from "./chunk-RF4GGQGM.js";
|
|
@@ -42,8 +43,8 @@ import { render } from "ink";
|
|
|
42
43
|
import { MouseProvider } from "@zenobius/ink-mouse";
|
|
43
44
|
|
|
44
45
|
// src/app.tsx
|
|
45
|
-
import { useState as
|
|
46
|
-
import { Box as Box11, Text as Text11, useInput,
|
|
46
|
+
import { useState as useState8, useEffect as useEffect7, useCallback as useCallback4, useRef as useRef6, useMemo as useMemo2 } from "react";
|
|
47
|
+
import { Box as Box11, Text as Text11, useInput, useApp } from "ink";
|
|
47
48
|
import { useMouse } from "@zenobius/ink-mouse";
|
|
48
49
|
|
|
49
50
|
// src/config-sync.ts
|
|
@@ -80,22 +81,6 @@ function reconcileDaemonConfig(previous, daemonConfig, pendingLocalConfig) {
|
|
|
80
81
|
return { config: daemonConfig, pendingLocalConfig: null };
|
|
81
82
|
}
|
|
82
83
|
|
|
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
84
|
// src/ui/shared.tsx
|
|
100
85
|
import { appendFileSync } from "fs";
|
|
101
86
|
import { memo, useEffect, useRef, useState } from "react";
|
|
@@ -274,18 +259,42 @@ var CARD_H = { full: 14, compact: 12, mini: 8 };
|
|
|
274
259
|
var VARIANT_ORDER = ["full", "compact", "mini"];
|
|
275
260
|
var INDICATOR_ROWS = 1;
|
|
276
261
|
var MAX_SINGLE_CARD = Math.round(MIN_CARD * 1.6);
|
|
277
|
-
function
|
|
262
|
+
function estimateCardHeights(groups, stats) {
|
|
263
|
+
return groups.map((g) => {
|
|
264
|
+
const meta = PROVIDERS[g.provider];
|
|
265
|
+
let h = 3;
|
|
266
|
+
if (meta.hasUsage) h += 5;
|
|
267
|
+
if (meta.hasBilling) {
|
|
268
|
+
if (meta.hasUsage) h += 1;
|
|
269
|
+
const multi = g.accounts.length > 1;
|
|
270
|
+
g.accounts.forEach((a, i) => {
|
|
271
|
+
const metricRows = stats.get(a.id)?.billing?.metrics.length || 1;
|
|
272
|
+
h += metricRows + (multi ? 1 : 0) + (multi && i > 0 ? 1 : 0);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
h += 2;
|
|
276
|
+
return Math.max(h, CARD_H.mini);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function chooseLayout(content, budget, n, single, cols, heights) {
|
|
278
280
|
if (n <= 0) return { ncols: 1, variant: "mini", cardsPerPage: 1, pageCount: 1 };
|
|
281
|
+
const heightFor = (variant) => {
|
|
282
|
+
if (!heights || heights.length === 0) return CARD_H[variant];
|
|
283
|
+
if (variant === "mini") return CARD_H.mini;
|
|
284
|
+
const hs = heights.map((h) => variant === "full" ? h : Math.max(h - 2, CARD_H.mini - 2));
|
|
285
|
+
return Math.max(...hs);
|
|
286
|
+
};
|
|
279
287
|
const gridHeight = (rows, H2) => rows * H2 + Math.max(0, rows - 1);
|
|
280
288
|
const colCap = single ? 1 : cols >= 3 * MIN_CARD_DENSE + 2 * GAP ? 3 : cols >= 2 * MIN_CARD + GAP ? 2 : 1;
|
|
281
289
|
const maxCols = Math.max(1, Math.min(colCap, n));
|
|
282
290
|
const cardWidthAt = (nc) => nc <= 1 ? content : Math.floor((content - GAP * (nc - 1)) / nc);
|
|
283
291
|
const minWidthAt = (nc) => nc >= 3 ? MIN_CARD_DENSE : MIN_CARD;
|
|
284
292
|
for (const variant of VARIANT_ORDER) {
|
|
293
|
+
const H2 = heightFor(variant);
|
|
285
294
|
for (let nc = maxCols; nc >= 1; nc--) {
|
|
286
295
|
if (nc > 1 && cardWidthAt(nc) < minWidthAt(nc)) continue;
|
|
287
296
|
const rows = Math.ceil(n / nc);
|
|
288
|
-
if (gridHeight(rows,
|
|
297
|
+
if (gridHeight(rows, H2) <= budget) {
|
|
289
298
|
return { ncols: nc, variant, cardsPerPage: n, pageCount: 1 };
|
|
290
299
|
}
|
|
291
300
|
}
|
|
@@ -304,6 +313,16 @@ function chooseLayout(content, budget, n, single, cols) {
|
|
|
304
313
|
const pageCount = Math.max(1, Math.ceil(n / cardsPerPage));
|
|
305
314
|
return { ncols, variant: "mini", cardsPerPage, pageCount };
|
|
306
315
|
}
|
|
316
|
+
function computeDashLayout(groups, stats, cols, budget, focusId, layoutPref) {
|
|
317
|
+
const content = Math.max(30, cols - 4);
|
|
318
|
+
const heights = estimateCardHeights(groups, stats);
|
|
319
|
+
const single = focusId !== null || layoutPref === "single";
|
|
320
|
+
if (layoutPref === "single" && focusId === null && groups.length > 1) {
|
|
321
|
+
const one = chooseLayout(content, budget, 1, true, cols, [Math.max(...heights)]);
|
|
322
|
+
return { ...one, cardsPerPage: 1, pageCount: groups.length };
|
|
323
|
+
}
|
|
324
|
+
return chooseLayout(content, budget, groups.length, single, cols, heights);
|
|
325
|
+
}
|
|
307
326
|
var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget, focusId, layout, page = 0 }) {
|
|
308
327
|
if (groups.length === 0) {
|
|
309
328
|
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
@@ -312,17 +331,14 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
|
|
|
312
331
|
" press s to pick providers."
|
|
313
332
|
] });
|
|
314
333
|
}
|
|
315
|
-
|
|
316
|
-
|
|
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);
|
|
334
|
+
const content = Math.max(30, cols - 4);
|
|
335
|
+
const { ncols, variant, cardsPerPage, pageCount } = computeDashLayout(groups, stats, cols, budget, focusId, layout);
|
|
320
336
|
let cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
|
|
321
337
|
if (ncols === 1 && cardW > MAX_SINGLE_CARD) cardW = MAX_SINGLE_CARD;
|
|
322
338
|
const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
|
|
323
|
-
const visible = pageCount > 1 ?
|
|
339
|
+
const visible = pageCount > 1 ? groups.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : groups;
|
|
324
340
|
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)) }),
|
|
341
|
+
/* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, alignItems: "flex-start", 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
342
|
pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
327
343
|
" ",
|
|
328
344
|
glyphs().middot,
|
|
@@ -346,7 +362,6 @@ function ProviderCard({ provider, accounts, stats, width, variant }) {
|
|
|
346
362
|
const plan = items.map((i) => i.s?.billing?.plan).find(Boolean) ?? null;
|
|
347
363
|
const activity = items.map((i) => i.s?.billing?.activity).find(Boolean) ?? null;
|
|
348
364
|
const inner = width - 4;
|
|
349
|
-
const barW = Math.max(10, Math.min(46, inner - 20));
|
|
350
365
|
const hasSpark = !!agg && agg.series.some((v) => v > 0);
|
|
351
366
|
const showBars = variant !== "mini";
|
|
352
367
|
const showSpark = variant === "full";
|
|
@@ -375,7 +390,7 @@ function ProviderCard({ provider, accounts, stats, width, variant }) {
|
|
|
375
390
|
] })),
|
|
376
391
|
meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
377
392
|
meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
|
|
378
|
-
/* @__PURE__ */ jsx2(LimitsBlock, { items,
|
|
393
|
+
/* @__PURE__ */ jsx2(LimitsBlock, { items, inner })
|
|
379
394
|
] }),
|
|
380
395
|
meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items }),
|
|
381
396
|
hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
@@ -399,7 +414,7 @@ function CompactBilling({ items }) {
|
|
|
399
414
|
glyphs().ellipsis
|
|
400
415
|
] });
|
|
401
416
|
if (billing.error) return /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error });
|
|
402
|
-
const m = billing.metrics[0];
|
|
417
|
+
const m = billing.metrics.find((x) => x.primary) ?? billing.metrics[0];
|
|
403
418
|
if (!m) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" });
|
|
404
419
|
return /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) });
|
|
405
420
|
}
|
|
@@ -443,8 +458,15 @@ function KpiLine({ agg }) {
|
|
|
443
458
|
] })
|
|
444
459
|
] });
|
|
445
460
|
}
|
|
446
|
-
function
|
|
461
|
+
function accountTitle(account, billing) {
|
|
462
|
+
const email = billing?.email;
|
|
463
|
+
return email && !account.name.includes("@") ? `${account.name} ${email}` : account.name;
|
|
464
|
+
}
|
|
465
|
+
function LimitsBlock({ items, inner }) {
|
|
447
466
|
const showName = items.length > 1;
|
|
467
|
+
const labels = items.flatMap((i) => i.s?.billing?.metrics ?? []).map((m) => m.label.length);
|
|
468
|
+
const labelW = Math.min(Math.max(7, ...labels) + 1, 14);
|
|
469
|
+
const barW = Math.max(10, Math.min(46, inner - labelW - 13));
|
|
448
470
|
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
|
|
449
471
|
const billing = s?.billing;
|
|
450
472
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
|
|
@@ -453,20 +475,20 @@ function LimitsBlock({ items, barW }) {
|
|
|
453
475
|
glyphs().dot,
|
|
454
476
|
" "
|
|
455
477
|
] }),
|
|
456
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.
|
|
478
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(accountTitle(account, billing), Math.max(22, inner - 2)) })
|
|
457
479
|
] }),
|
|
458
480
|
!billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
459
481
|
"Fetching",
|
|
460
482
|
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}`))
|
|
483
|
+
] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", wrap: "truncate-end", 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, labelW }, `${m.label}${i}`))
|
|
462
484
|
] }, account.id);
|
|
463
485
|
}) });
|
|
464
486
|
}
|
|
465
|
-
function MetricRow({ m, color, barW }) {
|
|
487
|
+
function MetricRow({ m, color, barW, labelW }) {
|
|
466
488
|
if (m.format.kind === "percent") {
|
|
467
489
|
const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
|
|
468
490
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
469
|
-
/* @__PURE__ */ jsx2(Box2, { width:
|
|
491
|
+
/* @__PURE__ */ jsx2(Box2, { width: labelW, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
|
|
470
492
|
/* @__PURE__ */ jsx2(Bar, { pct: m.used, color: barColor, width: barW }),
|
|
471
493
|
/* @__PURE__ */ jsx2(Box2, { width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
|
|
472
494
|
Math.round(m.used),
|
|
@@ -476,7 +498,7 @@ function MetricRow({ m, color, barW }) {
|
|
|
476
498
|
] });
|
|
477
499
|
}
|
|
478
500
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
479
|
-
/* @__PURE__ */ jsx2(Box2, { width:
|
|
501
|
+
/* @__PURE__ */ jsx2(Box2, { width: labelW, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
|
|
480
502
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) })
|
|
481
503
|
] });
|
|
482
504
|
}
|
|
@@ -1065,7 +1087,7 @@ var TableProviderBar = memo4(function TableProviderBar2({ providers, active, onS
|
|
|
1065
1087
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " p/P switch" })
|
|
1066
1088
|
] });
|
|
1067
1089
|
});
|
|
1068
|
-
var ControlBar = memo4(function ControlBar2({ views, period, sort, search, searchCaret, searching, showPeriod }) {
|
|
1090
|
+
var ControlBar = memo4(function ControlBar2({ views, period, sort, model, search, searchCaret, searching, showPeriod, showModel }) {
|
|
1069
1091
|
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1070
1092
|
showPeriod && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1071
1093
|
views.map((v, i) => /* @__PURE__ */ jsx4(Box4, { marginRight: 2, children: i === period ? /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
@@ -1082,6 +1104,15 @@ var ControlBar = memo4(function ControlBar2({ views, period, sort, search, searc
|
|
|
1082
1104
|
glyphs().middot,
|
|
1083
1105
|
" "
|
|
1084
1106
|
] }),
|
|
1107
|
+
showModel && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1108
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "model " }),
|
|
1109
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: model ? modelColor(model) : "cyan", children: model ?? "all" }),
|
|
1110
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1111
|
+
" m model ",
|
|
1112
|
+
glyphs().middot,
|
|
1113
|
+
" "
|
|
1114
|
+
] })
|
|
1115
|
+
] }),
|
|
1085
1116
|
searching ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1086
1117
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "/" }),
|
|
1087
1118
|
/* @__PURE__ */ jsx4(CaretText, { value: search, caret: searchCaret, color: "cyan" })
|
|
@@ -1568,6 +1599,9 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
|
|
|
1568
1599
|
const showOpt = IS_APPLE_TERMINAL && inner >= BASE + OPT;
|
|
1569
1600
|
const showJump = hasAccounts && inner >= BASE + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
|
|
1570
1601
|
const showPage = paginated && inner >= BASE + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
|
|
1602
|
+
if (inner < BASE) {
|
|
1603
|
+
return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, flexWrap: "nowrap", children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: "O=repo W=web s=settings q=quit" }) });
|
|
1604
|
+
}
|
|
1571
1605
|
return /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexWrap: "nowrap", children: [
|
|
1572
1606
|
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "by " }),
|
|
1573
1607
|
/* @__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" }) }) }),
|
|
@@ -2014,6 +2048,45 @@ function sortRows(rows, sortIdx) {
|
|
|
2014
2048
|
return sorted;
|
|
2015
2049
|
}
|
|
2016
2050
|
}
|
|
2051
|
+
function tableModelOptions(rows) {
|
|
2052
|
+
const models = /* @__PURE__ */ new Set();
|
|
2053
|
+
for (const row of rows) for (const model of row.breakdown) models.add(model.name);
|
|
2054
|
+
return [...models].sort();
|
|
2055
|
+
}
|
|
2056
|
+
function cycleTableModel(current, models, dir) {
|
|
2057
|
+
if (models.length === 0) return null;
|
|
2058
|
+
const idx = current ? models.indexOf(current) : -1;
|
|
2059
|
+
if (dir === 1) {
|
|
2060
|
+
if (idx < 0) return models[0];
|
|
2061
|
+
return idx === models.length - 1 ? null : models[idx + 1];
|
|
2062
|
+
}
|
|
2063
|
+
if (idx < 0) return models[models.length - 1];
|
|
2064
|
+
return idx === 0 ? null : models[idx - 1];
|
|
2065
|
+
}
|
|
2066
|
+
function filterRowsByModel(rows, model) {
|
|
2067
|
+
if (!model) return rows;
|
|
2068
|
+
return rows.flatMap((row) => {
|
|
2069
|
+
const detail = row.breakdown.find((m) => m.name === model);
|
|
2070
|
+
if (!detail) return [];
|
|
2071
|
+
const input = detail.input;
|
|
2072
|
+
const output = detail.output;
|
|
2073
|
+
const cacheCreate = detail.cacheCreate;
|
|
2074
|
+
const cacheRead = detail.cacheRead;
|
|
2075
|
+
return [{
|
|
2076
|
+
label: row.label,
|
|
2077
|
+
models: [model],
|
|
2078
|
+
input,
|
|
2079
|
+
output,
|
|
2080
|
+
cacheCreate,
|
|
2081
|
+
cacheRead,
|
|
2082
|
+
cacheSavings: detail.cacheSavings,
|
|
2083
|
+
total: input + output + cacheCreate + cacheRead,
|
|
2084
|
+
cost: detail.cost,
|
|
2085
|
+
count: detail.count,
|
|
2086
|
+
breakdown: [{ ...detail }]
|
|
2087
|
+
}];
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2017
2090
|
function filterTokenRows(rows, q) {
|
|
2018
2091
|
if (!q) return rows;
|
|
2019
2092
|
const s = q.toLowerCase();
|
|
@@ -2098,6 +2171,7 @@ function handleKey(input, key, ctx) {
|
|
|
2098
2171
|
setSort,
|
|
2099
2172
|
SORTS_FOR,
|
|
2100
2173
|
tableIsCursor,
|
|
2174
|
+
cycleTableModel: cycleTableModel2,
|
|
2101
2175
|
setView,
|
|
2102
2176
|
cursor,
|
|
2103
2177
|
rowCountRef,
|
|
@@ -2526,8 +2600,11 @@ function handleKey(input, key, ctx) {
|
|
|
2526
2600
|
return;
|
|
2527
2601
|
}
|
|
2528
2602
|
if (input === "m") {
|
|
2529
|
-
|
|
2530
|
-
|
|
2603
|
+
cycleTableModel2(1);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
if (input === "M") {
|
|
2607
|
+
cycleTableModel2(-1);
|
|
2531
2608
|
return;
|
|
2532
2609
|
}
|
|
2533
2610
|
if (key.leftArrow) {
|
|
@@ -2572,12 +2649,9 @@ function handleKey(input, key, ctx) {
|
|
|
2572
2649
|
}
|
|
2573
2650
|
}
|
|
2574
2651
|
|
|
2575
|
-
// src/
|
|
2576
|
-
import {
|
|
2577
|
-
|
|
2578
|
-
var LOADER_GRACE_MS = 600;
|
|
2579
|
-
var LOADER_MAX_MS = 8e3;
|
|
2580
|
-
var LOADER_MIN_VISIBLE_MS = 700;
|
|
2652
|
+
// src/ui/hooks/use-terminal-size.ts
|
|
2653
|
+
import { useState as useState4, useEffect as useEffect4 } from "react";
|
|
2654
|
+
import { useStdout } from "ink";
|
|
2581
2655
|
function useTerminalSize(settleMs = 90) {
|
|
2582
2656
|
const { stdout } = useStdout();
|
|
2583
2657
|
const read = () => ({ cols: stdout?.columns || 80, rows: stdout?.rows || 24 });
|
|
@@ -2606,234 +2680,98 @@ function useTerminalSize(settleMs = 90) {
|
|
|
2606
2680
|
}, [stdout, settleMs]);
|
|
2607
2681
|
return { cols: size.cols, rows: size.rows, resizing, live };
|
|
2608
2682
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
const [peakLocal, setPeak] = useState4(null);
|
|
2617
|
-
const [tableLocal, setTable] = useState4(null);
|
|
2618
|
-
const [tableLoading, setTableLoading] = useState4(false);
|
|
2619
|
-
const [error, setError] = useState4(null);
|
|
2620
|
-
const [updatedLocal, setUpdated] = useState4(/* @__PURE__ */ new Date());
|
|
2621
|
-
const [tab, setTab] = useState4(0);
|
|
2622
|
-
const [view, setView] = useState4(0);
|
|
2623
|
-
const [cursor, setCursor] = useState4(0);
|
|
2624
|
-
const [expanded, setExpanded] = useState4(-1);
|
|
2625
|
-
const [sort, setSort] = useState4(1);
|
|
2626
|
-
const [tableProvider, setTableProvider] = useState4(null);
|
|
2627
|
-
const [search, setSearch] = useState4("");
|
|
2628
|
-
const [searchMode, setSearchMode] = useState4(false);
|
|
2629
|
-
const [cursorRowsLocal, setCursorRows] = useState4(null);
|
|
2630
|
-
const [showSettings, setShowSettings] = useState4(false);
|
|
2631
|
-
const [settingsTab, setSettingsTab] = useState4("general");
|
|
2632
|
-
const [settingsCursor, setSettingsCursor] = useState4(0);
|
|
2633
|
-
const [tzEdit, setTzEdit] = useState4(null);
|
|
2634
|
-
const [tzError, setTzError] = useState4(null);
|
|
2635
|
-
const [tzCaret, setTzCaret] = useState4(0);
|
|
2636
|
-
const [searchCaret, setSearchCaret] = useState4(0);
|
|
2637
|
-
const [accountForm, setAccountForm] = useState4(null);
|
|
2638
|
-
const [onboardSel, setOnboardSel] = useState4(null);
|
|
2639
|
-
const [onboardCursor, setOnboardCursor] = useState4(0);
|
|
2640
|
-
const [dashPage, setDashPage] = useState4(0);
|
|
2641
|
-
const [debouncePassed, setDebouncePassed] = useState4(false);
|
|
2642
|
-
const [graceHold, setGraceHold] = useState4(false);
|
|
2643
|
-
const [loaderShownAt, setLoaderShownAt] = useState4(null);
|
|
2644
|
-
const loaderDone = useRef3(false);
|
|
2645
|
-
const [loaderDoneState, setLoaderDoneFlag] = useState4(false);
|
|
2646
|
-
const setLoaderDone = useCallback((v) => {
|
|
2647
|
-
loaderDone.current = v;
|
|
2648
|
-
setLoaderDoneFlag(v);
|
|
2649
|
-
}, []);
|
|
2650
|
-
const prevShowPicker = useRef3(false);
|
|
2651
|
-
const { exit } = useApp();
|
|
2652
|
-
const { cols, rows, resizing, live } = useTerminalSize();
|
|
2653
|
-
const webRef = useRef3(null);
|
|
2654
|
-
const webStartingRef = useRef3(false);
|
|
2655
|
-
useEffect4(() => () => {
|
|
2656
|
-
void webRef.current?.stop();
|
|
2657
|
-
}, []);
|
|
2658
|
-
const cfg = config ?? DEFAULTS;
|
|
2659
|
-
const interval = cliInterval ?? cfg.interval * 1e3;
|
|
2660
|
-
const billingMs = cfg.billingInterval * 6e4;
|
|
2661
|
-
const tz = resolveTimezone(cfg.timezone);
|
|
2662
|
-
const configReady = config !== null;
|
|
2663
|
-
const accounts = useMemo2(() => buildAccounts(cfg, detected), [cfg, detected]);
|
|
2664
|
-
const trackedAccountRows = useMemo2(() => getTrackedAccountRows(cfg, detected, accounts), [cfg, detected, accounts]);
|
|
2665
|
-
const settingsRowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
|
|
2666
|
-
const accountsRef = useRef3([]);
|
|
2667
|
-
accountsRef.current = accounts;
|
|
2668
|
-
const rowCountRef = useRef3(0);
|
|
2669
|
-
const tabRef = useRef3(0);
|
|
2670
|
-
tabRef.current = tab;
|
|
2671
|
-
const dashPageCountRef = useRef3(1);
|
|
2672
|
-
const seededRef = useRef3(false);
|
|
2683
|
+
|
|
2684
|
+
// src/ui/hooks/use-paste.ts
|
|
2685
|
+
import { useCallback, useRef as useRef3 } from "react";
|
|
2686
|
+
var PASTE_START = "\x1B[200~";
|
|
2687
|
+
var PASTE_END = "\x1B[201~";
|
|
2688
|
+
var PASTE_MAX = 1 << 20;
|
|
2689
|
+
function usePaste(onInsert) {
|
|
2673
2690
|
const pasteBufRef = useRef3(null);
|
|
2674
2691
|
const pasteCarryRef = useRef3("");
|
|
2675
|
-
const pendingLocalConfigRef = useRef3(null);
|
|
2676
2692
|
const insertPasteRef = useRef3(() => {
|
|
2677
2693
|
});
|
|
2678
|
-
|
|
2679
|
-
const
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2694
|
+
insertPasteRef.current = onInsert;
|
|
2695
|
+
const handlePasteData = useCallback((chunk) => {
|
|
2696
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2697
|
+
if (pasteBufRef.current !== null) {
|
|
2698
|
+
const combined2 = pasteBufRef.current + s;
|
|
2699
|
+
const end2 = combined2.indexOf(PASTE_END);
|
|
2700
|
+
if (end2 === -1) {
|
|
2701
|
+
if (combined2.length >= PASTE_MAX) {
|
|
2702
|
+
const clean3 = sanitizeTyped(combined2);
|
|
2703
|
+
pasteBufRef.current = null;
|
|
2704
|
+
if (clean3) insertPasteRef.current(clean3);
|
|
2705
|
+
return true;
|
|
2706
|
+
}
|
|
2707
|
+
pasteBufRef.current = combined2;
|
|
2708
|
+
return true;
|
|
2709
|
+
}
|
|
2710
|
+
const clean2 = sanitizeTyped(combined2.slice(0, end2));
|
|
2711
|
+
pasteBufRef.current = null;
|
|
2712
|
+
if (clean2) insertPasteRef.current(clean2);
|
|
2713
|
+
return end2 + PASTE_END.length >= combined2.length;
|
|
2698
2714
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
[connected, snapshot, updatedLocal]
|
|
2706
|
-
);
|
|
2707
|
-
const intervalLabel = connected && snapshot?.intervalMs ? Math.round(snapshot.intervalMs / 1e3) : cfg.interval;
|
|
2708
|
-
const readyInputFor = useCallback((id) => {
|
|
2709
|
-
if (connected) {
|
|
2710
|
-
const wa = snapshot?.accounts.find((a) => a.id === id);
|
|
2711
|
-
if (!wa) return void 0;
|
|
2712
|
-
return { summaryState: wa.summaryState, billingState: wa.billingState, billing: wa.billing };
|
|
2715
|
+
const combined = pasteCarryRef.current + s;
|
|
2716
|
+
const start = combined.indexOf(PASTE_START);
|
|
2717
|
+
if (start === -1) {
|
|
2718
|
+
const keep = Math.min(combined.length, PASTE_START.length - 1);
|
|
2719
|
+
pasteCarryRef.current = combined.slice(combined.length - keep);
|
|
2720
|
+
return false;
|
|
2713
2721
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
);
|
|
2721
|
-
const visibleAccounts = useMemo2(
|
|
2722
|
-
() => focusId === null ? accounts : accounts.filter((a) => a.id === focusId),
|
|
2723
|
-
[accounts, focusId]
|
|
2724
|
-
);
|
|
2725
|
-
const allGroups = useMemo2(() => accountsByProvider(accounts), [accounts]);
|
|
2726
|
-
const groups = useMemo2(
|
|
2727
|
-
() => focusId === null ? allGroups : accountsByProvider(visibleAccounts),
|
|
2728
|
-
[allGroups, visibleAccounts, focusId]
|
|
2729
|
-
);
|
|
2730
|
-
const tableProvs = useMemo2(() => allGroups.map((g) => g.provider), [allGroups]);
|
|
2731
|
-
const TOO_SMALL = cols < 40 || rows < 12;
|
|
2732
|
-
const allReady = accounts.length > 0 && accounts.every((a) => accountReady(readyInputFor(a.id), a.providerId));
|
|
2733
|
-
const { gridBudget } = useMemo2(() => computeChrome(slots, cols, rows), [slots, cols, rows]);
|
|
2734
|
-
const dashLayout = useMemo2(() => chooseLayout(
|
|
2735
|
-
Math.max(56, cols - 4),
|
|
2736
|
-
gridBudget,
|
|
2737
|
-
groups.length,
|
|
2738
|
-
focusId !== null || cfg.dashboardLayout === "single",
|
|
2739
|
-
cols
|
|
2740
|
-
), [cols, gridBudget, groups.length, focusId, cfg.dashboardLayout]);
|
|
2741
|
-
const dashPageCount = dashLayout.pageCount;
|
|
2742
|
-
const dashPaginated = dashPageCount > 1;
|
|
2743
|
-
dashPageCountRef.current = dashPageCount;
|
|
2744
|
-
tzValueRef.current = tzEdit ?? "";
|
|
2745
|
-
tzCaretRef.current = clampCaret(tzCaret, (tzEdit ?? "").length);
|
|
2746
|
-
searchValueRef.current = search;
|
|
2747
|
-
searchCaretRef.current = clampCaret(searchCaret, search.length);
|
|
2748
|
-
const isPrintable = (input, key) => !!input && !key.ctrl && !key.meta && !isPasteInput(input);
|
|
2749
|
-
const insertText = (text) => {
|
|
2750
|
-
if (showSettings && accountForm && (accountForm.field === "name" || accountForm.field === "homeDir")) {
|
|
2751
|
-
setAccountForm((f) => {
|
|
2752
|
-
if (!f || f.field !== "name" && f.field !== "homeDir") return f;
|
|
2753
|
-
const r = spliceInsert(f[f.field], f.caret, text);
|
|
2754
|
-
return { ...f, [f.field]: r.value, caret: r.caret, error: null };
|
|
2755
|
-
});
|
|
2756
|
-
} else if (showSettings && tzEdit !== null) {
|
|
2757
|
-
const r = spliceInsert(tzValueRef.current, tzCaretRef.current, text);
|
|
2758
|
-
tzValueRef.current = r.value;
|
|
2759
|
-
tzCaretRef.current = r.caret;
|
|
2760
|
-
setTzEdit(r.value);
|
|
2761
|
-
setTzCaret(r.caret);
|
|
2762
|
-
setTzError(null);
|
|
2763
|
-
} else if (tab === 1 && searchMode) {
|
|
2764
|
-
const r = spliceInsert(searchValueRef.current, searchCaretRef.current, text);
|
|
2765
|
-
searchValueRef.current = r.value;
|
|
2766
|
-
searchCaretRef.current = r.caret;
|
|
2767
|
-
setSearch(r.value);
|
|
2768
|
-
setSearchCaret(r.caret);
|
|
2722
|
+
pasteCarryRef.current = "";
|
|
2723
|
+
const rest = combined.slice(start + PASTE_START.length);
|
|
2724
|
+
const end = rest.indexOf(PASTE_END);
|
|
2725
|
+
if (end === -1) {
|
|
2726
|
+
pasteBufRef.current = rest;
|
|
2727
|
+
return true;
|
|
2769
2728
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
[
|
|
2777
|
-
);
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
const
|
|
2789
|
-
const
|
|
2790
|
-
const
|
|
2729
|
+
const clean = sanitizeTyped(rest.slice(0, end));
|
|
2730
|
+
if (clean) insertPasteRef.current(clean);
|
|
2731
|
+
return true;
|
|
2732
|
+
}, []);
|
|
2733
|
+
const isPasteInput = useCallback((input) => {
|
|
2734
|
+
if (pasteBufRef.current !== null) return true;
|
|
2735
|
+
return input.includes("[200~") || input.includes("[201~");
|
|
2736
|
+
}, []);
|
|
2737
|
+
return { handlePasteData, isPasteInput };
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// src/ui/hooks/use-loader.ts
|
|
2741
|
+
import { useState as useState5, useEffect as useEffect5, useCallback as useCallback2, useRef as useRef4 } from "react";
|
|
2742
|
+
var DEBOUNCE_MS = 300;
|
|
2743
|
+
var LOADER_GRACE_MS = 600;
|
|
2744
|
+
var LOADER_MAX_MS = 8e3;
|
|
2745
|
+
var LOADER_MIN_VISIBLE_MS = 700;
|
|
2746
|
+
function useLoader({ configReady, showPicker, accountsKey, allReady, tooSmall, showSettings, accountsCount }) {
|
|
2747
|
+
const [debouncePassed, setDebouncePassed] = useState5(false);
|
|
2748
|
+
const [graceHold, setGraceHold] = useState5(false);
|
|
2749
|
+
const [loaderShownAt, setLoaderShownAt] = useState5(null);
|
|
2750
|
+
const loaderDone = useRef4(false);
|
|
2751
|
+
const [loaderDoneState, setLoaderDoneFlag] = useState5(false);
|
|
2752
|
+
const setLoaderDone = useCallback2((v) => {
|
|
2753
|
+
loaderDone.current = v;
|
|
2754
|
+
setLoaderDoneFlag(v);
|
|
2755
|
+
}, []);
|
|
2756
|
+
const prevShowPicker = useRef4(false);
|
|
2757
|
+
const resetLoader = useCallback2(() => {
|
|
2758
|
+
setLoaderDone(false);
|
|
2759
|
+
setDebouncePassed(false);
|
|
2760
|
+
setGraceHold(false);
|
|
2761
|
+
setLoaderShownAt(null);
|
|
2762
|
+
}, [setLoaderDone]);
|
|
2791
2763
|
const minVisibleHold = loaderShownAt !== null && Date.now() - loaderShownAt < LOADER_MIN_VISIBLE_MS;
|
|
2792
|
-
const showLoader = configReady && !showPicker && !showSettings && !
|
|
2793
|
-
|
|
2794
|
-
const onboardEnabled = onboardSel ?? detected;
|
|
2795
|
-
const onboardItems = pickerProviders.map((pid) => ({
|
|
2796
|
-
id: pid,
|
|
2797
|
-
name: PROVIDERS[pid].name,
|
|
2798
|
-
color: PROVIDERS[pid].color,
|
|
2799
|
-
detected: detected.includes(pid),
|
|
2800
|
-
enabled: onboardEnabled.includes(pid)
|
|
2801
|
-
}));
|
|
2802
|
-
useEffect4(() => {
|
|
2764
|
+
const showLoader = configReady && !showPicker && !showSettings && !tooSmall && accountsCount > 0 && (!allReady || graceHold || minVisibleHold) && (debouncePassed || loaderShownAt !== null) && !loaderDoneState;
|
|
2765
|
+
useEffect5(() => {
|
|
2803
2766
|
const wasPicker = prevShowPicker.current;
|
|
2804
2767
|
prevShowPicker.current = showPicker;
|
|
2805
|
-
if (wasPicker && !showPicker)
|
|
2806
|
-
setLoaderDone(false);
|
|
2807
|
-
setDebouncePassed(false);
|
|
2808
|
-
setGraceHold(false);
|
|
2809
|
-
setLoaderShownAt(null);
|
|
2810
|
-
}
|
|
2768
|
+
if (wasPicker && !showPicker) resetLoader();
|
|
2811
2769
|
}, [showPicker]);
|
|
2812
|
-
|
|
2770
|
+
useEffect5(() => {
|
|
2813
2771
|
if (showLoader && loaderShownAt === null) setLoaderShownAt(Date.now());
|
|
2814
2772
|
}, [showLoader, loaderShownAt]);
|
|
2815
|
-
|
|
2816
|
-
if (!
|
|
2817
|
-
detectProviders().then(setDetected);
|
|
2818
|
-
}, []);
|
|
2819
|
-
useEffect4(() => {
|
|
2820
|
-
if (!degraded) return;
|
|
2821
|
-
if (seededRef.current || !configReady || showPicker || accounts.length === 0) return;
|
|
2822
|
-
seededRef.current = true;
|
|
2823
|
-
loadSeedSnapshot().then((snap) => {
|
|
2824
|
-
setStats((prev) => {
|
|
2825
|
-
if (prev.size > 0) return prev;
|
|
2826
|
-
const next = new Map(prev);
|
|
2827
|
-
for (const acc of accountsRef.current) {
|
|
2828
|
-
const s = snap[acc.id];
|
|
2829
|
-
if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
|
|
2830
|
-
}
|
|
2831
|
-
return next;
|
|
2832
|
-
});
|
|
2833
|
-
});
|
|
2834
|
-
}, [degraded, configReady, showPicker, accountsKey]);
|
|
2835
|
-
useEffect4(() => {
|
|
2836
|
-
if (!configReady || showPicker || accounts.length === 0) return;
|
|
2773
|
+
useEffect5(() => {
|
|
2774
|
+
if (!configReady || showPicker || accountsCount === 0) return;
|
|
2837
2775
|
if (allReady || loaderDone.current) return;
|
|
2838
2776
|
const debounce = setTimeout(() => setDebouncePassed(true), DEBOUNCE_MS);
|
|
2839
2777
|
const deadline = setTimeout(() => {
|
|
@@ -2845,7 +2783,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2845
2783
|
clearTimeout(deadline);
|
|
2846
2784
|
};
|
|
2847
2785
|
}, [configReady, showPicker, accountsKey]);
|
|
2848
|
-
|
|
2786
|
+
useEffect5(() => {
|
|
2849
2787
|
if (!allReady || loaderDone.current) return;
|
|
2850
2788
|
if (loaderShownAt === null) {
|
|
2851
2789
|
setLoaderDone(true);
|
|
@@ -2860,17 +2798,62 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2860
2798
|
}, hold);
|
|
2861
2799
|
return () => clearTimeout(t);
|
|
2862
2800
|
}, [allReady, loaderShownAt]);
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2801
|
+
return { showLoader, resetLoader };
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// src/ui/hooks/use-degraded-polling.ts
|
|
2805
|
+
import { useState as useState6, useEffect as useEffect6, useRef as useRef5 } from "react";
|
|
2806
|
+
|
|
2807
|
+
// src/client/seed-cache.ts
|
|
2808
|
+
import { readFile } from "fs/promises";
|
|
2809
|
+
async function loadSeedSnapshot() {
|
|
2810
|
+
try {
|
|
2811
|
+
const parsed = JSON.parse(await readFile(snapshotCacheFile(), "utf-8"));
|
|
2812
|
+
if (!parsed || !Array.isArray(parsed.accounts)) return {};
|
|
2813
|
+
const out = {};
|
|
2814
|
+
for (const a of parsed.accounts) {
|
|
2815
|
+
if (a.dashboard || a.billing) out[a.id] = { dashboard: a.dashboard ?? null, billing: a.billing ?? null };
|
|
2816
|
+
}
|
|
2817
|
+
return out;
|
|
2818
|
+
} catch {
|
|
2819
|
+
return {};
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/ui/hooks/use-degraded-polling.ts
|
|
2824
|
+
function useDegradedPolling({ degraded, configReady, showPicker, accountsKey, accountsRef, interval, billingMs, tz }) {
|
|
2825
|
+
const [statsLocal, setStats] = useState6(/* @__PURE__ */ new Map());
|
|
2826
|
+
const [peakLocal, setPeak] = useState6(null);
|
|
2827
|
+
const [error, setError] = useState6(null);
|
|
2828
|
+
const [updatedLocal, setUpdated] = useState6(/* @__PURE__ */ new Date());
|
|
2829
|
+
const seededRef = useRef5(false);
|
|
2830
|
+
useEffect6(() => {
|
|
2831
|
+
if (!degraded) return;
|
|
2832
|
+
if (seededRef.current || !configReady || showPicker || accountsRef.current.length === 0) return;
|
|
2833
|
+
seededRef.current = true;
|
|
2834
|
+
loadSeedSnapshot().then((snap) => {
|
|
2835
|
+
setStats((prev) => {
|
|
2836
|
+
if (prev.size > 0) return prev;
|
|
2837
|
+
const next = new Map(prev);
|
|
2838
|
+
for (const acc of accountsRef.current) {
|
|
2839
|
+
const s = snap[acc.id];
|
|
2840
|
+
if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
|
|
2841
|
+
}
|
|
2842
|
+
return next;
|
|
2843
|
+
});
|
|
2844
|
+
});
|
|
2845
|
+
}, [degraded, configReady, showPicker, accountsKey]);
|
|
2846
|
+
useEffect6(() => {
|
|
2847
|
+
if (!degraded || !configReady || showPicker) return;
|
|
2848
|
+
let active = true;
|
|
2849
|
+
let timer;
|
|
2850
|
+
const load = async () => {
|
|
2851
|
+
try {
|
|
2852
|
+
await Promise.all(accountsRef.current.map(async (acc) => {
|
|
2853
|
+
const provider = PROVIDERS[acc.providerId];
|
|
2854
|
+
if (!provider.hasUsage || !provider.fetchSummary) return;
|
|
2855
|
+
try {
|
|
2856
|
+
const dashboard = await provider.fetchSummary(acc, tz);
|
|
2874
2857
|
if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
|
|
2875
2858
|
} catch {
|
|
2876
2859
|
}
|
|
@@ -2889,7 +2872,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2889
2872
|
clearTimeout(timer);
|
|
2890
2873
|
};
|
|
2891
2874
|
}, [degraded, interval, tz, configReady, showPicker, accountsKey]);
|
|
2892
|
-
|
|
2875
|
+
useEffect6(() => {
|
|
2893
2876
|
if (!degraded || !configReady || showPicker) return;
|
|
2894
2877
|
let active = true;
|
|
2895
2878
|
let timer;
|
|
@@ -2900,7 +2883,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2900
2883
|
const provider = PROVIDERS[acc.providerId];
|
|
2901
2884
|
if (!provider.hasBilling || !provider.fetchBilling) return;
|
|
2902
2885
|
try {
|
|
2903
|
-
const billing = await provider.fetchBilling(acc);
|
|
2886
|
+
const billing = await provider.fetchBilling(acc, tz);
|
|
2904
2887
|
if (active) setStats((prev) => upsert(prev, acc, { billing }));
|
|
2905
2888
|
} catch {
|
|
2906
2889
|
}
|
|
@@ -2916,20 +2899,329 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2916
2899
|
active = false;
|
|
2917
2900
|
clearTimeout(timer);
|
|
2918
2901
|
};
|
|
2919
|
-
}, [degraded, billingMs, configReady, showPicker, accountsKey]);
|
|
2902
|
+
}, [degraded, billingMs, tz, configReady, showPicker, accountsKey]);
|
|
2903
|
+
return { statsLocal, peakLocal, updatedLocal, error };
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// src/ui/hooks/use-account-form.ts
|
|
2907
|
+
import { useState as useState7, useCallback as useCallback3 } from "react";
|
|
2908
|
+
function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSettingsCursor }) {
|
|
2909
|
+
const [accountForm, setAccountForm] = useState7(null);
|
|
2910
|
+
function openAddAccount(defaults) {
|
|
2911
|
+
const providerId = defaults?.providerId ?? (detected[0] ?? "claude");
|
|
2912
|
+
setAccountForm({
|
|
2913
|
+
mode: "add",
|
|
2914
|
+
field: "provider",
|
|
2915
|
+
providerId,
|
|
2916
|
+
name: defaults?.name ?? "",
|
|
2917
|
+
homeDir: defaults?.homeDir ?? "~",
|
|
2918
|
+
color: defaults?.color ?? pickAccentColor(cfg.accounts),
|
|
2919
|
+
caret: defaults?.name?.length ?? 0,
|
|
2920
|
+
editingId: null,
|
|
2921
|
+
error: null
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
function openConfigureAccount(row) {
|
|
2925
|
+
openAddAccount(row);
|
|
2926
|
+
}
|
|
2927
|
+
function openEditAccount(acc) {
|
|
2928
|
+
setAccountForm({
|
|
2929
|
+
mode: "edit",
|
|
2930
|
+
field: "provider",
|
|
2931
|
+
providerId: acc.providerId,
|
|
2932
|
+
name: acc.name,
|
|
2933
|
+
homeDir: acc.homeDir,
|
|
2934
|
+
color: acc.color || PROVIDERS[acc.providerId].color,
|
|
2935
|
+
caret: acc.name.length,
|
|
2936
|
+
editingId: acc.id,
|
|
2937
|
+
error: null
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
function commitAccountForm() {
|
|
2941
|
+
if (!accountForm) return;
|
|
2942
|
+
const name = accountForm.name.trim();
|
|
2943
|
+
const homeDir = accountForm.homeDir.trim() || "~";
|
|
2944
|
+
if (!name) {
|
|
2945
|
+
setAccountForm({ ...accountForm, error: "Name required", field: "name", caret: accountForm.name.length });
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
updateConfig((c) => {
|
|
2949
|
+
if (accountForm.mode === "add") {
|
|
2950
|
+
const id = generateAccountId(name, c.accounts);
|
|
2951
|
+
const account = { id, providerId: accountForm.providerId, name, homeDir, color: accountForm.color };
|
|
2952
|
+
return { ...c, accounts: [...c.accounts, account] };
|
|
2953
|
+
}
|
|
2954
|
+
return {
|
|
2955
|
+
...c,
|
|
2956
|
+
accounts: c.accounts.map((a) => a.id === accountForm.editingId ? { ...a, providerId: accountForm.providerId, name, homeDir, color: accountForm.color } : a)
|
|
2957
|
+
};
|
|
2958
|
+
});
|
|
2959
|
+
setAccountForm(null);
|
|
2960
|
+
}
|
|
2961
|
+
const cycleFormField = useCallback3((dir) => {
|
|
2962
|
+
setAccountForm((f) => {
|
|
2963
|
+
if (!f) return f;
|
|
2964
|
+
const i = FORM_FIELDS.indexOf(f.field);
|
|
2965
|
+
const next = FORM_FIELDS[(i + dir + FORM_FIELDS.length) % FORM_FIELDS.length];
|
|
2966
|
+
const caret = next === "name" ? f.name.length : next === "homeDir" ? f.homeDir.length : f.caret;
|
|
2967
|
+
return { ...f, field: next, caret };
|
|
2968
|
+
});
|
|
2969
|
+
}, []);
|
|
2970
|
+
const cycleProvider = useCallback3((dir) => {
|
|
2971
|
+
setAccountForm((f) => {
|
|
2972
|
+
if (!f) return f;
|
|
2973
|
+
const i = PROVIDER_ORDER.indexOf(f.providerId);
|
|
2974
|
+
return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
|
|
2975
|
+
});
|
|
2976
|
+
}, []);
|
|
2977
|
+
const cycleColor = useCallback3((dir) => {
|
|
2978
|
+
setAccountForm((f) => {
|
|
2979
|
+
if (!f) return f;
|
|
2980
|
+
const i = COLOR_PALETTE.indexOf(f.color);
|
|
2981
|
+
const idx = i < 0 ? 0 : i;
|
|
2982
|
+
return { ...f, color: COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length] };
|
|
2983
|
+
});
|
|
2984
|
+
}, []);
|
|
2985
|
+
function deleteAccount(id) {
|
|
2986
|
+
updateConfig((c) => ({
|
|
2987
|
+
...c,
|
|
2988
|
+
accounts: c.accounts.filter((a) => a.id !== id),
|
|
2989
|
+
activeAccountId: c.activeAccountId === id ? null : c.activeAccountId
|
|
2990
|
+
}));
|
|
2991
|
+
}
|
|
2992
|
+
function moveAccount(idx, dir) {
|
|
2993
|
+
updateConfig((c) => {
|
|
2994
|
+
const next = [...c.accounts];
|
|
2995
|
+
const target = idx + dir;
|
|
2996
|
+
if (target < 0 || target >= next.length) return c;
|
|
2997
|
+
[next[idx], next[target]] = [next[target], next[idx]];
|
|
2998
|
+
return { ...c, accounts: next };
|
|
2999
|
+
});
|
|
3000
|
+
setSettingsCursor((c) => Math.max(0, Math.min(trackedAccountRows.length - 1, c + dir)));
|
|
3001
|
+
}
|
|
3002
|
+
return {
|
|
3003
|
+
accountForm,
|
|
3004
|
+
setAccountForm,
|
|
3005
|
+
openAddAccount,
|
|
3006
|
+
openConfigureAccount,
|
|
3007
|
+
openEditAccount,
|
|
3008
|
+
commitAccountForm,
|
|
3009
|
+
cycleFormField,
|
|
3010
|
+
cycleProvider,
|
|
3011
|
+
cycleColor,
|
|
3012
|
+
deleteAccount,
|
|
3013
|
+
moveAccount
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
// src/app.tsx
|
|
3018
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3019
|
+
function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = null, mode = "degraded" }) {
|
|
3020
|
+
const connected = mode === "connected" && baseUrl !== null && wsToken !== null;
|
|
3021
|
+
const degraded = !connected;
|
|
3022
|
+
const daemon = useDaemon(connected ? baseUrl : null, connected ? wsToken : null);
|
|
3023
|
+
const [config, setConfig] = useState8(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
|
|
3024
|
+
const [detected, setDetected] = useState8([]);
|
|
3025
|
+
const [tableLocal, setTable] = useState8(null);
|
|
3026
|
+
const [tableLoading, setTableLoading] = useState8(false);
|
|
3027
|
+
const [tab, setTab] = useState8(0);
|
|
3028
|
+
const [view, setView] = useState8(0);
|
|
3029
|
+
const [cursor, setCursor] = useState8(0);
|
|
3030
|
+
const [expanded, setExpanded] = useState8(-1);
|
|
3031
|
+
const [sort, setSort] = useState8(1);
|
|
3032
|
+
const [tableProvider, setTableProvider] = useState8(null);
|
|
3033
|
+
const [tableModel, setTableModel] = useState8(null);
|
|
3034
|
+
const [search, setSearch] = useState8("");
|
|
3035
|
+
const [searchMode, setSearchMode] = useState8(false);
|
|
3036
|
+
const [cursorRowsLocal, setCursorRows] = useState8(null);
|
|
3037
|
+
const [showSettings, setShowSettings] = useState8(false);
|
|
3038
|
+
const [settingsTab, setSettingsTab] = useState8("general");
|
|
3039
|
+
const [settingsCursor, setSettingsCursor] = useState8(0);
|
|
3040
|
+
const [tzEdit, setTzEdit] = useState8(null);
|
|
3041
|
+
const [tzError, setTzError] = useState8(null);
|
|
3042
|
+
const [tzCaret, setTzCaret] = useState8(0);
|
|
3043
|
+
const [searchCaret, setSearchCaret] = useState8(0);
|
|
3044
|
+
const [onboardSel, setOnboardSel] = useState8(null);
|
|
3045
|
+
const [onboardCursor, setOnboardCursor] = useState8(0);
|
|
3046
|
+
const [dashPage, setDashPage] = useState8(0);
|
|
3047
|
+
const { exit } = useApp();
|
|
3048
|
+
const { cols, rows, resizing, live } = useTerminalSize();
|
|
3049
|
+
const webRef = useRef6(null);
|
|
3050
|
+
const webStartingRef = useRef6(false);
|
|
3051
|
+
useEffect7(() => () => {
|
|
3052
|
+
void webRef.current?.stop();
|
|
3053
|
+
}, []);
|
|
3054
|
+
const cfg = config ?? DEFAULTS;
|
|
3055
|
+
const interval = cliInterval ?? cfg.interval * 1e3;
|
|
3056
|
+
const billingMs = cfg.billingInterval * 6e4;
|
|
3057
|
+
const tz = resolveTimezone(cfg.timezone);
|
|
3058
|
+
const configReady = config !== null;
|
|
3059
|
+
const accounts = useMemo2(() => buildAccounts(cfg, detected), [cfg, detected]);
|
|
3060
|
+
const trackedAccountRows = useMemo2(() => getTrackedAccountRows(cfg, detected, accounts), [cfg, detected, accounts]);
|
|
3061
|
+
const settingsRowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
|
|
3062
|
+
const accountsRef = useRef6([]);
|
|
3063
|
+
accountsRef.current = accounts;
|
|
3064
|
+
const rowCountRef = useRef6(0);
|
|
3065
|
+
const tabRef = useRef6(0);
|
|
3066
|
+
tabRef.current = tab;
|
|
3067
|
+
const dashPageCountRef = useRef6(1);
|
|
3068
|
+
const pendingLocalConfigRef = useRef6(null);
|
|
3069
|
+
const tzValueRef = useRef6("");
|
|
3070
|
+
const tzCaretRef = useRef6(0);
|
|
3071
|
+
const searchValueRef = useRef6("");
|
|
3072
|
+
const searchCaretRef = useRef6(0);
|
|
3073
|
+
const accountsKey = useMemo2(() => accounts.map(acctKey).join("|"), [accounts]);
|
|
3074
|
+
const needsOnboarding = configReady && !cfg.onboarded;
|
|
3075
|
+
const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
|
|
3076
|
+
const showPicker = needsOnboarding || newProviders.length > 0;
|
|
3077
|
+
const { statsLocal, peakLocal, updatedLocal, error } = useDegradedPolling({
|
|
3078
|
+
degraded,
|
|
3079
|
+
configReady,
|
|
3080
|
+
showPicker,
|
|
3081
|
+
accountsKey,
|
|
3082
|
+
accountsRef,
|
|
3083
|
+
interval,
|
|
3084
|
+
billingMs,
|
|
3085
|
+
tz
|
|
3086
|
+
});
|
|
3087
|
+
const snapshot = daemon.snapshot;
|
|
3088
|
+
const stats = useMemo2(
|
|
3089
|
+
() => connected ? toStatsMap(snapshot, accounts) : statsLocal,
|
|
3090
|
+
[connected, snapshot, accounts, statsLocal]
|
|
3091
|
+
);
|
|
3092
|
+
const accountIdentities = useMemo2(() => {
|
|
3093
|
+
const out = /* @__PURE__ */ new Map();
|
|
3094
|
+
for (const [id, stat] of stats) {
|
|
3095
|
+
const billing = stat.billing;
|
|
3096
|
+
if (!billing) continue;
|
|
3097
|
+
out.set(id, {
|
|
3098
|
+
email: billing.email ?? null,
|
|
3099
|
+
displayName: billing.displayName ?? null,
|
|
3100
|
+
plan: billing.plan ?? null
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
return out;
|
|
3104
|
+
}, [stats]);
|
|
3105
|
+
const showPeak = accounts.some((a) => a.providerId === "claude");
|
|
3106
|
+
const peak = connected ? showPeak ? snapshot?.peak ?? null : null : peakLocal;
|
|
3107
|
+
const updated = useMemo2(
|
|
3108
|
+
() => connected ? new Date(snapshot?.generatedAt ?? Date.now()) : updatedLocal,
|
|
3109
|
+
[connected, snapshot, updatedLocal]
|
|
3110
|
+
);
|
|
3111
|
+
const intervalLabel = connected && snapshot?.intervalMs ? Math.round(snapshot.intervalMs / 1e3) : cfg.interval;
|
|
3112
|
+
const readyInputFor = useCallback4((id) => {
|
|
3113
|
+
if (connected) {
|
|
3114
|
+
const wa = snapshot?.accounts.find((a) => a.id === id);
|
|
3115
|
+
if (!wa) return void 0;
|
|
3116
|
+
return { summaryState: wa.summaryState, billingState: wa.billingState, billing: wa.billing };
|
|
3117
|
+
}
|
|
3118
|
+
return statsReadyInput(statsLocal.get(id));
|
|
3119
|
+
}, [connected, snapshot, statsLocal]);
|
|
3120
|
+
const slots = useMemo2(() => deriveSlots(accounts), [accounts]);
|
|
3121
|
+
const { activeSlotIdx, focusId } = useMemo2(
|
|
3122
|
+
() => findActiveSlot(slots, cfg.activeAccountId),
|
|
3123
|
+
[slots, cfg.activeAccountId]
|
|
3124
|
+
);
|
|
3125
|
+
const visibleAccounts = useMemo2(
|
|
3126
|
+
() => focusId === null ? accounts : accounts.filter((a) => a.id === focusId),
|
|
3127
|
+
[accounts, focusId]
|
|
3128
|
+
);
|
|
3129
|
+
const allGroups = useMemo2(() => accountsByProvider(accounts), [accounts]);
|
|
3130
|
+
const groups = useMemo2(
|
|
3131
|
+
() => focusId === null ? allGroups : accountsByProvider(visibleAccounts),
|
|
3132
|
+
[allGroups, visibleAccounts, focusId]
|
|
3133
|
+
);
|
|
3134
|
+
const tableProvs = useMemo2(() => allGroups.map((g) => g.provider), [allGroups]);
|
|
3135
|
+
const TOO_SMALL = cols < 40 || rows < 12;
|
|
3136
|
+
const allReady = accounts.length > 0 && accounts.every((a) => accountReady(readyInputFor(a.id), a.providerId));
|
|
3137
|
+
const { gridBudget } = useMemo2(() => computeChrome(slots, cols, rows), [slots, cols, rows]);
|
|
3138
|
+
const dashLayout = useMemo2(
|
|
3139
|
+
() => computeDashLayout(groups, stats, cols, gridBudget, focusId, cfg.dashboardLayout),
|
|
3140
|
+
[groups, stats, cols, gridBudget, focusId, cfg.dashboardLayout]
|
|
3141
|
+
);
|
|
3142
|
+
const dashPageCount = dashLayout.pageCount;
|
|
3143
|
+
const dashPaginated = dashPageCount > 1;
|
|
3144
|
+
dashPageCountRef.current = dashPageCount;
|
|
3145
|
+
tzValueRef.current = tzEdit ?? "";
|
|
3146
|
+
tzCaretRef.current = clampCaret(tzCaret, (tzEdit ?? "").length);
|
|
3147
|
+
searchValueRef.current = search;
|
|
3148
|
+
searchCaretRef.current = clampCaret(searchCaret, search.length);
|
|
3149
|
+
const isPrintable = (input, key) => !!input && !key.ctrl && !key.meta && !isPasteInput(input);
|
|
3150
|
+
const insertText = (text) => {
|
|
3151
|
+
if (showSettings && accountForm && (accountForm.field === "name" || accountForm.field === "homeDir")) {
|
|
3152
|
+
setAccountForm((f) => {
|
|
3153
|
+
if (!f || f.field !== "name" && f.field !== "homeDir") return f;
|
|
3154
|
+
const r = spliceInsert(f[f.field], f.caret, text);
|
|
3155
|
+
return { ...f, [f.field]: r.value, caret: r.caret, error: null };
|
|
3156
|
+
});
|
|
3157
|
+
} else if (showSettings && tzEdit !== null) {
|
|
3158
|
+
const r = spliceInsert(tzValueRef.current, tzCaretRef.current, text);
|
|
3159
|
+
tzValueRef.current = r.value;
|
|
3160
|
+
tzCaretRef.current = r.caret;
|
|
3161
|
+
setTzEdit(r.value);
|
|
3162
|
+
setTzCaret(r.caret);
|
|
3163
|
+
setTzError(null);
|
|
3164
|
+
} else if (tab === 1 && searchMode) {
|
|
3165
|
+
const r = spliceInsert(searchValueRef.current, searchCaretRef.current, text);
|
|
3166
|
+
searchValueRef.current = r.value;
|
|
3167
|
+
searchCaretRef.current = r.caret;
|
|
3168
|
+
setSearch(r.value);
|
|
3169
|
+
setSearchCaret(r.caret);
|
|
3170
|
+
}
|
|
3171
|
+
};
|
|
3172
|
+
const { handlePasteData, isPasteInput } = usePaste(insertText);
|
|
3173
|
+
const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
|
|
3174
|
+
const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
|
|
3175
|
+
const tableAccounts = useMemo2(
|
|
3176
|
+
() => effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [],
|
|
3177
|
+
[accounts, effTableProvider]
|
|
3178
|
+
);
|
|
3179
|
+
const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
|
|
3180
|
+
const tableAccountIds = useMemo2(() => tableAccounts.map((a) => a.id), [tableAccounts]);
|
|
3181
|
+
const table = useMemo2(
|
|
3182
|
+
() => connected ? pickTable(snapshot, tableAccountIds) : tableLocal,
|
|
3183
|
+
[connected, snapshot, tableAccountIds, tableLocal]
|
|
3184
|
+
);
|
|
3185
|
+
const cursorRows = useMemo2(
|
|
3186
|
+
() => connected ? toCursorRows(snapshot, tableAccounts[0]?.id) : cursorRowsLocal,
|
|
3187
|
+
[connected, snapshot, tableAccounts, cursorRowsLocal]
|
|
3188
|
+
);
|
|
3189
|
+
const { showLoader } = useLoader({
|
|
3190
|
+
configReady,
|
|
3191
|
+
showPicker,
|
|
3192
|
+
accountsKey,
|
|
3193
|
+
allReady,
|
|
3194
|
+
tooSmall: TOO_SMALL,
|
|
3195
|
+
showSettings,
|
|
3196
|
+
accountsCount: accounts.length
|
|
3197
|
+
});
|
|
3198
|
+
const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
|
|
3199
|
+
const onboardEnabled = onboardSel ?? detected;
|
|
3200
|
+
const onboardItems = pickerProviders.map((pid) => ({
|
|
3201
|
+
id: pid,
|
|
3202
|
+
name: PROVIDERS[pid].name,
|
|
3203
|
+
color: PROVIDERS[pid].color,
|
|
3204
|
+
detected: detected.includes(pid),
|
|
3205
|
+
enabled: onboardEnabled.includes(pid)
|
|
3206
|
+
}));
|
|
3207
|
+
useEffect7(() => {
|
|
3208
|
+
if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
|
|
3209
|
+
detectProviders().then(setDetected);
|
|
3210
|
+
}, []);
|
|
2920
3211
|
const tableKey = useMemo2(
|
|
2921
3212
|
() => `${effTableProvider}|${tableAccounts.map(acctKey).join(",")}|${tz}`,
|
|
2922
3213
|
[effTableProvider, tableAccounts, tz]
|
|
2923
3214
|
);
|
|
2924
|
-
|
|
3215
|
+
useEffect7(() => {
|
|
2925
3216
|
setTable(null);
|
|
2926
3217
|
setCursorRows(null);
|
|
2927
3218
|
setCursor(0);
|
|
2928
3219
|
setExpanded(-1);
|
|
3220
|
+
setTableModel(null);
|
|
2929
3221
|
setSort(tableIsCursor ? 0 : 1);
|
|
2930
3222
|
setTableLoading(false);
|
|
2931
3223
|
}, [tableKey]);
|
|
2932
|
-
|
|
3224
|
+
useEffect7(() => {
|
|
2933
3225
|
if (!degraded || tab !== 1 || !effTableProvider) return;
|
|
2934
3226
|
let active = true;
|
|
2935
3227
|
let timer;
|
|
@@ -2962,68 +3254,23 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2962
3254
|
clearTimeout(timer);
|
|
2963
3255
|
};
|
|
2964
3256
|
}, [degraded, tab, tableKey, interval]);
|
|
2965
|
-
|
|
3257
|
+
useEffect7(() => {
|
|
2966
3258
|
setCursor(0);
|
|
2967
3259
|
setExpanded(-1);
|
|
2968
|
-
}, [search]);
|
|
2969
|
-
|
|
3260
|
+
}, [search, tableModel]);
|
|
3261
|
+
useEffect7(() => {
|
|
2970
3262
|
setDashPage((p) => Math.min(p, dashPageCount - 1));
|
|
2971
3263
|
}, [dashPageCount]);
|
|
2972
|
-
|
|
3264
|
+
useEffect7(() => {
|
|
2973
3265
|
setSettingsCursor((c) => Math.max(-1, Math.min(c, settingsRowCount - 1)));
|
|
2974
3266
|
}, [settingsRowCount]);
|
|
2975
|
-
const resetView =
|
|
3267
|
+
const resetView = useCallback4(() => {
|
|
2976
3268
|
setCursor(0);
|
|
2977
3269
|
setExpanded(-1);
|
|
2978
3270
|
}, []);
|
|
2979
3271
|
const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
|
|
2980
|
-
const PASTE_START = "\x1B[200~";
|
|
2981
|
-
const PASTE_END = "\x1B[201~";
|
|
2982
|
-
const PASTE_MAX = 1 << 20;
|
|
2983
|
-
const handlePasteData = useCallback((chunk) => {
|
|
2984
|
-
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2985
|
-
if (pasteBufRef.current !== null) {
|
|
2986
|
-
const combined2 = pasteBufRef.current + s;
|
|
2987
|
-
const end2 = combined2.indexOf(PASTE_END);
|
|
2988
|
-
if (end2 === -1) {
|
|
2989
|
-
if (combined2.length >= PASTE_MAX) {
|
|
2990
|
-
const clean3 = sanitizeTyped(combined2);
|
|
2991
|
-
pasteBufRef.current = null;
|
|
2992
|
-
if (clean3) insertPasteRef.current(clean3);
|
|
2993
|
-
return true;
|
|
2994
|
-
}
|
|
2995
|
-
pasteBufRef.current = combined2;
|
|
2996
|
-
return true;
|
|
2997
|
-
}
|
|
2998
|
-
const clean2 = sanitizeTyped(combined2.slice(0, end2));
|
|
2999
|
-
pasteBufRef.current = null;
|
|
3000
|
-
if (clean2) insertPasteRef.current(clean2);
|
|
3001
|
-
return end2 + PASTE_END.length >= combined2.length;
|
|
3002
|
-
}
|
|
3003
|
-
const combined = pasteCarryRef.current + s;
|
|
3004
|
-
const start = combined.indexOf(PASTE_START);
|
|
3005
|
-
if (start === -1) {
|
|
3006
|
-
const keep = Math.min(combined.length, PASTE_START.length - 1);
|
|
3007
|
-
pasteCarryRef.current = combined.slice(combined.length - keep);
|
|
3008
|
-
return false;
|
|
3009
|
-
}
|
|
3010
|
-
pasteCarryRef.current = "";
|
|
3011
|
-
const rest = combined.slice(start + PASTE_START.length);
|
|
3012
|
-
const end = rest.indexOf(PASTE_END);
|
|
3013
|
-
if (end === -1) {
|
|
3014
|
-
pasteBufRef.current = rest;
|
|
3015
|
-
return true;
|
|
3016
|
-
}
|
|
3017
|
-
const clean = sanitizeTyped(rest.slice(0, end));
|
|
3018
|
-
if (clean) insertPasteRef.current(clean);
|
|
3019
|
-
return true;
|
|
3020
|
-
}, []);
|
|
3021
|
-
const isPasteInput = useCallback((input) => {
|
|
3022
|
-
if (pasteBufRef.current !== null) return true;
|
|
3023
|
-
return input.includes("[200~") || input.includes("[201~");
|
|
3024
|
-
}, []);
|
|
3025
3272
|
const mouse = useMouse();
|
|
3026
|
-
|
|
3273
|
+
useEffect7(() => {
|
|
3027
3274
|
if (!IS_TTY) return;
|
|
3028
3275
|
mouse.enable();
|
|
3029
3276
|
if (process.stdout.isTTY) {
|
|
@@ -3051,7 +3298,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3051
3298
|
process.stdin.off("data", onData);
|
|
3052
3299
|
};
|
|
3053
3300
|
}, []);
|
|
3054
|
-
const updateConfig =
|
|
3301
|
+
const updateConfig = useCallback4((fn) => {
|
|
3055
3302
|
setConfig((prev) => {
|
|
3056
3303
|
const next = normalizeConfig(fn(prev ?? DEFAULTS));
|
|
3057
3304
|
pendingLocalConfigRef.current = connected ? next : null;
|
|
@@ -3068,7 +3315,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3068
3315
|
});
|
|
3069
3316
|
}, [connected, daemon]);
|
|
3070
3317
|
const daemonConfig = daemon.config;
|
|
3071
|
-
|
|
3318
|
+
useEffect7(() => {
|
|
3072
3319
|
if (!connected || !daemonConfig) return;
|
|
3073
3320
|
setConfig((prev) => {
|
|
3074
3321
|
const reconciled = reconcileDaemonConfig(prev, daemonConfig, pendingLocalConfigRef.current);
|
|
@@ -3112,13 +3359,13 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3112
3359
|
setOnboardSel(null);
|
|
3113
3360
|
setOnboardCursor(0);
|
|
3114
3361
|
}
|
|
3115
|
-
const cycleAccount =
|
|
3362
|
+
const cycleAccount = useCallback4((dir) => {
|
|
3116
3363
|
if (slots.length <= 1) return;
|
|
3117
3364
|
const next = (activeSlotIdx + dir + slots.length) % slots.length;
|
|
3118
3365
|
updateConfig((c) => ({ ...c, activeAccountId: slots[next].id }));
|
|
3119
3366
|
resetView();
|
|
3120
3367
|
}, [slots, activeSlotIdx, updateConfig, resetView]);
|
|
3121
|
-
const cycleTableProvider =
|
|
3368
|
+
const cycleTableProvider = useCallback4((dir) => {
|
|
3122
3369
|
if (tableProvs.length <= 1) return;
|
|
3123
3370
|
const cur = effTableProvider ? tableProvs.indexOf(effTableProvider) : 0;
|
|
3124
3371
|
const nextProv = tableProvs[(cur + dir + tableProvs.length) % tableProvs.length];
|
|
@@ -3127,102 +3374,24 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3127
3374
|
setSort(nextIsCursor ? 0 : 1);
|
|
3128
3375
|
setCursor(0);
|
|
3129
3376
|
setExpanded(-1);
|
|
3377
|
+
setTableModel(null);
|
|
3130
3378
|
setSearch("");
|
|
3131
3379
|
setSearchCaret(0);
|
|
3132
3380
|
setSearchMode(false);
|
|
3133
3381
|
}, [tableProvs, effTableProvider]);
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
setAccountForm
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
}
|
|
3148
|
-
function openConfigureAccount(row) {
|
|
3149
|
-
openAddAccount(row);
|
|
3150
|
-
}
|
|
3151
|
-
function openEditAccount(acc) {
|
|
3152
|
-
setAccountForm({
|
|
3153
|
-
mode: "edit",
|
|
3154
|
-
field: "provider",
|
|
3155
|
-
providerId: acc.providerId,
|
|
3156
|
-
name: acc.name,
|
|
3157
|
-
homeDir: acc.homeDir,
|
|
3158
|
-
color: acc.color || PROVIDERS[acc.providerId].color,
|
|
3159
|
-
caret: acc.name.length,
|
|
3160
|
-
editingId: acc.id,
|
|
3161
|
-
error: null
|
|
3162
|
-
});
|
|
3163
|
-
}
|
|
3164
|
-
function commitAccountForm() {
|
|
3165
|
-
if (!accountForm) return;
|
|
3166
|
-
const name = accountForm.name.trim();
|
|
3167
|
-
const homeDir = accountForm.homeDir.trim() || "~";
|
|
3168
|
-
if (!name) {
|
|
3169
|
-
setAccountForm({ ...accountForm, error: "Name required", field: "name", caret: accountForm.name.length });
|
|
3170
|
-
return;
|
|
3171
|
-
}
|
|
3172
|
-
updateConfig((c) => {
|
|
3173
|
-
if (accountForm.mode === "add") {
|
|
3174
|
-
const id = generateAccountId(name, c.accounts);
|
|
3175
|
-
const account = { id, providerId: accountForm.providerId, name, homeDir, color: accountForm.color };
|
|
3176
|
-
return { ...c, accounts: [...c.accounts, account] };
|
|
3177
|
-
}
|
|
3178
|
-
return {
|
|
3179
|
-
...c,
|
|
3180
|
-
accounts: c.accounts.map((a) => a.id === accountForm.editingId ? { ...a, providerId: accountForm.providerId, name, homeDir, color: accountForm.color } : a)
|
|
3181
|
-
};
|
|
3182
|
-
});
|
|
3183
|
-
setAccountForm(null);
|
|
3184
|
-
}
|
|
3185
|
-
const cycleFormField = useCallback((dir) => {
|
|
3186
|
-
setAccountForm((f) => {
|
|
3187
|
-
if (!f) return f;
|
|
3188
|
-
const i = FORM_FIELDS.indexOf(f.field);
|
|
3189
|
-
const next = FORM_FIELDS[(i + dir + FORM_FIELDS.length) % FORM_FIELDS.length];
|
|
3190
|
-
const caret = next === "name" ? f.name.length : next === "homeDir" ? f.homeDir.length : f.caret;
|
|
3191
|
-
return { ...f, field: next, caret };
|
|
3192
|
-
});
|
|
3193
|
-
}, []);
|
|
3194
|
-
const cycleProvider = useCallback((dir) => {
|
|
3195
|
-
setAccountForm((f) => {
|
|
3196
|
-
if (!f) return f;
|
|
3197
|
-
const i = PROVIDER_ORDER.indexOf(f.providerId);
|
|
3198
|
-
return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
|
|
3199
|
-
});
|
|
3200
|
-
}, []);
|
|
3201
|
-
const cycleColor = useCallback((dir) => {
|
|
3202
|
-
setAccountForm((f) => {
|
|
3203
|
-
if (!f) return f;
|
|
3204
|
-
const i = COLOR_PALETTE.indexOf(f.color);
|
|
3205
|
-
const idx = i < 0 ? 0 : i;
|
|
3206
|
-
return { ...f, color: COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length] };
|
|
3207
|
-
});
|
|
3208
|
-
}, []);
|
|
3209
|
-
function deleteAccount(id) {
|
|
3210
|
-
updateConfig((c) => ({
|
|
3211
|
-
...c,
|
|
3212
|
-
accounts: c.accounts.filter((a) => a.id !== id),
|
|
3213
|
-
activeAccountId: c.activeAccountId === id ? null : c.activeAccountId
|
|
3214
|
-
}));
|
|
3215
|
-
}
|
|
3216
|
-
function moveAccount(idx, dir) {
|
|
3217
|
-
updateConfig((c) => {
|
|
3218
|
-
const next = [...c.accounts];
|
|
3219
|
-
const target = idx + dir;
|
|
3220
|
-
if (target < 0 || target >= next.length) return c;
|
|
3221
|
-
[next[idx], next[target]] = [next[target], next[idx]];
|
|
3222
|
-
return { ...c, accounts: next };
|
|
3223
|
-
});
|
|
3224
|
-
setSettingsCursor((c) => Math.max(0, Math.min(trackedAccountRows.length - 1, c + dir)));
|
|
3225
|
-
}
|
|
3382
|
+
const {
|
|
3383
|
+
accountForm,
|
|
3384
|
+
setAccountForm,
|
|
3385
|
+
openAddAccount,
|
|
3386
|
+
openConfigureAccount,
|
|
3387
|
+
openEditAccount,
|
|
3388
|
+
commitAccountForm,
|
|
3389
|
+
cycleFormField,
|
|
3390
|
+
cycleProvider,
|
|
3391
|
+
cycleColor,
|
|
3392
|
+
deleteAccount,
|
|
3393
|
+
moveAccount
|
|
3394
|
+
} = useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSettingsCursor });
|
|
3226
3395
|
async function toggleWeb() {
|
|
3227
3396
|
if (connected) {
|
|
3228
3397
|
if (baseUrl) openUrl(baseUrl);
|
|
@@ -3235,7 +3404,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3235
3404
|
if (webStartingRef.current) return;
|
|
3236
3405
|
webStartingRef.current = true;
|
|
3237
3406
|
try {
|
|
3238
|
-
const { startWebServer } = await import("./server-
|
|
3407
|
+
const { startWebServer } = await import("./server-GEGTLRUW.js");
|
|
3239
3408
|
const ctrl = await startWebServer({ config: cfg, log: false });
|
|
3240
3409
|
webRef.current = ctrl;
|
|
3241
3410
|
openUrl(ctrl.url);
|
|
@@ -3244,30 +3413,44 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3244
3413
|
webStartingRef.current = false;
|
|
3245
3414
|
}
|
|
3246
3415
|
}
|
|
3247
|
-
const onTabSelect =
|
|
3416
|
+
const onTabSelect = useCallback4((i) => {
|
|
3248
3417
|
setTab(i);
|
|
3249
3418
|
resetView();
|
|
3250
3419
|
}, [resetView]);
|
|
3251
|
-
const onStripSelect =
|
|
3420
|
+
const onStripSelect = useCallback4((i) => {
|
|
3252
3421
|
updateConfig((c) => ({ ...c, activeAccountId: slots[i].id }));
|
|
3253
3422
|
resetView();
|
|
3254
3423
|
}, [slots, updateConfig, resetView]);
|
|
3255
|
-
const onProviderSelect =
|
|
3424
|
+
const onProviderSelect = useCallback4((p) => {
|
|
3256
3425
|
setTableProvider(p);
|
|
3257
3426
|
setCursor(0);
|
|
3258
3427
|
setExpanded(-1);
|
|
3428
|
+
setTableModel(null);
|
|
3259
3429
|
setSearch("");
|
|
3260
3430
|
setSearchCaret(0);
|
|
3261
3431
|
setSearchMode(false);
|
|
3262
3432
|
}, []);
|
|
3263
|
-
const onRowClickToken =
|
|
3433
|
+
const onRowClickToken = useCallback4((idx) => {
|
|
3264
3434
|
if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
|
|
3265
3435
|
else setCursor(idx);
|
|
3266
3436
|
}, [cursor]);
|
|
3267
|
-
const onRowClickCursor =
|
|
3437
|
+
const onRowClickCursor = useCallback4((idx) => setCursor(idx), []);
|
|
3438
|
+
const rawTokenRows = useMemo2(
|
|
3439
|
+
() => tab === 1 && !tableIsCursor ? table ? [table.daily, table.weekly, table.monthly][view] : [] : [],
|
|
3440
|
+
[tab, tableIsCursor, table, view]
|
|
3441
|
+
);
|
|
3442
|
+
const tokenModelOptions = useMemo2(() => tableModelOptions(rawTokenRows), [rawTokenRows]);
|
|
3443
|
+
useEffect7(() => {
|
|
3444
|
+
if (tableModel && !tokenModelOptions.includes(tableModel)) setTableModel(null);
|
|
3445
|
+
}, [tableModel, tokenModelOptions]);
|
|
3446
|
+
const cycleTableModelFilter = useCallback4((dir) => {
|
|
3447
|
+
setTableModel((cur) => cycleTableModel(cur, tokenModelOptions, dir));
|
|
3448
|
+
resetView();
|
|
3449
|
+
}, [tokenModelOptions, resetView]);
|
|
3450
|
+
const activeTableModel = tableModel && tokenModelOptions.includes(tableModel) ? tableModel : null;
|
|
3268
3451
|
const tokenRows = useMemo2(
|
|
3269
|
-
() => tab === 1 && !tableIsCursor ? sortRows(filterTokenRows(
|
|
3270
|
-
[tab, tableIsCursor,
|
|
3452
|
+
() => tab === 1 && !tableIsCursor ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
|
|
3453
|
+
[tab, tableIsCursor, rawTokenRows, activeTableModel, search, sort]
|
|
3271
3454
|
);
|
|
3272
3455
|
const cursorTableRows = useMemo2(
|
|
3273
3456
|
() => tab === 1 && tableIsCursor ? sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort) : [],
|
|
@@ -3333,6 +3516,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3333
3516
|
setSort,
|
|
3334
3517
|
SORTS_FOR,
|
|
3335
3518
|
tableIsCursor,
|
|
3519
|
+
cycleTableModel: cycleTableModelFilter,
|
|
3336
3520
|
setView,
|
|
3337
3521
|
cursor,
|
|
3338
3522
|
rowCountRef,
|
|
@@ -3450,10 +3634,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3450
3634
|
views: VIEWS,
|
|
3451
3635
|
period: view,
|
|
3452
3636
|
sort: sortLabel(SORTS_FOR[sort % SORTS_FOR.length]),
|
|
3637
|
+
model: activeTableModel,
|
|
3453
3638
|
search,
|
|
3454
3639
|
searchCaret,
|
|
3455
3640
|
searching: searchMode,
|
|
3456
|
-
showPeriod: !tableIsCursor
|
|
3641
|
+
showPeriod: !tableIsCursor,
|
|
3642
|
+
showModel: !tableIsCursor
|
|
3457
3643
|
}
|
|
3458
3644
|
),
|
|
3459
3645
|
/* @__PURE__ */ jsx11(Box11, { height: 1 }),
|