tokmon 0.20.6 → 0.22.0

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