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.
@@ -18,7 +18,7 @@ import {
18
18
  systemTimezone,
19
19
  time,
20
20
  tokens
21
- } from "./chunk-YWJT3OG4.js";
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-XQEJ4WQ5.js";
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 useState4, useEffect as useEffect4, useCallback, useRef as useRef3, useMemo as useMemo2 } from "react";
46
- import { Box as Box11, Text as Text11, useInput, useStdout, useApp } from "ink";
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 chooseLayout(content, budget, n, single, cols) {
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, CARD_H[variant]) <= budget) {
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
- let shown = groups;
316
- if (layout === "single" && focusId === null) shown = groups.slice(0, 1);
317
- const single = focusId !== null || layout === "single";
318
- const content = Math.max(MIN_CARD, cols - 4);
319
- const { ncols, variant, cardsPerPage, pageCount } = chooseLayout(content, budget, shown.length, single, cols);
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 ? shown.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : shown;
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, barW })
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 LimitsBlock({ items, barW }) {
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.name, 22) })
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: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
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: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
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/app.tsx
2576
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2577
- var DEBOUNCE_MS = 300;
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
- function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = null, mode = "degraded" }) {
2610
- const connected = mode === "connected" && baseUrl !== null && wsToken !== null;
2611
- const degraded = !connected;
2612
- const daemon = useDaemon(connected ? baseUrl : null, connected ? wsToken : null);
2613
- const [config, setConfig] = useState4(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
2614
- const [detected, setDetected] = useState4([]);
2615
- const [statsLocal, setStats] = useState4(/* @__PURE__ */ new Map());
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
- const tzValueRef = useRef3("");
2679
- const tzCaretRef = useRef3(0);
2680
- const searchValueRef = useRef3("");
2681
- const searchCaretRef = useRef3(0);
2682
- const accountsKey = useMemo2(() => accounts.map(acctKey).join("|"), [accounts]);
2683
- const snapshot = daemon.snapshot;
2684
- const stats = useMemo2(
2685
- () => connected ? toStatsMap(snapshot, accounts) : statsLocal,
2686
- [connected, snapshot, accounts, statsLocal]
2687
- );
2688
- const accountIdentities = useMemo2(() => {
2689
- const out = /* @__PURE__ */ new Map();
2690
- for (const [id, stat] of stats) {
2691
- const billing = stat.billing;
2692
- if (!billing) continue;
2693
- out.set(id, {
2694
- email: billing.email ?? null,
2695
- displayName: billing.displayName ?? null,
2696
- plan: billing.plan ?? null
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
- return out;
2700
- }, [stats]);
2701
- const showPeak = accounts.some((a) => a.providerId === "claude");
2702
- const peak = connected ? showPeak ? snapshot?.peak ?? null : null : peakLocal;
2703
- const updated = useMemo2(
2704
- () => connected ? new Date(snapshot?.generatedAt ?? Date.now()) : updatedLocal,
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
- return statsReadyInput(statsLocal.get(id));
2715
- }, [connected, snapshot, statsLocal]);
2716
- const slots = useMemo2(() => deriveSlots(accounts), [accounts]);
2717
- const { activeSlotIdx, focusId } = useMemo2(
2718
- () => findActiveSlot(slots, cfg.activeAccountId),
2719
- [slots, cfg.activeAccountId]
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
- insertPasteRef.current = insertText;
2772
- const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
2773
- const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
2774
- const tableAccounts = useMemo2(
2775
- () => effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [],
2776
- [accounts, effTableProvider]
2777
- );
2778
- const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
2779
- const tableAccountIds = useMemo2(() => tableAccounts.map((a) => a.id), [tableAccounts]);
2780
- const table = useMemo2(
2781
- () => connected ? pickTable(snapshot, tableAccountIds) : tableLocal,
2782
- [connected, snapshot, tableAccountIds, tableLocal]
2783
- );
2784
- const cursorRows = useMemo2(
2785
- () => connected ? toCursorRows(snapshot, tableAccounts[0]?.id) : cursorRowsLocal,
2786
- [connected, snapshot, tableAccounts, cursorRowsLocal]
2787
- );
2788
- const needsOnboarding = configReady && !cfg.onboarded;
2789
- const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
2790
- const showPicker = needsOnboarding || newProviders.length > 0;
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 && !TOO_SMALL && accounts.length > 0 && (!allReady || graceHold || minVisibleHold) && (debouncePassed || loaderShownAt !== null) && !loaderDoneState;
2793
- const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
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
- useEffect4(() => {
2717
+ useEffect5(() => {
2813
2718
  if (showLoader && loaderShownAt === null) setLoaderShownAt(Date.now());
2814
2719
  }, [showLoader, loaderShownAt]);
2815
- useEffect4(() => {
2816
- if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
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
- useEffect4(() => {
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
- useEffect4(() => {
2864
- if (!degraded || !configReady || showPicker) return;
2865
- let active = true;
2866
- let timer;
2867
- const load = async () => {
2868
- try {
2869
- await Promise.all(accountsRef.current.map(async (acc) => {
2870
- const provider = PROVIDERS[acc.providerId];
2871
- if (!provider.hasUsage || !provider.fetchSummary) return;
2872
- try {
2873
- const dashboard = await provider.fetchSummary(acc, tz);
2874
- if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
2875
- } catch {
2876
- }
2877
- }));
2878
- if (active) {
2879
- setError(null);
2880
- setUpdated(/* @__PURE__ */ new Date());
2881
- }
2882
- } finally {
2883
- if (active) timer = setTimeout(load, interval);
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
- useEffect4(() => {
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
- useEffect4(() => {
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
- useEffect4(() => {
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
- useEffect4(() => {
3202
+ useEffect7(() => {
2966
3203
  setCursor(0);
2967
3204
  setExpanded(-1);
2968
3205
  }, [search]);
2969
- useEffect4(() => {
3206
+ useEffect7(() => {
2970
3207
  setDashPage((p) => Math.min(p, dashPageCount - 1));
2971
3208
  }, [dashPageCount]);
2972
- useEffect4(() => {
3209
+ useEffect7(() => {
2973
3210
  setSettingsCursor((c) => Math.max(-1, Math.min(c, settingsRowCount - 1)));
2974
3211
  }, [settingsRowCount]);
2975
- const resetView = useCallback(() => {
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
- useEffect4(() => {
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 = useCallback((fn) => {
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
- useEffect4(() => {
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 = useCallback((dir) => {
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 = useCallback((dir) => {
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
- function openAddAccount(defaults) {
3135
- const providerId = defaults?.providerId ?? (detected[0] ?? "claude");
3136
- setAccountForm({
3137
- mode: "add",
3138
- field: "provider",
3139
- providerId,
3140
- name: defaults?.name ?? "",
3141
- homeDir: defaults?.homeDir ?? "~",
3142
- color: defaults?.color ?? pickAccentColor(cfg.accounts),
3143
- caret: defaults?.name?.length ?? 0,
3144
- editingId: null,
3145
- error: null
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-TSTBMVTJ.js");
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 = useCallback((i) => {
3360
+ const onTabSelect = useCallback4((i) => {
3248
3361
  setTab(i);
3249
3362
  resetView();
3250
3363
  }, [resetView]);
3251
- const onStripSelect = useCallback((i) => {
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 = useCallback((p) => {
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 = useCallback((idx) => {
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 = useCallback((idx) => setCursor(idx), []);
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]