tokmon 0.20.6 → 0.21.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-QHJOOWIW.js} +552 -439
- package/dist/{chunk-XQEJ4WQ5.js → chunk-7MFZMI5C.js} +2 -2
- package/dist/{chunk-E6YT66ST.js → chunk-CPN67OK6.js} +70 -111
- package/dist/{chunk-YWJT3OG4.js → chunk-YCIUMAKK.js} +425 -390
- package/dist/cli.js +4 -4
- package/dist/{config-C6Z65JUP.js → config-UPNBGIJO.js} +1 -1
- package/dist/{daemon-ZUWJQEKA.js → daemon-LZVP4SWF.js} +3 -3
- package/dist/server-F7CUK4ZM.js +9 -0
- package/dist/web/assets/{breakdown-DFnPYZtA.js → breakdown-ClwCJce8.js} +1 -1
- package/dist/web/assets/{chart-C8J22kR3.js → chart-Dcuh_e3J.js} +1 -1
- package/dist/web/assets/index-nAr5lqix.js +105 -0
- package/dist/web/assets/{timeline-BjoaOdbh.js → timeline-gDyrCQ0O.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/server-TSTBMVTJ.js +0 -9
- package/dist/web/assets/index-B9eW55YB.js +0 -105
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
systemTimezone,
|
|
19
19
|
time,
|
|
20
20
|
tokens
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-YCIUMAKK.js";
|
|
22
22
|
import {
|
|
23
23
|
COLOR_PALETTE,
|
|
24
24
|
DEFAULTS,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
sanitizeTyped,
|
|
33
33
|
saveConfigSync,
|
|
34
34
|
snapshotCacheFile
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-7MFZMI5C.js";
|
|
36
36
|
import {
|
|
37
37
|
glyphs
|
|
38
38
|
} from "./chunk-RF4GGQGM.js";
|
|
@@ -42,8 +42,8 @@ import { render } from "ink";
|
|
|
42
42
|
import { MouseProvider } from "@zenobius/ink-mouse";
|
|
43
43
|
|
|
44
44
|
// src/app.tsx
|
|
45
|
-
import { useState as
|
|
46
|
-
import { Box as Box11, Text as Text11, useInput,
|
|
45
|
+
import { useState as useState8, useEffect as useEffect7, useCallback as useCallback4, useRef as useRef6, useMemo as useMemo2 } from "react";
|
|
46
|
+
import { Box as Box11, Text as Text11, useInput, useApp } from "ink";
|
|
47
47
|
import { useMouse } from "@zenobius/ink-mouse";
|
|
48
48
|
|
|
49
49
|
// src/config-sync.ts
|
|
@@ -80,22 +80,6 @@ function reconcileDaemonConfig(previous, daemonConfig, pendingLocalConfig) {
|
|
|
80
80
|
return { config: daemonConfig, pendingLocalConfig: null };
|
|
81
81
|
}
|
|
82
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
83
|
// src/ui/shared.tsx
|
|
100
84
|
import { appendFileSync } from "fs";
|
|
101
85
|
import { memo, useEffect, useRef, useState } from "react";
|
|
@@ -274,18 +258,42 @@ var CARD_H = { full: 14, compact: 12, mini: 8 };
|
|
|
274
258
|
var VARIANT_ORDER = ["full", "compact", "mini"];
|
|
275
259
|
var INDICATOR_ROWS = 1;
|
|
276
260
|
var MAX_SINGLE_CARD = Math.round(MIN_CARD * 1.6);
|
|
277
|
-
function
|
|
261
|
+
function estimateCardHeights(groups, stats) {
|
|
262
|
+
return groups.map((g) => {
|
|
263
|
+
const meta = PROVIDERS[g.provider];
|
|
264
|
+
let h = 3;
|
|
265
|
+
if (meta.hasUsage) h += 5;
|
|
266
|
+
if (meta.hasBilling) {
|
|
267
|
+
if (meta.hasUsage) h += 1;
|
|
268
|
+
const multi = g.accounts.length > 1;
|
|
269
|
+
g.accounts.forEach((a, i) => {
|
|
270
|
+
const metricRows = stats.get(a.id)?.billing?.metrics.length || 1;
|
|
271
|
+
h += metricRows + (multi ? 1 : 0) + (multi && i > 0 ? 1 : 0);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
h += 2;
|
|
275
|
+
return Math.max(h, CARD_H.mini);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function chooseLayout(content, budget, n, single, cols, heights) {
|
|
278
279
|
if (n <= 0) return { ncols: 1, variant: "mini", cardsPerPage: 1, pageCount: 1 };
|
|
280
|
+
const heightFor = (variant) => {
|
|
281
|
+
if (!heights || heights.length === 0) return CARD_H[variant];
|
|
282
|
+
if (variant === "mini") return CARD_H.mini;
|
|
283
|
+
const hs = heights.map((h) => variant === "full" ? h : Math.max(h - 2, CARD_H.mini - 2));
|
|
284
|
+
return Math.max(...hs);
|
|
285
|
+
};
|
|
279
286
|
const gridHeight = (rows, H2) => rows * H2 + Math.max(0, rows - 1);
|
|
280
287
|
const colCap = single ? 1 : cols >= 3 * MIN_CARD_DENSE + 2 * GAP ? 3 : cols >= 2 * MIN_CARD + GAP ? 2 : 1;
|
|
281
288
|
const maxCols = Math.max(1, Math.min(colCap, n));
|
|
282
289
|
const cardWidthAt = (nc) => nc <= 1 ? content : Math.floor((content - GAP * (nc - 1)) / nc);
|
|
283
290
|
const minWidthAt = (nc) => nc >= 3 ? MIN_CARD_DENSE : MIN_CARD;
|
|
284
291
|
for (const variant of VARIANT_ORDER) {
|
|
292
|
+
const H2 = heightFor(variant);
|
|
285
293
|
for (let nc = maxCols; nc >= 1; nc--) {
|
|
286
294
|
if (nc > 1 && cardWidthAt(nc) < minWidthAt(nc)) continue;
|
|
287
295
|
const rows = Math.ceil(n / nc);
|
|
288
|
-
if (gridHeight(rows,
|
|
296
|
+
if (gridHeight(rows, H2) <= budget) {
|
|
289
297
|
return { ncols: nc, variant, cardsPerPage: n, pageCount: 1 };
|
|
290
298
|
}
|
|
291
299
|
}
|
|
@@ -304,6 +312,16 @@ function chooseLayout(content, budget, n, single, cols) {
|
|
|
304
312
|
const pageCount = Math.max(1, Math.ceil(n / cardsPerPage));
|
|
305
313
|
return { ncols, variant: "mini", cardsPerPage, pageCount };
|
|
306
314
|
}
|
|
315
|
+
function computeDashLayout(groups, stats, cols, budget, focusId, layoutPref) {
|
|
316
|
+
const content = Math.max(30, cols - 4);
|
|
317
|
+
const heights = estimateCardHeights(groups, stats);
|
|
318
|
+
const single = focusId !== null || layoutPref === "single";
|
|
319
|
+
if (layoutPref === "single" && focusId === null && groups.length > 1) {
|
|
320
|
+
const one = chooseLayout(content, budget, 1, true, cols, [Math.max(...heights)]);
|
|
321
|
+
return { ...one, cardsPerPage: 1, pageCount: groups.length };
|
|
322
|
+
}
|
|
323
|
+
return chooseLayout(content, budget, groups.length, single, cols, heights);
|
|
324
|
+
}
|
|
307
325
|
var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget, focusId, layout, page = 0 }) {
|
|
308
326
|
if (groups.length === 0) {
|
|
309
327
|
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
@@ -312,17 +330,14 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
|
|
|
312
330
|
" press s to pick providers."
|
|
313
331
|
] });
|
|
314
332
|
}
|
|
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);
|
|
333
|
+
const content = Math.max(30, cols - 4);
|
|
334
|
+
const { ncols, variant, cardsPerPage, pageCount } = computeDashLayout(groups, stats, cols, budget, focusId, layout);
|
|
320
335
|
let cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
|
|
321
336
|
if (ncols === 1 && cardW > MAX_SINGLE_CARD) cardW = MAX_SINGLE_CARD;
|
|
322
337
|
const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
|
|
323
|
-
const visible = pageCount > 1 ?
|
|
338
|
+
const visible = pageCount > 1 ? groups.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : groups;
|
|
324
339
|
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)) }),
|
|
340
|
+
/* @__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
341
|
pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
327
342
|
" ",
|
|
328
343
|
glyphs().middot,
|
|
@@ -346,7 +361,6 @@ function ProviderCard({ provider, accounts, stats, width, variant }) {
|
|
|
346
361
|
const plan = items.map((i) => i.s?.billing?.plan).find(Boolean) ?? null;
|
|
347
362
|
const activity = items.map((i) => i.s?.billing?.activity).find(Boolean) ?? null;
|
|
348
363
|
const inner = width - 4;
|
|
349
|
-
const barW = Math.max(10, Math.min(46, inner - 20));
|
|
350
364
|
const hasSpark = !!agg && agg.series.some((v) => v > 0);
|
|
351
365
|
const showBars = variant !== "mini";
|
|
352
366
|
const showSpark = variant === "full";
|
|
@@ -375,7 +389,7 @@ function ProviderCard({ provider, accounts, stats, width, variant }) {
|
|
|
375
389
|
] })),
|
|
376
390
|
meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
377
391
|
meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
|
|
378
|
-
/* @__PURE__ */ jsx2(LimitsBlock, { items,
|
|
392
|
+
/* @__PURE__ */ jsx2(LimitsBlock, { items, inner })
|
|
379
393
|
] }),
|
|
380
394
|
meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items }),
|
|
381
395
|
hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
@@ -399,7 +413,7 @@ function CompactBilling({ items }) {
|
|
|
399
413
|
glyphs().ellipsis
|
|
400
414
|
] });
|
|
401
415
|
if (billing.error) return /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error });
|
|
402
|
-
const m = billing.metrics[0];
|
|
416
|
+
const m = billing.metrics.find((x) => x.primary) ?? billing.metrics[0];
|
|
403
417
|
if (!m) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" });
|
|
404
418
|
return /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) });
|
|
405
419
|
}
|
|
@@ -443,8 +457,15 @@ function KpiLine({ agg }) {
|
|
|
443
457
|
] })
|
|
444
458
|
] });
|
|
445
459
|
}
|
|
446
|
-
function
|
|
460
|
+
function accountTitle(account, billing) {
|
|
461
|
+
const email = billing?.email;
|
|
462
|
+
return email && !account.name.includes("@") ? `${account.name} ${email}` : account.name;
|
|
463
|
+
}
|
|
464
|
+
function LimitsBlock({ items, inner }) {
|
|
447
465
|
const showName = items.length > 1;
|
|
466
|
+
const labels = items.flatMap((i) => i.s?.billing?.metrics ?? []).map((m) => m.label.length);
|
|
467
|
+
const labelW = Math.min(Math.max(7, ...labels) + 1, 14);
|
|
468
|
+
const barW = Math.max(10, Math.min(46, inner - labelW - 13));
|
|
448
469
|
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
|
|
449
470
|
const billing = s?.billing;
|
|
450
471
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
|
|
@@ -453,20 +474,20 @@ function LimitsBlock({ items, barW }) {
|
|
|
453
474
|
glyphs().dot,
|
|
454
475
|
" "
|
|
455
476
|
] }),
|
|
456
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.
|
|
477
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(accountTitle(account, billing), Math.max(22, inner - 2)) })
|
|
457
478
|
] }),
|
|
458
479
|
!billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
459
480
|
"Fetching",
|
|
460
481
|
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}`))
|
|
482
|
+
] }) : 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
483
|
] }, account.id);
|
|
463
484
|
}) });
|
|
464
485
|
}
|
|
465
|
-
function MetricRow({ m, color, barW }) {
|
|
486
|
+
function MetricRow({ m, color, barW, labelW }) {
|
|
466
487
|
if (m.format.kind === "percent") {
|
|
467
488
|
const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
|
|
468
489
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
469
|
-
/* @__PURE__ */ jsx2(Box2, { width:
|
|
490
|
+
/* @__PURE__ */ jsx2(Box2, { width: labelW, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
|
|
470
491
|
/* @__PURE__ */ jsx2(Bar, { pct: m.used, color: barColor, width: barW }),
|
|
471
492
|
/* @__PURE__ */ jsx2(Box2, { width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
|
|
472
493
|
Math.round(m.used),
|
|
@@ -476,7 +497,7 @@ function MetricRow({ m, color, barW }) {
|
|
|
476
497
|
] });
|
|
477
498
|
}
|
|
478
499
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
479
|
-
/* @__PURE__ */ jsx2(Box2, { width:
|
|
500
|
+
/* @__PURE__ */ jsx2(Box2, { width: labelW, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
|
|
480
501
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) })
|
|
481
502
|
] });
|
|
482
503
|
}
|
|
@@ -1568,6 +1589,9 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
|
|
|
1568
1589
|
const showOpt = IS_APPLE_TERMINAL && inner >= BASE + OPT;
|
|
1569
1590
|
const showJump = hasAccounts && inner >= BASE + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
|
|
1570
1591
|
const showPage = paginated && inner >= BASE + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
|
|
1592
|
+
if (inner < BASE) {
|
|
1593
|
+
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" }) });
|
|
1594
|
+
}
|
|
1571
1595
|
return /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexWrap: "nowrap", children: [
|
|
1572
1596
|
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "by " }),
|
|
1573
1597
|
/* @__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" }) }) }),
|
|
@@ -2572,12 +2596,9 @@ function handleKey(input, key, ctx) {
|
|
|
2572
2596
|
}
|
|
2573
2597
|
}
|
|
2574
2598
|
|
|
2575
|
-
// src/
|
|
2576
|
-
import {
|
|
2577
|
-
|
|
2578
|
-
var LOADER_GRACE_MS = 600;
|
|
2579
|
-
var LOADER_MAX_MS = 8e3;
|
|
2580
|
-
var LOADER_MIN_VISIBLE_MS = 700;
|
|
2599
|
+
// src/ui/hooks/use-terminal-size.ts
|
|
2600
|
+
import { useState as useState4, useEffect as useEffect4 } from "react";
|
|
2601
|
+
import { useStdout } from "ink";
|
|
2581
2602
|
function useTerminalSize(settleMs = 90) {
|
|
2582
2603
|
const { stdout } = useStdout();
|
|
2583
2604
|
const read = () => ({ cols: stdout?.columns || 80, rows: stdout?.rows || 24 });
|
|
@@ -2606,234 +2627,98 @@ function useTerminalSize(settleMs = 90) {
|
|
|
2606
2627
|
}, [stdout, settleMs]);
|
|
2607
2628
|
return { cols: size.cols, rows: size.rows, resizing, live };
|
|
2608
2629
|
}
|
|
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);
|
|
2630
|
+
|
|
2631
|
+
// src/ui/hooks/use-paste.ts
|
|
2632
|
+
import { useCallback, useRef as useRef3 } from "react";
|
|
2633
|
+
var PASTE_START = "\x1B[200~";
|
|
2634
|
+
var PASTE_END = "\x1B[201~";
|
|
2635
|
+
var PASTE_MAX = 1 << 20;
|
|
2636
|
+
function usePaste(onInsert) {
|
|
2673
2637
|
const pasteBufRef = useRef3(null);
|
|
2674
2638
|
const pasteCarryRef = useRef3("");
|
|
2675
|
-
const pendingLocalConfigRef = useRef3(null);
|
|
2676
2639
|
const insertPasteRef = useRef3(() => {
|
|
2677
2640
|
});
|
|
2678
|
-
|
|
2679
|
-
const
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2641
|
+
insertPasteRef.current = onInsert;
|
|
2642
|
+
const handlePasteData = useCallback((chunk) => {
|
|
2643
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2644
|
+
if (pasteBufRef.current !== null) {
|
|
2645
|
+
const combined2 = pasteBufRef.current + s;
|
|
2646
|
+
const end2 = combined2.indexOf(PASTE_END);
|
|
2647
|
+
if (end2 === -1) {
|
|
2648
|
+
if (combined2.length >= PASTE_MAX) {
|
|
2649
|
+
const clean3 = sanitizeTyped(combined2);
|
|
2650
|
+
pasteBufRef.current = null;
|
|
2651
|
+
if (clean3) insertPasteRef.current(clean3);
|
|
2652
|
+
return true;
|
|
2653
|
+
}
|
|
2654
|
+
pasteBufRef.current = combined2;
|
|
2655
|
+
return true;
|
|
2656
|
+
}
|
|
2657
|
+
const clean2 = sanitizeTyped(combined2.slice(0, end2));
|
|
2658
|
+
pasteBufRef.current = null;
|
|
2659
|
+
if (clean2) insertPasteRef.current(clean2);
|
|
2660
|
+
return end2 + PASTE_END.length >= combined2.length;
|
|
2698
2661
|
}
|
|
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 };
|
|
2662
|
+
const combined = pasteCarryRef.current + s;
|
|
2663
|
+
const start = combined.indexOf(PASTE_START);
|
|
2664
|
+
if (start === -1) {
|
|
2665
|
+
const keep = Math.min(combined.length, PASTE_START.length - 1);
|
|
2666
|
+
pasteCarryRef.current = combined.slice(combined.length - keep);
|
|
2667
|
+
return false;
|
|
2713
2668
|
}
|
|
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);
|
|
2669
|
+
pasteCarryRef.current = "";
|
|
2670
|
+
const rest = combined.slice(start + PASTE_START.length);
|
|
2671
|
+
const end = rest.indexOf(PASTE_END);
|
|
2672
|
+
if (end === -1) {
|
|
2673
|
+
pasteBufRef.current = rest;
|
|
2674
|
+
return true;
|
|
2769
2675
|
}
|
|
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
|
|
2676
|
+
const clean = sanitizeTyped(rest.slice(0, end));
|
|
2677
|
+
if (clean) insertPasteRef.current(clean);
|
|
2678
|
+
return true;
|
|
2679
|
+
}, []);
|
|
2680
|
+
const isPasteInput = useCallback((input) => {
|
|
2681
|
+
if (pasteBufRef.current !== null) return true;
|
|
2682
|
+
return input.includes("[200~") || input.includes("[201~");
|
|
2683
|
+
}, []);
|
|
2684
|
+
return { handlePasteData, isPasteInput };
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// src/ui/hooks/use-loader.ts
|
|
2688
|
+
import { useState as useState5, useEffect as useEffect5, useCallback as useCallback2, useRef as useRef4 } from "react";
|
|
2689
|
+
var DEBOUNCE_MS = 300;
|
|
2690
|
+
var LOADER_GRACE_MS = 600;
|
|
2691
|
+
var LOADER_MAX_MS = 8e3;
|
|
2692
|
+
var LOADER_MIN_VISIBLE_MS = 700;
|
|
2693
|
+
function useLoader({ configReady, showPicker, accountsKey, allReady, tooSmall, showSettings, accountsCount }) {
|
|
2694
|
+
const [debouncePassed, setDebouncePassed] = useState5(false);
|
|
2695
|
+
const [graceHold, setGraceHold] = useState5(false);
|
|
2696
|
+
const [loaderShownAt, setLoaderShownAt] = useState5(null);
|
|
2697
|
+
const loaderDone = useRef4(false);
|
|
2698
|
+
const [loaderDoneState, setLoaderDoneFlag] = useState5(false);
|
|
2699
|
+
const setLoaderDone = useCallback2((v) => {
|
|
2700
|
+
loaderDone.current = v;
|
|
2701
|
+
setLoaderDoneFlag(v);
|
|
2702
|
+
}, []);
|
|
2703
|
+
const prevShowPicker = useRef4(false);
|
|
2704
|
+
const resetLoader = useCallback2(() => {
|
|
2705
|
+
setLoaderDone(false);
|
|
2706
|
+
setDebouncePassed(false);
|
|
2707
|
+
setGraceHold(false);
|
|
2708
|
+
setLoaderShownAt(null);
|
|
2709
|
+
}, [setLoaderDone]);
|
|
2791
2710
|
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(() => {
|
|
2711
|
+
const showLoader = configReady && !showPicker && !showSettings && !tooSmall && accountsCount > 0 && (!allReady || graceHold || minVisibleHold) && (debouncePassed || loaderShownAt !== null) && !loaderDoneState;
|
|
2712
|
+
useEffect5(() => {
|
|
2803
2713
|
const wasPicker = prevShowPicker.current;
|
|
2804
2714
|
prevShowPicker.current = showPicker;
|
|
2805
|
-
if (wasPicker && !showPicker)
|
|
2806
|
-
setLoaderDone(false);
|
|
2807
|
-
setDebouncePassed(false);
|
|
2808
|
-
setGraceHold(false);
|
|
2809
|
-
setLoaderShownAt(null);
|
|
2810
|
-
}
|
|
2715
|
+
if (wasPicker && !showPicker) resetLoader();
|
|
2811
2716
|
}, [showPicker]);
|
|
2812
|
-
|
|
2717
|
+
useEffect5(() => {
|
|
2813
2718
|
if (showLoader && loaderShownAt === null) setLoaderShownAt(Date.now());
|
|
2814
2719
|
}, [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;
|
|
2720
|
+
useEffect5(() => {
|
|
2721
|
+
if (!configReady || showPicker || accountsCount === 0) return;
|
|
2837
2722
|
if (allReady || loaderDone.current) return;
|
|
2838
2723
|
const debounce = setTimeout(() => setDebouncePassed(true), DEBOUNCE_MS);
|
|
2839
2724
|
const deadline = setTimeout(() => {
|
|
@@ -2845,7 +2730,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2845
2730
|
clearTimeout(deadline);
|
|
2846
2731
|
};
|
|
2847
2732
|
}, [configReady, showPicker, accountsKey]);
|
|
2848
|
-
|
|
2733
|
+
useEffect5(() => {
|
|
2849
2734
|
if (!allReady || loaderDone.current) return;
|
|
2850
2735
|
if (loaderShownAt === null) {
|
|
2851
2736
|
setLoaderDone(true);
|
|
@@ -2860,28 +2745,73 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2860
2745
|
}, hold);
|
|
2861
2746
|
return () => clearTimeout(t);
|
|
2862
2747
|
}, [allReady, loaderShownAt]);
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2748
|
+
return { showLoader, resetLoader };
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// src/ui/hooks/use-degraded-polling.ts
|
|
2752
|
+
import { useState as useState6, useEffect as useEffect6, useRef as useRef5 } from "react";
|
|
2753
|
+
|
|
2754
|
+
// src/client/seed-cache.ts
|
|
2755
|
+
import { readFile } from "fs/promises";
|
|
2756
|
+
async function loadSeedSnapshot() {
|
|
2757
|
+
try {
|
|
2758
|
+
const parsed = JSON.parse(await readFile(snapshotCacheFile(), "utf-8"));
|
|
2759
|
+
if (!parsed || !Array.isArray(parsed.accounts)) return {};
|
|
2760
|
+
const out = {};
|
|
2761
|
+
for (const a of parsed.accounts) {
|
|
2762
|
+
if (a.dashboard || a.billing) out[a.id] = { dashboard: a.dashboard ?? null, billing: a.billing ?? null };
|
|
2763
|
+
}
|
|
2764
|
+
return out;
|
|
2765
|
+
} catch {
|
|
2766
|
+
return {};
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/ui/hooks/use-degraded-polling.ts
|
|
2771
|
+
function useDegradedPolling({ degraded, configReady, showPicker, accountsKey, accountsRef, interval, billingMs, tz }) {
|
|
2772
|
+
const [statsLocal, setStats] = useState6(/* @__PURE__ */ new Map());
|
|
2773
|
+
const [peakLocal, setPeak] = useState6(null);
|
|
2774
|
+
const [error, setError] = useState6(null);
|
|
2775
|
+
const [updatedLocal, setUpdated] = useState6(/* @__PURE__ */ new Date());
|
|
2776
|
+
const seededRef = useRef5(false);
|
|
2777
|
+
useEffect6(() => {
|
|
2778
|
+
if (!degraded) return;
|
|
2779
|
+
if (seededRef.current || !configReady || showPicker || accountsRef.current.length === 0) return;
|
|
2780
|
+
seededRef.current = true;
|
|
2781
|
+
loadSeedSnapshot().then((snap) => {
|
|
2782
|
+
setStats((prev) => {
|
|
2783
|
+
if (prev.size > 0) return prev;
|
|
2784
|
+
const next = new Map(prev);
|
|
2785
|
+
for (const acc of accountsRef.current) {
|
|
2786
|
+
const s = snap[acc.id];
|
|
2787
|
+
if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
|
|
2788
|
+
}
|
|
2789
|
+
return next;
|
|
2790
|
+
});
|
|
2791
|
+
});
|
|
2792
|
+
}, [degraded, configReady, showPicker, accountsKey]);
|
|
2793
|
+
useEffect6(() => {
|
|
2794
|
+
if (!degraded || !configReady || showPicker) return;
|
|
2795
|
+
let active = true;
|
|
2796
|
+
let timer;
|
|
2797
|
+
const load = async () => {
|
|
2798
|
+
try {
|
|
2799
|
+
await Promise.all(accountsRef.current.map(async (acc) => {
|
|
2800
|
+
const provider = PROVIDERS[acc.providerId];
|
|
2801
|
+
if (!provider.hasUsage || !provider.fetchSummary) return;
|
|
2802
|
+
try {
|
|
2803
|
+
const dashboard = await provider.fetchSummary(acc, tz);
|
|
2804
|
+
if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
|
|
2805
|
+
} catch {
|
|
2806
|
+
}
|
|
2807
|
+
}));
|
|
2808
|
+
if (active) {
|
|
2809
|
+
setError(null);
|
|
2810
|
+
setUpdated(/* @__PURE__ */ new Date());
|
|
2811
|
+
}
|
|
2812
|
+
} finally {
|
|
2813
|
+
if (active) timer = setTimeout(load, interval);
|
|
2814
|
+
}
|
|
2885
2815
|
};
|
|
2886
2816
|
load();
|
|
2887
2817
|
return () => {
|
|
@@ -2889,7 +2819,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2889
2819
|
clearTimeout(timer);
|
|
2890
2820
|
};
|
|
2891
2821
|
}, [degraded, interval, tz, configReady, showPicker, accountsKey]);
|
|
2892
|
-
|
|
2822
|
+
useEffect6(() => {
|
|
2893
2823
|
if (!degraded || !configReady || showPicker) return;
|
|
2894
2824
|
let active = true;
|
|
2895
2825
|
let timer;
|
|
@@ -2900,7 +2830,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2900
2830
|
const provider = PROVIDERS[acc.providerId];
|
|
2901
2831
|
if (!provider.hasBilling || !provider.fetchBilling) return;
|
|
2902
2832
|
try {
|
|
2903
|
-
const billing = await provider.fetchBilling(acc);
|
|
2833
|
+
const billing = await provider.fetchBilling(acc, tz);
|
|
2904
2834
|
if (active) setStats((prev) => upsert(prev, acc, { billing }));
|
|
2905
2835
|
} catch {
|
|
2906
2836
|
}
|
|
@@ -2916,12 +2846,319 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2916
2846
|
active = false;
|
|
2917
2847
|
clearTimeout(timer);
|
|
2918
2848
|
};
|
|
2919
|
-
}, [degraded, billingMs, configReady, showPicker, accountsKey]);
|
|
2849
|
+
}, [degraded, billingMs, tz, configReady, showPicker, accountsKey]);
|
|
2850
|
+
return { statsLocal, peakLocal, updatedLocal, error };
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// src/ui/hooks/use-account-form.ts
|
|
2854
|
+
import { useState as useState7, useCallback as useCallback3 } from "react";
|
|
2855
|
+
function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSettingsCursor }) {
|
|
2856
|
+
const [accountForm, setAccountForm] = useState7(null);
|
|
2857
|
+
function openAddAccount(defaults) {
|
|
2858
|
+
const providerId = defaults?.providerId ?? (detected[0] ?? "claude");
|
|
2859
|
+
setAccountForm({
|
|
2860
|
+
mode: "add",
|
|
2861
|
+
field: "provider",
|
|
2862
|
+
providerId,
|
|
2863
|
+
name: defaults?.name ?? "",
|
|
2864
|
+
homeDir: defaults?.homeDir ?? "~",
|
|
2865
|
+
color: defaults?.color ?? pickAccentColor(cfg.accounts),
|
|
2866
|
+
caret: defaults?.name?.length ?? 0,
|
|
2867
|
+
editingId: null,
|
|
2868
|
+
error: null
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
function openConfigureAccount(row) {
|
|
2872
|
+
openAddAccount(row);
|
|
2873
|
+
}
|
|
2874
|
+
function openEditAccount(acc) {
|
|
2875
|
+
setAccountForm({
|
|
2876
|
+
mode: "edit",
|
|
2877
|
+
field: "provider",
|
|
2878
|
+
providerId: acc.providerId,
|
|
2879
|
+
name: acc.name,
|
|
2880
|
+
homeDir: acc.homeDir,
|
|
2881
|
+
color: acc.color || PROVIDERS[acc.providerId].color,
|
|
2882
|
+
caret: acc.name.length,
|
|
2883
|
+
editingId: acc.id,
|
|
2884
|
+
error: null
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
function commitAccountForm() {
|
|
2888
|
+
if (!accountForm) return;
|
|
2889
|
+
const name = accountForm.name.trim();
|
|
2890
|
+
const homeDir = accountForm.homeDir.trim() || "~";
|
|
2891
|
+
if (!name) {
|
|
2892
|
+
setAccountForm({ ...accountForm, error: "Name required", field: "name", caret: accountForm.name.length });
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
updateConfig((c) => {
|
|
2896
|
+
if (accountForm.mode === "add") {
|
|
2897
|
+
const id = generateAccountId(name, c.accounts);
|
|
2898
|
+
const account = { id, providerId: accountForm.providerId, name, homeDir, color: accountForm.color };
|
|
2899
|
+
return { ...c, accounts: [...c.accounts, account] };
|
|
2900
|
+
}
|
|
2901
|
+
return {
|
|
2902
|
+
...c,
|
|
2903
|
+
accounts: c.accounts.map((a) => a.id === accountForm.editingId ? { ...a, providerId: accountForm.providerId, name, homeDir, color: accountForm.color } : a)
|
|
2904
|
+
};
|
|
2905
|
+
});
|
|
2906
|
+
setAccountForm(null);
|
|
2907
|
+
}
|
|
2908
|
+
const cycleFormField = useCallback3((dir) => {
|
|
2909
|
+
setAccountForm((f) => {
|
|
2910
|
+
if (!f) return f;
|
|
2911
|
+
const i = FORM_FIELDS.indexOf(f.field);
|
|
2912
|
+
const next = FORM_FIELDS[(i + dir + FORM_FIELDS.length) % FORM_FIELDS.length];
|
|
2913
|
+
const caret = next === "name" ? f.name.length : next === "homeDir" ? f.homeDir.length : f.caret;
|
|
2914
|
+
return { ...f, field: next, caret };
|
|
2915
|
+
});
|
|
2916
|
+
}, []);
|
|
2917
|
+
const cycleProvider = useCallback3((dir) => {
|
|
2918
|
+
setAccountForm((f) => {
|
|
2919
|
+
if (!f) return f;
|
|
2920
|
+
const i = PROVIDER_ORDER.indexOf(f.providerId);
|
|
2921
|
+
return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
|
|
2922
|
+
});
|
|
2923
|
+
}, []);
|
|
2924
|
+
const cycleColor = useCallback3((dir) => {
|
|
2925
|
+
setAccountForm((f) => {
|
|
2926
|
+
if (!f) return f;
|
|
2927
|
+
const i = COLOR_PALETTE.indexOf(f.color);
|
|
2928
|
+
const idx = i < 0 ? 0 : i;
|
|
2929
|
+
return { ...f, color: COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length] };
|
|
2930
|
+
});
|
|
2931
|
+
}, []);
|
|
2932
|
+
function deleteAccount(id) {
|
|
2933
|
+
updateConfig((c) => ({
|
|
2934
|
+
...c,
|
|
2935
|
+
accounts: c.accounts.filter((a) => a.id !== id),
|
|
2936
|
+
activeAccountId: c.activeAccountId === id ? null : c.activeAccountId
|
|
2937
|
+
}));
|
|
2938
|
+
}
|
|
2939
|
+
function moveAccount(idx, dir) {
|
|
2940
|
+
updateConfig((c) => {
|
|
2941
|
+
const next = [...c.accounts];
|
|
2942
|
+
const target = idx + dir;
|
|
2943
|
+
if (target < 0 || target >= next.length) return c;
|
|
2944
|
+
[next[idx], next[target]] = [next[target], next[idx]];
|
|
2945
|
+
return { ...c, accounts: next };
|
|
2946
|
+
});
|
|
2947
|
+
setSettingsCursor((c) => Math.max(0, Math.min(trackedAccountRows.length - 1, c + dir)));
|
|
2948
|
+
}
|
|
2949
|
+
return {
|
|
2950
|
+
accountForm,
|
|
2951
|
+
setAccountForm,
|
|
2952
|
+
openAddAccount,
|
|
2953
|
+
openConfigureAccount,
|
|
2954
|
+
openEditAccount,
|
|
2955
|
+
commitAccountForm,
|
|
2956
|
+
cycleFormField,
|
|
2957
|
+
cycleProvider,
|
|
2958
|
+
cycleColor,
|
|
2959
|
+
deleteAccount,
|
|
2960
|
+
moveAccount
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// src/app.tsx
|
|
2965
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2966
|
+
function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = null, mode = "degraded" }) {
|
|
2967
|
+
const connected = mode === "connected" && baseUrl !== null && wsToken !== null;
|
|
2968
|
+
const degraded = !connected;
|
|
2969
|
+
const daemon = useDaemon(connected ? baseUrl : null, connected ? wsToken : null);
|
|
2970
|
+
const [config, setConfig] = useState8(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
|
|
2971
|
+
const [detected, setDetected] = useState8([]);
|
|
2972
|
+
const [tableLocal, setTable] = useState8(null);
|
|
2973
|
+
const [tableLoading, setTableLoading] = useState8(false);
|
|
2974
|
+
const [tab, setTab] = useState8(0);
|
|
2975
|
+
const [view, setView] = useState8(0);
|
|
2976
|
+
const [cursor, setCursor] = useState8(0);
|
|
2977
|
+
const [expanded, setExpanded] = useState8(-1);
|
|
2978
|
+
const [sort, setSort] = useState8(1);
|
|
2979
|
+
const [tableProvider, setTableProvider] = useState8(null);
|
|
2980
|
+
const [search, setSearch] = useState8("");
|
|
2981
|
+
const [searchMode, setSearchMode] = useState8(false);
|
|
2982
|
+
const [cursorRowsLocal, setCursorRows] = useState8(null);
|
|
2983
|
+
const [showSettings, setShowSettings] = useState8(false);
|
|
2984
|
+
const [settingsTab, setSettingsTab] = useState8("general");
|
|
2985
|
+
const [settingsCursor, setSettingsCursor] = useState8(0);
|
|
2986
|
+
const [tzEdit, setTzEdit] = useState8(null);
|
|
2987
|
+
const [tzError, setTzError] = useState8(null);
|
|
2988
|
+
const [tzCaret, setTzCaret] = useState8(0);
|
|
2989
|
+
const [searchCaret, setSearchCaret] = useState8(0);
|
|
2990
|
+
const [onboardSel, setOnboardSel] = useState8(null);
|
|
2991
|
+
const [onboardCursor, setOnboardCursor] = useState8(0);
|
|
2992
|
+
const [dashPage, setDashPage] = useState8(0);
|
|
2993
|
+
const { exit } = useApp();
|
|
2994
|
+
const { cols, rows, resizing, live } = useTerminalSize();
|
|
2995
|
+
const webRef = useRef6(null);
|
|
2996
|
+
const webStartingRef = useRef6(false);
|
|
2997
|
+
useEffect7(() => () => {
|
|
2998
|
+
void webRef.current?.stop();
|
|
2999
|
+
}, []);
|
|
3000
|
+
const cfg = config ?? DEFAULTS;
|
|
3001
|
+
const interval = cliInterval ?? cfg.interval * 1e3;
|
|
3002
|
+
const billingMs = cfg.billingInterval * 6e4;
|
|
3003
|
+
const tz = resolveTimezone(cfg.timezone);
|
|
3004
|
+
const configReady = config !== null;
|
|
3005
|
+
const accounts = useMemo2(() => buildAccounts(cfg, detected), [cfg, detected]);
|
|
3006
|
+
const trackedAccountRows = useMemo2(() => getTrackedAccountRows(cfg, detected, accounts), [cfg, detected, accounts]);
|
|
3007
|
+
const settingsRowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
|
|
3008
|
+
const accountsRef = useRef6([]);
|
|
3009
|
+
accountsRef.current = accounts;
|
|
3010
|
+
const rowCountRef = useRef6(0);
|
|
3011
|
+
const tabRef = useRef6(0);
|
|
3012
|
+
tabRef.current = tab;
|
|
3013
|
+
const dashPageCountRef = useRef6(1);
|
|
3014
|
+
const pendingLocalConfigRef = useRef6(null);
|
|
3015
|
+
const tzValueRef = useRef6("");
|
|
3016
|
+
const tzCaretRef = useRef6(0);
|
|
3017
|
+
const searchValueRef = useRef6("");
|
|
3018
|
+
const searchCaretRef = useRef6(0);
|
|
3019
|
+
const accountsKey = useMemo2(() => accounts.map(acctKey).join("|"), [accounts]);
|
|
3020
|
+
const needsOnboarding = configReady && !cfg.onboarded;
|
|
3021
|
+
const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
|
|
3022
|
+
const showPicker = needsOnboarding || newProviders.length > 0;
|
|
3023
|
+
const { statsLocal, peakLocal, updatedLocal, error } = useDegradedPolling({
|
|
3024
|
+
degraded,
|
|
3025
|
+
configReady,
|
|
3026
|
+
showPicker,
|
|
3027
|
+
accountsKey,
|
|
3028
|
+
accountsRef,
|
|
3029
|
+
interval,
|
|
3030
|
+
billingMs,
|
|
3031
|
+
tz
|
|
3032
|
+
});
|
|
3033
|
+
const snapshot = daemon.snapshot;
|
|
3034
|
+
const stats = useMemo2(
|
|
3035
|
+
() => connected ? toStatsMap(snapshot, accounts) : statsLocal,
|
|
3036
|
+
[connected, snapshot, accounts, statsLocal]
|
|
3037
|
+
);
|
|
3038
|
+
const accountIdentities = useMemo2(() => {
|
|
3039
|
+
const out = /* @__PURE__ */ new Map();
|
|
3040
|
+
for (const [id, stat] of stats) {
|
|
3041
|
+
const billing = stat.billing;
|
|
3042
|
+
if (!billing) continue;
|
|
3043
|
+
out.set(id, {
|
|
3044
|
+
email: billing.email ?? null,
|
|
3045
|
+
displayName: billing.displayName ?? null,
|
|
3046
|
+
plan: billing.plan ?? null
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
return out;
|
|
3050
|
+
}, [stats]);
|
|
3051
|
+
const showPeak = accounts.some((a) => a.providerId === "claude");
|
|
3052
|
+
const peak = connected ? showPeak ? snapshot?.peak ?? null : null : peakLocal;
|
|
3053
|
+
const updated = useMemo2(
|
|
3054
|
+
() => connected ? new Date(snapshot?.generatedAt ?? Date.now()) : updatedLocal,
|
|
3055
|
+
[connected, snapshot, updatedLocal]
|
|
3056
|
+
);
|
|
3057
|
+
const intervalLabel = connected && snapshot?.intervalMs ? Math.round(snapshot.intervalMs / 1e3) : cfg.interval;
|
|
3058
|
+
const readyInputFor = useCallback4((id) => {
|
|
3059
|
+
if (connected) {
|
|
3060
|
+
const wa = snapshot?.accounts.find((a) => a.id === id);
|
|
3061
|
+
if (!wa) return void 0;
|
|
3062
|
+
return { summaryState: wa.summaryState, billingState: wa.billingState, billing: wa.billing };
|
|
3063
|
+
}
|
|
3064
|
+
return statsReadyInput(statsLocal.get(id));
|
|
3065
|
+
}, [connected, snapshot, statsLocal]);
|
|
3066
|
+
const slots = useMemo2(() => deriveSlots(accounts), [accounts]);
|
|
3067
|
+
const { activeSlotIdx, focusId } = useMemo2(
|
|
3068
|
+
() => findActiveSlot(slots, cfg.activeAccountId),
|
|
3069
|
+
[slots, cfg.activeAccountId]
|
|
3070
|
+
);
|
|
3071
|
+
const visibleAccounts = useMemo2(
|
|
3072
|
+
() => focusId === null ? accounts : accounts.filter((a) => a.id === focusId),
|
|
3073
|
+
[accounts, focusId]
|
|
3074
|
+
);
|
|
3075
|
+
const allGroups = useMemo2(() => accountsByProvider(accounts), [accounts]);
|
|
3076
|
+
const groups = useMemo2(
|
|
3077
|
+
() => focusId === null ? allGroups : accountsByProvider(visibleAccounts),
|
|
3078
|
+
[allGroups, visibleAccounts, focusId]
|
|
3079
|
+
);
|
|
3080
|
+
const tableProvs = useMemo2(() => allGroups.map((g) => g.provider), [allGroups]);
|
|
3081
|
+
const TOO_SMALL = cols < 40 || rows < 12;
|
|
3082
|
+
const allReady = accounts.length > 0 && accounts.every((a) => accountReady(readyInputFor(a.id), a.providerId));
|
|
3083
|
+
const { gridBudget } = useMemo2(() => computeChrome(slots, cols, rows), [slots, cols, rows]);
|
|
3084
|
+
const dashLayout = useMemo2(
|
|
3085
|
+
() => computeDashLayout(groups, stats, cols, gridBudget, focusId, cfg.dashboardLayout),
|
|
3086
|
+
[groups, stats, cols, gridBudget, focusId, cfg.dashboardLayout]
|
|
3087
|
+
);
|
|
3088
|
+
const dashPageCount = dashLayout.pageCount;
|
|
3089
|
+
const dashPaginated = dashPageCount > 1;
|
|
3090
|
+
dashPageCountRef.current = dashPageCount;
|
|
3091
|
+
tzValueRef.current = tzEdit ?? "";
|
|
3092
|
+
tzCaretRef.current = clampCaret(tzCaret, (tzEdit ?? "").length);
|
|
3093
|
+
searchValueRef.current = search;
|
|
3094
|
+
searchCaretRef.current = clampCaret(searchCaret, search.length);
|
|
3095
|
+
const isPrintable = (input, key) => !!input && !key.ctrl && !key.meta && !isPasteInput(input);
|
|
3096
|
+
const insertText = (text) => {
|
|
3097
|
+
if (showSettings && accountForm && (accountForm.field === "name" || accountForm.field === "homeDir")) {
|
|
3098
|
+
setAccountForm((f) => {
|
|
3099
|
+
if (!f || f.field !== "name" && f.field !== "homeDir") return f;
|
|
3100
|
+
const r = spliceInsert(f[f.field], f.caret, text);
|
|
3101
|
+
return { ...f, [f.field]: r.value, caret: r.caret, error: null };
|
|
3102
|
+
});
|
|
3103
|
+
} else if (showSettings && tzEdit !== null) {
|
|
3104
|
+
const r = spliceInsert(tzValueRef.current, tzCaretRef.current, text);
|
|
3105
|
+
tzValueRef.current = r.value;
|
|
3106
|
+
tzCaretRef.current = r.caret;
|
|
3107
|
+
setTzEdit(r.value);
|
|
3108
|
+
setTzCaret(r.caret);
|
|
3109
|
+
setTzError(null);
|
|
3110
|
+
} else if (tab === 1 && searchMode) {
|
|
3111
|
+
const r = spliceInsert(searchValueRef.current, searchCaretRef.current, text);
|
|
3112
|
+
searchValueRef.current = r.value;
|
|
3113
|
+
searchCaretRef.current = r.caret;
|
|
3114
|
+
setSearch(r.value);
|
|
3115
|
+
setSearchCaret(r.caret);
|
|
3116
|
+
}
|
|
3117
|
+
};
|
|
3118
|
+
const { handlePasteData, isPasteInput } = usePaste(insertText);
|
|
3119
|
+
const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
|
|
3120
|
+
const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
|
|
3121
|
+
const tableAccounts = useMemo2(
|
|
3122
|
+
() => effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [],
|
|
3123
|
+
[accounts, effTableProvider]
|
|
3124
|
+
);
|
|
3125
|
+
const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
|
|
3126
|
+
const tableAccountIds = useMemo2(() => tableAccounts.map((a) => a.id), [tableAccounts]);
|
|
3127
|
+
const table = useMemo2(
|
|
3128
|
+
() => connected ? pickTable(snapshot, tableAccountIds) : tableLocal,
|
|
3129
|
+
[connected, snapshot, tableAccountIds, tableLocal]
|
|
3130
|
+
);
|
|
3131
|
+
const cursorRows = useMemo2(
|
|
3132
|
+
() => connected ? toCursorRows(snapshot, tableAccounts[0]?.id) : cursorRowsLocal,
|
|
3133
|
+
[connected, snapshot, tableAccounts, cursorRowsLocal]
|
|
3134
|
+
);
|
|
3135
|
+
const { showLoader } = useLoader({
|
|
3136
|
+
configReady,
|
|
3137
|
+
showPicker,
|
|
3138
|
+
accountsKey,
|
|
3139
|
+
allReady,
|
|
3140
|
+
tooSmall: TOO_SMALL,
|
|
3141
|
+
showSettings,
|
|
3142
|
+
accountsCount: accounts.length
|
|
3143
|
+
});
|
|
3144
|
+
const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
|
|
3145
|
+
const onboardEnabled = onboardSel ?? detected;
|
|
3146
|
+
const onboardItems = pickerProviders.map((pid) => ({
|
|
3147
|
+
id: pid,
|
|
3148
|
+
name: PROVIDERS[pid].name,
|
|
3149
|
+
color: PROVIDERS[pid].color,
|
|
3150
|
+
detected: detected.includes(pid),
|
|
3151
|
+
enabled: onboardEnabled.includes(pid)
|
|
3152
|
+
}));
|
|
3153
|
+
useEffect7(() => {
|
|
3154
|
+
if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
|
|
3155
|
+
detectProviders().then(setDetected);
|
|
3156
|
+
}, []);
|
|
2920
3157
|
const tableKey = useMemo2(
|
|
2921
3158
|
() => `${effTableProvider}|${tableAccounts.map(acctKey).join(",")}|${tz}`,
|
|
2922
3159
|
[effTableProvider, tableAccounts, tz]
|
|
2923
3160
|
);
|
|
2924
|
-
|
|
3161
|
+
useEffect7(() => {
|
|
2925
3162
|
setTable(null);
|
|
2926
3163
|
setCursorRows(null);
|
|
2927
3164
|
setCursor(0);
|
|
@@ -2929,7 +3166,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2929
3166
|
setSort(tableIsCursor ? 0 : 1);
|
|
2930
3167
|
setTableLoading(false);
|
|
2931
3168
|
}, [tableKey]);
|
|
2932
|
-
|
|
3169
|
+
useEffect7(() => {
|
|
2933
3170
|
if (!degraded || tab !== 1 || !effTableProvider) return;
|
|
2934
3171
|
let active = true;
|
|
2935
3172
|
let timer;
|
|
@@ -2962,68 +3199,23 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
2962
3199
|
clearTimeout(timer);
|
|
2963
3200
|
};
|
|
2964
3201
|
}, [degraded, tab, tableKey, interval]);
|
|
2965
|
-
|
|
3202
|
+
useEffect7(() => {
|
|
2966
3203
|
setCursor(0);
|
|
2967
3204
|
setExpanded(-1);
|
|
2968
3205
|
}, [search]);
|
|
2969
|
-
|
|
3206
|
+
useEffect7(() => {
|
|
2970
3207
|
setDashPage((p) => Math.min(p, dashPageCount - 1));
|
|
2971
3208
|
}, [dashPageCount]);
|
|
2972
|
-
|
|
3209
|
+
useEffect7(() => {
|
|
2973
3210
|
setSettingsCursor((c) => Math.max(-1, Math.min(c, settingsRowCount - 1)));
|
|
2974
3211
|
}, [settingsRowCount]);
|
|
2975
|
-
const resetView =
|
|
3212
|
+
const resetView = useCallback4(() => {
|
|
2976
3213
|
setCursor(0);
|
|
2977
3214
|
setExpanded(-1);
|
|
2978
3215
|
}, []);
|
|
2979
3216
|
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
3217
|
const mouse = useMouse();
|
|
3026
|
-
|
|
3218
|
+
useEffect7(() => {
|
|
3027
3219
|
if (!IS_TTY) return;
|
|
3028
3220
|
mouse.enable();
|
|
3029
3221
|
if (process.stdout.isTTY) {
|
|
@@ -3051,7 +3243,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3051
3243
|
process.stdin.off("data", onData);
|
|
3052
3244
|
};
|
|
3053
3245
|
}, []);
|
|
3054
|
-
const updateConfig =
|
|
3246
|
+
const updateConfig = useCallback4((fn) => {
|
|
3055
3247
|
setConfig((prev) => {
|
|
3056
3248
|
const next = normalizeConfig(fn(prev ?? DEFAULTS));
|
|
3057
3249
|
pendingLocalConfigRef.current = connected ? next : null;
|
|
@@ -3068,7 +3260,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3068
3260
|
});
|
|
3069
3261
|
}, [connected, daemon]);
|
|
3070
3262
|
const daemonConfig = daemon.config;
|
|
3071
|
-
|
|
3263
|
+
useEffect7(() => {
|
|
3072
3264
|
if (!connected || !daemonConfig) return;
|
|
3073
3265
|
setConfig((prev) => {
|
|
3074
3266
|
const reconciled = reconcileDaemonConfig(prev, daemonConfig, pendingLocalConfigRef.current);
|
|
@@ -3112,13 +3304,13 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3112
3304
|
setOnboardSel(null);
|
|
3113
3305
|
setOnboardCursor(0);
|
|
3114
3306
|
}
|
|
3115
|
-
const cycleAccount =
|
|
3307
|
+
const cycleAccount = useCallback4((dir) => {
|
|
3116
3308
|
if (slots.length <= 1) return;
|
|
3117
3309
|
const next = (activeSlotIdx + dir + slots.length) % slots.length;
|
|
3118
3310
|
updateConfig((c) => ({ ...c, activeAccountId: slots[next].id }));
|
|
3119
3311
|
resetView();
|
|
3120
3312
|
}, [slots, activeSlotIdx, updateConfig, resetView]);
|
|
3121
|
-
const cycleTableProvider =
|
|
3313
|
+
const cycleTableProvider = useCallback4((dir) => {
|
|
3122
3314
|
if (tableProvs.length <= 1) return;
|
|
3123
3315
|
const cur = effTableProvider ? tableProvs.indexOf(effTableProvider) : 0;
|
|
3124
3316
|
const nextProv = tableProvs[(cur + dir + tableProvs.length) % tableProvs.length];
|
|
@@ -3131,98 +3323,19 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3131
3323
|
setSearchCaret(0);
|
|
3132
3324
|
setSearchMode(false);
|
|
3133
3325
|
}, [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
|
-
}
|
|
3326
|
+
const {
|
|
3327
|
+
accountForm,
|
|
3328
|
+
setAccountForm,
|
|
3329
|
+
openAddAccount,
|
|
3330
|
+
openConfigureAccount,
|
|
3331
|
+
openEditAccount,
|
|
3332
|
+
commitAccountForm,
|
|
3333
|
+
cycleFormField,
|
|
3334
|
+
cycleProvider,
|
|
3335
|
+
cycleColor,
|
|
3336
|
+
deleteAccount,
|
|
3337
|
+
moveAccount
|
|
3338
|
+
} = useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSettingsCursor });
|
|
3226
3339
|
async function toggleWeb() {
|
|
3227
3340
|
if (connected) {
|
|
3228
3341
|
if (baseUrl) openUrl(baseUrl);
|
|
@@ -3235,7 +3348,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3235
3348
|
if (webStartingRef.current) return;
|
|
3236
3349
|
webStartingRef.current = true;
|
|
3237
3350
|
try {
|
|
3238
|
-
const { startWebServer } = await import("./server-
|
|
3351
|
+
const { startWebServer } = await import("./server-F7CUK4ZM.js");
|
|
3239
3352
|
const ctrl = await startWebServer({ config: cfg, log: false });
|
|
3240
3353
|
webRef.current = ctrl;
|
|
3241
3354
|
openUrl(ctrl.url);
|
|
@@ -3244,15 +3357,15 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3244
3357
|
webStartingRef.current = false;
|
|
3245
3358
|
}
|
|
3246
3359
|
}
|
|
3247
|
-
const onTabSelect =
|
|
3360
|
+
const onTabSelect = useCallback4((i) => {
|
|
3248
3361
|
setTab(i);
|
|
3249
3362
|
resetView();
|
|
3250
3363
|
}, [resetView]);
|
|
3251
|
-
const onStripSelect =
|
|
3364
|
+
const onStripSelect = useCallback4((i) => {
|
|
3252
3365
|
updateConfig((c) => ({ ...c, activeAccountId: slots[i].id }));
|
|
3253
3366
|
resetView();
|
|
3254
3367
|
}, [slots, updateConfig, resetView]);
|
|
3255
|
-
const onProviderSelect =
|
|
3368
|
+
const onProviderSelect = useCallback4((p) => {
|
|
3256
3369
|
setTableProvider(p);
|
|
3257
3370
|
setCursor(0);
|
|
3258
3371
|
setExpanded(-1);
|
|
@@ -3260,11 +3373,11 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3260
3373
|
setSearchCaret(0);
|
|
3261
3374
|
setSearchMode(false);
|
|
3262
3375
|
}, []);
|
|
3263
|
-
const onRowClickToken =
|
|
3376
|
+
const onRowClickToken = useCallback4((idx) => {
|
|
3264
3377
|
if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
|
|
3265
3378
|
else setCursor(idx);
|
|
3266
3379
|
}, [cursor]);
|
|
3267
|
-
const onRowClickCursor =
|
|
3380
|
+
const onRowClickCursor = useCallback4((idx) => setCursor(idx), []);
|
|
3268
3381
|
const tokenRows = useMemo2(
|
|
3269
3382
|
() => tab === 1 && !tableIsCursor ? sortRows(filterTokenRows(table ? [table.daily, table.weekly, table.monthly][view] : [], search), sort) : [],
|
|
3270
3383
|
[tab, tableIsCursor, table, view, search, sort]
|