tokmon 0.22.6 → 0.23.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.
Files changed (57) hide show
  1. package/README.md +5 -5
  2. package/dist/{bootstrap-ink-CHJWH5XC.js → bootstrap-ink-UD3KDTOB.js} +1113 -934
  3. package/dist/{chunk-FO7IUYCM.js → chunk-AON6GE7D.js} +368 -280
  4. package/dist/{chunk-QTHCHB7S.js → chunk-DJPUYMZM.js} +52 -18
  5. package/dist/chunk-HV6AYQDB.js +192 -0
  6. package/dist/chunk-HXDAOOSI.js +154 -0
  7. package/dist/{chunk-AQNFQRWV.js → chunk-PGQK7MOE.js} +2583 -1711
  8. package/dist/cli.js +93 -54
  9. package/dist/{config-64N7KBZ7.js → config-5CNJN3P7.js} +1 -1
  10. package/dist/daemon-AW3YHFSJ.js +193 -0
  11. package/dist/daemon-handle-EYAXNNKZ.js +126 -0
  12. package/dist/server-4CDZMLH6.js +10 -0
  13. package/dist/web/assets/Area-DGo9K-U3.js +2 -0
  14. package/dist/web/assets/analytics-BOhbhV5Z.js +2 -0
  15. package/dist/web/assets/breakdown-CO04nSN-.js +4 -0
  16. package/dist/web/assets/chart-bNxdfXxA.js +69 -0
  17. package/dist/web/assets/explore-sZnmjzAj.js +22 -0
  18. package/dist/web/assets/index-DSj-Bxov.css +1 -0
  19. package/dist/web/assets/index-DnDr1VUr.js +83 -0
  20. package/dist/web/assets/models-Cl6D8VVF.js +2 -0
  21. package/dist/web/assets/overview-C18nvaAK.js +2 -0
  22. package/dist/web/assets/panel-DTjMnHaz.js +1 -0
  23. package/dist/web/assets/primitives-Dj_9YxTF.js +1 -0
  24. package/dist/web/assets/settings-sheet-0BGb5Ahv.js +1 -0
  25. package/dist/web/assets/share-sheet-BHtt-OmT.js +2 -0
  26. package/dist/web/assets/timeline-DmzB8Z3W.js +1 -0
  27. package/dist/web/assets/use-dialog-trap-DeXFtxx8.js +1 -0
  28. package/dist/web/index.html +3 -2
  29. package/package.json +10 -6
  30. package/dist/daemon-ATXHNYTK.js +0 -213
  31. package/dist/daemon-handle-HLSKLMWU.js +0 -142
  32. package/dist/server-56C7MMEZ.js +0 -9
  33. package/dist/web/assets/breakdown-Bj0BAVYf.js +0 -4
  34. package/dist/web/assets/index-BCaPGYQA.css +0 -1
  35. package/dist/web/assets/index-Dziiggw_.js +0 -173
  36. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  37. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  38. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  39. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  40. package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  41. package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  42. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  43. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  44. package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  45. package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  46. package/dist/web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  47. package/dist/web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  48. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  49. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  50. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  51. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  52. package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  53. package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  54. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  55. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  56. package/dist/web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  57. package/dist/web/assets/timeline-BQpqTU-a.js +0 -1
@@ -10,7 +10,6 @@ import {
10
10
  coalesceTables,
11
11
  col,
12
12
  currency,
13
- cursorModelSpend,
14
13
  detectProviders,
15
14
  fetchPeak,
16
15
  modelColor,
@@ -20,7 +19,10 @@ import {
20
19
  time,
21
20
  tokens,
22
21
  withTimeout
23
- } from "./chunk-AQNFQRWV.js";
22
+ } from "./chunk-PGQK7MOE.js";
23
+ import {
24
+ glyphs
25
+ } from "./chunk-RF4GGQGM.js";
24
26
  import {
25
27
  COLOR_PALETTE,
26
28
  DEFAULTS,
@@ -35,54 +37,17 @@ import {
35
37
  sanitizeTyped,
36
38
  saveConfigSync,
37
39
  snapshotCacheFile
38
- } from "./chunk-QTHCHB7S.js";
39
- import {
40
- glyphs
41
- } from "./chunk-RF4GGQGM.js";
40
+ } from "./chunk-DJPUYMZM.js";
42
41
 
43
42
  // src/bootstrap-ink.tsx
44
43
  import { render } from "ink";
45
44
  import { MouseProvider } from "@zenobius/ink-mouse";
46
45
 
47
46
  // src/app.tsx
48
- import { useState as useState8, useEffect as useEffect7, useCallback as useCallback4, useRef as useRef6, useMemo as useMemo2 } from "react";
49
- import { Box as Box11, Text as Text11, useInput, useApp } from "ink";
47
+ import { useState as useState10, useEffect as useEffect9, useCallback as useCallback7, useRef as useRef8, useMemo as useMemo2 } from "react";
48
+ import { Box as Box11, Text as Text12, useInput, useApp } from "ink";
50
49
  import { useMouse } from "@zenobius/ink-mouse";
51
50
 
52
- // src/config-sync.ts
53
- function deepEqual(a, b) {
54
- if (a === b) return true;
55
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
56
- if (Array.isArray(a) || Array.isArray(b)) {
57
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
58
- for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
59
- return true;
60
- }
61
- const ka = Object.keys(a);
62
- const kb = Object.keys(b);
63
- if (ka.length !== kb.length) return false;
64
- for (const k of ka) {
65
- if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
66
- if (!deepEqual(a[k], b[k])) return false;
67
- }
68
- return true;
69
- }
70
- function reconcileDaemonConfig(previous, daemonConfig, pendingLocalConfig) {
71
- if (pendingLocalConfig) {
72
- if (deepEqual(daemonConfig, pendingLocalConfig)) {
73
- return { config: previous && deepEqual(previous, daemonConfig) ? previous : daemonConfig, pendingLocalConfig: null };
74
- }
75
- return { config: previous ?? pendingLocalConfig, pendingLocalConfig };
76
- }
77
- if (previous && deepEqual(previous, daemonConfig)) {
78
- return { config: previous, pendingLocalConfig: null };
79
- }
80
- if (previous?.onboarded === true && daemonConfig.onboarded === false) {
81
- return { config: previous, pendingLocalConfig: null };
82
- }
83
- return { config: daemonConfig, pendingLocalConfig: null };
84
- }
85
-
86
51
  // src/ui/shared.tsx
87
52
  import { appendFileSync } from "fs";
88
53
  import { memo, useEffect, useRef, useState } from "react";
@@ -1246,62 +1211,6 @@ function RowDetail({ row, indent }) {
1246
1211
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: currency(m.cost) })
1247
1212
  ] }, m.name)) });
1248
1213
  }
1249
- var CursorSpendTable = memo4(function CursorSpendTable2({ rows, cursor, maxRows, onRowClick }) {
1250
- if (rows.length === 0) return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No Cursor spend recorded locally." });
1251
- const total = rows.reduce((a, r) => a + r.usd, 0);
1252
- const totalAmt = rows.reduce((a, r) => a + r.requests, 0);
1253
- const clamped = Math.min(cursor, rows.length - 1);
1254
- const scrollStart = Math.max(0, Math.min(clamped - Math.floor(maxRows / 2), rows.length - maxRows));
1255
- const visible = rows.slice(scrollStart, scrollStart + maxRows);
1256
- const W = { model: 34, cost: 12, amount: 12, share: 8 };
1257
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1258
- /* @__PURE__ */ jsxs4(Text4, { children: [
1259
- /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
1260
- " ",
1261
- col("Model", W.model, "left")
1262
- ] }),
1263
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Cost", W.cost) }),
1264
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Amount", W.amount) }),
1265
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: col("Share", W.share) })
1266
- ] }),
1267
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1268
- visible.map((r, vi) => {
1269
- const idx = scrollStart + vi;
1270
- const selected = idx === clamped;
1271
- const share = total > 0 ? r.usd / total * 100 : 0;
1272
- return /* @__PURE__ */ jsx4(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs4(Text4, { inverse: selected, children: [
1273
- /* @__PURE__ */ jsxs4(Text4, { color: selected ? void 0 : "magenta", children: [
1274
- selected ? `${glyphs().caretR} ` : " ",
1275
- col(r.name, W.model, "left")
1276
- ] }),
1277
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.usd), W.cost) }),
1278
- /* @__PURE__ */ jsx4(Text4, { children: col(tokens(r.requests), W.amount) }),
1279
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: col(share.toFixed(1) + "%", W.share) })
1280
- ] }) }, r.name);
1281
- }),
1282
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1283
- /* @__PURE__ */ jsxs4(Text4, { children: [
1284
- /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "greenBright", children: [
1285
- " ",
1286
- col("Total", W.model, "left")
1287
- ] }),
1288
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellowBright", children: col(currency(total), W.cost) }),
1289
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: col(tokens(totalAmt), W.amount) }),
1290
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: col("100%", W.share) })
1291
- ] }),
1292
- /* @__PURE__ */ jsx4(Box4, { height: 1 }),
1293
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1294
- "local spend by model (composerData) ",
1295
- glyphs().middot,
1296
- " est. API-equivalent ",
1297
- glyphs().middot,
1298
- " ",
1299
- clamped + 1,
1300
- "/",
1301
- rows.length
1302
- ] })
1303
- ] });
1304
- });
1305
1214
  function fmtLabel(label) {
1306
1215
  if (label.length === 7 && label[4] === "-") {
1307
1216
  return `${MONTHS[Number(label.slice(5, 7))]} '${label.slice(2, 4)}`;
@@ -1598,7 +1507,7 @@ function osc8(text, url) {
1598
1507
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1599
1508
  var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1600
1509
  const inner = cols - 4;
1601
- const BASE = "by David Ilie (davidilie.com) \xB7 O=repo W=web s=settings q=quit".length;
1510
+ const BASE = "by David Ilie (davidilie.com) \xB7 r/R=refresh O=repo W=web s=settings q=quit".length;
1602
1511
  const optHint = (glyphs().shift === "\u21E7" ? "\u2325" : "opt") + "-click links ";
1603
1512
  const OPT = IS_APPLE_TERMINAL ? optHint.length : 0;
1604
1513
  const JUMP = "0-9=jump a/A=cycle ".length;
@@ -1607,7 +1516,7 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1607
1516
  const showJump = hasAccounts && inner >= BASE + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
1608
1517
  const showPage = paginated && inner >= BASE + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
1609
1518
  if (inner < BASE) {
1610
- 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" }) });
1519
+ return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, flexWrap: "nowrap", children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-end", children: "r/R=refresh O=repo W=web s=settings q=quit" }) });
1611
1520
  }
1612
1521
  return /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexWrap: "nowrap", children: [
1613
1522
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "by " }),
@@ -1617,7 +1526,7 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1617
1526
  /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1618
1527
  ") ",
1619
1528
  glyphs().middot,
1620
- " O=repo W=web s=settings "
1529
+ " r/R=refresh O=repo W=web s=settings "
1621
1530
  ] }),
1622
1531
  showOpt && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: optHint }),
1623
1532
  showJump && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "0-9=jump a/A=cycle " }),
@@ -1627,30 +1536,52 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1627
1536
  });
1628
1537
 
1629
1538
  // src/ui/tiny-fallback.tsx
1630
- import { Box as Box10, Text as Text10 } from "ink";
1539
+ import { Box as Box10, Text as Text11 } from "ink";
1540
+
1541
+ // src/ui/refresh-status.tsx
1542
+ import { memo as memo7 } from "react";
1543
+ import { Text as Text10 } from "ink";
1631
1544
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1632
- function TinyFallback({ groups, stats, rows, cols }) {
1545
+ var RefreshStatusLine = memo7(function RefreshStatusLine2({ status }) {
1546
+ if (status.phase === "idle") return null;
1547
+ if (status.phase === "refreshing") return /* @__PURE__ */ jsx10(Spinner, { label: `${status.message}${glyphs().ellipsis}` });
1548
+ if (status.phase === "success") return /* @__PURE__ */ jsxs10(Text10, { color: "green", children: [
1549
+ glyphs().check,
1550
+ " ",
1551
+ status.message
1552
+ ] });
1553
+ return /* @__PURE__ */ jsxs10(Text10, { color: "red", children: [
1554
+ glyphs().warn,
1555
+ " ",
1556
+ status.message
1557
+ ] });
1558
+ });
1559
+
1560
+ // src/ui/tiny-fallback.tsx
1561
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1562
+ function TinyFallback({ groups, stats, rows, cols, refreshStatus }) {
1633
1563
  const maxLines = Math.max(1, rows - 4);
1634
1564
  const visible = groups.slice(0, maxLines);
1635
1565
  const hidden = groups.length - visible.length;
1636
1566
  const w = Math.max(8, cols - 2);
1637
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
1638
- /* @__PURE__ */ jsxs10(Text10, { bold: true, color: "greenBright", children: [
1567
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
1568
+ /* @__PURE__ */ jsxs11(Text11, { bold: true, color: "greenBright", children: [
1639
1569
  glyphs().dotSel,
1640
1570
  " tokmon"
1641
1571
  ] }),
1642
- groups.length === 0 ? /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
1572
+ groups.length === 0 ? /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
1643
1573
  "No providers ",
1644
1574
  glyphs().emDash,
1645
1575
  " s=settings"
1646
- ] }) : visible.map((g) => /* @__PURE__ */ jsx10(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
1647
- hidden > 0 && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
1576
+ ] }) : visible.map((g) => /* @__PURE__ */ jsx11(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
1577
+ hidden > 0 && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
1648
1578
  "+",
1649
1579
  hidden,
1650
1580
  " more (enlarge terminal)"
1651
1581
  ] }),
1652
- /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1653
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "s=settings q=quit" })
1582
+ /* @__PURE__ */ jsx11(RefreshStatusLine, { status: refreshStatus }),
1583
+ /* @__PURE__ */ jsx11(Box10, { flexGrow: 1 }),
1584
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "R=refresh s=settings q=quit" })
1654
1585
  ] });
1655
1586
  }
1656
1587
  function TinyRow({ provider, accounts, stats, width }) {
@@ -1661,14 +1592,14 @@ function TinyRow({ provider, accounts, stats, width }) {
1661
1592
  const pctMetric = billings.flatMap((b) => b?.metrics ?? []).find((m) => m.format.kind === "percent");
1662
1593
  const detail = meta.hasUsage ? `${currency(todayCost)} today` : pctMetric ? `${Math.round(pctMetric.used)}%` : "billing";
1663
1594
  const name = truncateName(meta.name, Math.max(4, width - 18));
1664
- return /* @__PURE__ */ jsxs10(Box10, { width, children: [
1665
- /* @__PURE__ */ jsxs10(Text10, { color: meta.color, children: [
1595
+ return /* @__PURE__ */ jsxs11(Box10, { width, children: [
1596
+ /* @__PURE__ */ jsxs11(Text11, { color: meta.color, children: [
1666
1597
  glyphs().dot,
1667
1598
  " "
1668
1599
  ] }),
1669
- /* @__PURE__ */ jsx10(Text10, { bold: true, color: meta.color, children: name }),
1670
- /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1671
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: detail })
1600
+ /* @__PURE__ */ jsx11(Text11, { bold: true, color: meta.color, children: name }),
1601
+ /* @__PURE__ */ jsx11(Box10, { flexGrow: 1 }),
1602
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: detail })
1672
1603
  ] });
1673
1604
  }
1674
1605
 
@@ -1692,6 +1623,8 @@ function toWsUrl(baseUrl, token) {
1692
1623
  if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1693
1624
  throw new Error(`unsupported daemon RPC protocol: ${url.protocol}`);
1694
1625
  }
1626
+ url.hash = "";
1627
+ url.searchParams.delete("tokmonToken");
1695
1628
  url.pathname = TOKMON_WS_PATH;
1696
1629
  if (token) url.searchParams.set("wsToken", token);
1697
1630
  return url.toString();
@@ -1878,12 +1811,12 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1878
1811
  };
1879
1812
  };
1880
1813
  return {
1881
- getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})),
1882
- setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)),
1814
+ getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})).then((state) => state),
1815
+ setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)).then((state) => state),
1883
1816
  refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
1884
1817
  browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
1885
- subscribeSnapshot: (onSnapshot) => subscribe((client) => client[TOKMON_WS_METHODS.snapshot]({}), onSnapshot),
1886
- subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}), onConfig),
1818
+ subscribeSnapshot: (onSnapshot) => subscribe((client) => client[TOKMON_WS_METHODS.snapshot]({}).pipe(Stream.map((value) => value)), onSnapshot),
1819
+ subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}).pipe(Stream.map((value) => value)), onConfig),
1887
1820
  async close() {
1888
1821
  if (closed) return;
1889
1822
  closed = true;
@@ -1940,7 +1873,7 @@ function useDaemon(baseUrl, wsToken) {
1940
1873
  snapshot,
1941
1874
  conn,
1942
1875
  config,
1943
- setConfig: (next) => requireClient().setConfig(next),
1876
+ setConfig: (next, expectedRevision) => requireClient().setConfig({ config: next, expectedRevision }),
1944
1877
  refresh: (scope) => requireClient().refresh(scope),
1945
1878
  browse: (path) => requireClient().browseFs(path)
1946
1879
  };
@@ -1965,15 +1898,6 @@ function toStatsMap(snapshot, accounts) {
1965
1898
  }
1966
1899
  return out;
1967
1900
  }
1968
- function toCursorRows(snapshot, accountId) {
1969
- if (!snapshot || !accountId) return null;
1970
- const wa = snapshot.accounts.find((a) => a.id === accountId);
1971
- if (!wa) return null;
1972
- if (wa.billingState === "pending") return null;
1973
- const spend = wa.billing?.modelSpend;
1974
- if (!spend) return [];
1975
- return spend.map((m) => ({ name: m.name, usd: m.usd, requests: m.requests }));
1976
- }
1977
1901
  function pickTable(snapshot, accountIds) {
1978
1902
  if (!snapshot) return null;
1979
1903
  const byId = indexById(snapshot);
@@ -1994,11 +1918,6 @@ var SORTS = [
1994
1918
  { label: "cost", dir: "up" },
1995
1919
  { label: "cost", dir: "down" }
1996
1920
  ];
1997
- var CURSOR_SORTS = [
1998
- { label: "cost", dir: "down" },
1999
- { label: "amount", dir: "down" },
2000
- { label: "model", dir: null }
2001
- ];
2002
1921
  var acctKey = (a) => `${a.id}:${a.homeDir ?? ""}`;
2003
1922
  var clampCaret = (caret, len) => Math.max(0, Math.min(caret, len));
2004
1923
  function spliceInsert(value, caret, text) {
@@ -2021,19 +1940,6 @@ function upsert(prev, account, patch) {
2021
1940
  next.set(account.id, { ...cur, account, ...patch });
2022
1941
  return next;
2023
1942
  }
2024
- async function fetchScopeTable(scope, tz) {
2025
- const tables = await Promise.all(scope.map(async (acc) => {
2026
- const provider = PROVIDERS[acc.providerId];
2027
- if (!provider.fetchTable) return null;
2028
- try {
2029
- return await provider.fetchTable(acc, tz);
2030
- } catch {
2031
- return null;
2032
- }
2033
- }));
2034
- const valid = tables.filter((t) => t !== null);
2035
- return coalesceTables(valid);
2036
- }
2037
1943
  function sortLabel(entry) {
2038
1944
  if (entry.dir === "up") return `${entry.label} ${glyphs().arrowU}`;
2039
1945
  if (entry.dir === "down") return `${entry.label} ${glyphs().arrowD}`;
@@ -2098,579 +2004,567 @@ function filterTokenRows(rows, q) {
2098
2004
  const s = q.toLowerCase();
2099
2005
  return rows.filter((r) => r.label.toLowerCase().includes(s) || r.models.some((m) => m.toLowerCase().includes(s)));
2100
2006
  }
2101
- function filterCursorRows(rows, q) {
2102
- if (!q) return rows;
2103
- const s = q.toLowerCase();
2104
- return rows.filter((r) => r.name.toLowerCase().includes(s));
2105
- }
2106
- function sortCursorRows(rows, sortIdx) {
2107
- const out = [...rows];
2108
- switch (sortIdx % CURSOR_SORTS.length) {
2109
- case 1:
2110
- return out.sort((a, b) => b.requests - a.requests);
2111
- case 2:
2112
- return out.sort((a, b) => a.name.localeCompare(b.name));
2113
- default:
2114
- return out.sort((a, b) => b.usd - a.usd);
2115
- }
2116
- }
2117
2007
 
2118
- // src/ui/keybindings.ts
2119
- function handleKey(input, key, ctx) {
2120
- const {
2121
- showPicker,
2122
- pickerProviders,
2123
- onboardCursor,
2124
- setOnboardCursor,
2125
- toggleOnboard,
2126
- confirmOnboarding,
2127
- exit,
2128
- showSettings,
2129
- accountForm,
2130
- setAccountForm,
2131
- commitAccountForm,
2132
- cycleFormField,
2133
- cycleProvider,
2134
- cycleColor,
2135
- isPrintable,
2136
- insertText,
2137
- tzEdit,
2138
- setTzEdit,
2139
- setTzError,
2140
- updateConfig,
2141
- setTzCaret,
2142
- tzValueRef,
2143
- tzCaretRef,
2144
- tab,
2145
- searchMode,
2146
- setSearchMode,
2147
- search,
2148
- setSearch,
2149
- setSearchCaret,
2150
- searchValueRef,
2151
- searchCaretRef,
2152
- showLoader,
2153
- configReady,
2154
- toggleWeb,
2155
- settingsCursor,
2156
- settingsTab,
2157
- setSettingsTab,
2158
- setShowSettings,
2159
- cfg,
2160
- trackedAccountRows,
2161
- moveAccount,
2162
- setSettingsCursor,
2163
- toggleProvider,
2164
- openEditAccount,
2165
- openConfigureAccount,
2166
- deleteAccount,
2167
- openAddAccount,
2168
- cycleAccount,
2169
- setTab,
2170
- resetView,
2171
- slots,
2172
- dashPaginated,
2173
- dashPageCount,
2174
- setDashPage,
2175
- cycleTableProvider,
2176
- setExpanded,
2177
- setSort,
2178
- SORTS_FOR,
2179
- tableIsCursor,
2180
- cycleTableModel: cycleTableModel2,
2181
- setView,
2182
- cursor,
2183
- rowCountRef,
2184
- rows,
2185
- setCursor,
2186
- clampRow
2187
- } = ctx;
2188
- if (showPicker) {
2189
- if (input === "q") {
2190
- exit();
2191
- return;
2192
- }
2193
- const startIdx = pickerProviders.length;
2194
- if (key.upArrow) {
2195
- setOnboardCursor((c) => Math.max(0, c - 1));
2196
- return;
2197
- }
2198
- if (key.downArrow) {
2199
- setOnboardCursor((c) => Math.min(startIdx, c + 1));
2008
+ // src/ui/keybindings-editors.ts
2009
+ function handleAccountEditor(input, key, editor, text) {
2010
+ const { form, setForm, commit, cycleField, cycleProvider, cycleColor } = editor;
2011
+ if (!form) return;
2012
+ if (key.escape) {
2013
+ setForm(null);
2014
+ return;
2015
+ }
2016
+ if (key.ctrl && input === "s") {
2017
+ commit();
2018
+ return;
2019
+ }
2020
+ if (key.tab) {
2021
+ cycleField(key.shift ? -1 : 1);
2022
+ return;
2023
+ }
2024
+ if (key.upArrow) {
2025
+ cycleField(-1);
2026
+ return;
2027
+ }
2028
+ if (key.downArrow) {
2029
+ cycleField(1);
2030
+ return;
2031
+ }
2032
+ if (form.field === "provider") {
2033
+ if (key.leftArrow) {
2034
+ cycleProvider(-1);
2200
2035
  return;
2201
2036
  }
2202
- if (input === " ") {
2203
- toggleOnboard(onboardCursor);
2037
+ if (key.rightArrow) {
2038
+ cycleProvider(1);
2204
2039
  return;
2205
2040
  }
2206
2041
  if (key.return) {
2207
- if (onboardCursor === startIdx) confirmOnboarding();
2208
- else toggleOnboard(onboardCursor);
2042
+ setForm((value) => value && { ...value, field: "name", caret: value.name.length });
2209
2043
  return;
2210
2044
  }
2211
2045
  return;
2212
2046
  }
2213
- if (showSettings && accountForm) {
2214
- if (key.escape) {
2215
- setAccountForm(null);
2216
- return;
2217
- }
2218
- if (key.ctrl && input === "s") {
2219
- commitAccountForm();
2047
+ if (form.field === "color") {
2048
+ if (key.leftArrow) {
2049
+ cycleColor(-1);
2220
2050
  return;
2221
2051
  }
2222
- if (key.tab) {
2223
- cycleFormField(key.shift ? -1 : 1);
2052
+ if (key.rightArrow) {
2053
+ cycleColor(1);
2224
2054
  return;
2225
2055
  }
2226
- if (key.upArrow) {
2227
- cycleFormField(-1);
2056
+ if (key.return) {
2057
+ commit();
2228
2058
  return;
2229
2059
  }
2230
- if (key.downArrow) {
2231
- cycleFormField(1);
2060
+ return;
2061
+ }
2062
+ const field = form.field;
2063
+ if (key.leftArrow) {
2064
+ setForm((value) => value && { ...value, caret: clampCaret(value.caret - 1, value[field].length) });
2065
+ return;
2066
+ }
2067
+ if (key.rightArrow) {
2068
+ setForm((value) => value && { ...value, caret: clampCaret(value.caret + 1, value[field].length) });
2069
+ return;
2070
+ }
2071
+ if (key.ctrl && input === "a") {
2072
+ setForm((value) => value && { ...value, caret: 0 });
2073
+ return;
2074
+ }
2075
+ if (key.ctrl && input === "e") {
2076
+ setForm((value) => value && { ...value, caret: value[field].length });
2077
+ return;
2078
+ }
2079
+ if (key.return) {
2080
+ if (field === "name" && form.name.trim() === "") {
2081
+ setForm((value) => value && { ...value, error: "Name required", caret: value.name.length });
2232
2082
  return;
2233
2083
  }
2234
- if (accountForm.field === "provider") {
2235
- if (key.leftArrow) {
2236
- cycleProvider(-1);
2237
- return;
2238
- }
2239
- if (key.rightArrow) {
2240
- cycleProvider(1);
2241
- return;
2242
- }
2243
- if (key.return) {
2244
- setAccountForm((f) => f && { ...f, field: "name", caret: f.name.length });
2245
- return;
2246
- }
2247
- return;
2084
+ setForm((value) => value && {
2085
+ ...value,
2086
+ field: field === "name" ? "homeDir" : "color",
2087
+ caret: field === "name" ? value.homeDir.length : value.caret
2088
+ });
2089
+ return;
2090
+ }
2091
+ if (key.backspace || key.delete) {
2092
+ setForm((value) => {
2093
+ if (!value || value.field !== "name" && value.field !== "homeDir") return value;
2094
+ const result = spliceBackspace(value[value.field], value.caret);
2095
+ return { ...value, [value.field]: result.value, caret: result.caret, error: null };
2096
+ });
2097
+ return;
2098
+ }
2099
+ if (text.isPrintable(input, key)) {
2100
+ const clean = sanitizeTyped(input);
2101
+ if (clean) text.insert(clean);
2102
+ }
2103
+ }
2104
+ function handleTimezoneEditor(input, key, editor, text, updateConfig) {
2105
+ const { value, setValue, setError, setCaret, valueRef, caretRef } = editor;
2106
+ if (value === null) return;
2107
+ if (key.escape) {
2108
+ setValue(null);
2109
+ setError(null);
2110
+ return;
2111
+ }
2112
+ if (key.return) {
2113
+ const next = value.trim();
2114
+ if (next === "" || next.toLowerCase() === "system") {
2115
+ updateConfig((config) => ({ ...config, timezone: null }));
2116
+ setValue(null);
2117
+ setError(null);
2118
+ } else if (isValidTimezone(next)) {
2119
+ updateConfig((config) => ({ ...config, timezone: next }));
2120
+ setValue(null);
2121
+ setError(null);
2122
+ } else {
2123
+ setError(`Invalid: ${next}`);
2248
2124
  }
2249
- if (accountForm.field === "color") {
2250
- if (key.leftArrow) {
2251
- cycleColor(-1);
2252
- return;
2253
- }
2254
- if (key.rightArrow) {
2255
- cycleColor(1);
2256
- return;
2257
- }
2258
- if (key.return) {
2259
- commitAccountForm();
2260
- return;
2261
- }
2262
- return;
2125
+ return;
2126
+ }
2127
+ if (key.leftArrow) {
2128
+ setCaret((caret) => clampCaret(caret - 1, value.length));
2129
+ return;
2130
+ }
2131
+ if (key.rightArrow) {
2132
+ setCaret((caret) => clampCaret(caret + 1, value.length));
2133
+ return;
2134
+ }
2135
+ if (key.ctrl && input === "a") {
2136
+ setCaret(0);
2137
+ return;
2138
+ }
2139
+ if (key.ctrl && input === "e") {
2140
+ setCaret(value.length);
2141
+ return;
2142
+ }
2143
+ if (key.backspace || key.delete) {
2144
+ const result = spliceBackspace(valueRef.current, caretRef.current);
2145
+ valueRef.current = result.value;
2146
+ caretRef.current = result.caret;
2147
+ setValue(result.value);
2148
+ setCaret(result.caret);
2149
+ setError(null);
2150
+ return;
2151
+ }
2152
+ if (text.isPrintable(input, key)) {
2153
+ const clean = sanitizeTyped(input);
2154
+ if (clean) text.insert(clean);
2155
+ }
2156
+ }
2157
+ function handleSearchEditor(input, key, table, text) {
2158
+ const { search, setSearch, setSearchMode, setSearchCaret, searchValueRef, searchCaretRef } = table;
2159
+ if (key.return || key.escape) {
2160
+ setSearchMode(false);
2161
+ if (key.escape) {
2162
+ setSearch("");
2163
+ setSearchCaret(0);
2263
2164
  }
2264
- const tf = accountForm.field;
2165
+ return;
2166
+ }
2167
+ if (key.leftArrow) {
2168
+ setSearchCaret((caret) => clampCaret(caret - 1, search.length));
2169
+ return;
2170
+ }
2171
+ if (key.rightArrow) {
2172
+ setSearchCaret((caret) => clampCaret(caret + 1, search.length));
2173
+ return;
2174
+ }
2175
+ if (key.ctrl && input === "a") {
2176
+ setSearchCaret(0);
2177
+ return;
2178
+ }
2179
+ if (key.ctrl && input === "e") {
2180
+ setSearchCaret(search.length);
2181
+ return;
2182
+ }
2183
+ if (key.backspace || key.delete) {
2184
+ const result = spliceBackspace(searchValueRef.current, searchCaretRef.current);
2185
+ searchValueRef.current = result.value;
2186
+ searchCaretRef.current = result.caret;
2187
+ setSearch(result.value);
2188
+ setSearchCaret(result.caret);
2189
+ return;
2190
+ }
2191
+ if (text.isPrintable(input, key)) {
2192
+ const clean = sanitizeTyped(input);
2193
+ if (clean) text.insert(clean);
2194
+ }
2195
+ }
2196
+
2197
+ // src/ui/keybindings-settings.ts
2198
+ function handleSettings(input, key, ctx) {
2199
+ const {
2200
+ settings,
2201
+ timezoneEditor,
2202
+ textInput,
2203
+ global: { config, updateConfig }
2204
+ } = ctx;
2205
+ const {
2206
+ cursor,
2207
+ tab,
2208
+ setShow,
2209
+ setTab,
2210
+ setCursor,
2211
+ trackedAccounts,
2212
+ moveAccount,
2213
+ toggleProvider,
2214
+ openEditAccount,
2215
+ openConfigureAccount,
2216
+ deleteAccount,
2217
+ openAddAccount
2218
+ } = settings;
2219
+ if (key.escape || input === "s") {
2220
+ setShow(false);
2221
+ return;
2222
+ }
2223
+ const switchTab = (direction) => {
2224
+ const index = SETTINGS_TABS.indexOf(tab);
2225
+ setTab(SETTINGS_TABS[(index + direction + SETTINGS_TABS.length) % SETTINGS_TABS.length]);
2226
+ setCursor(-1);
2227
+ timezoneEditor.setValue(null);
2228
+ timezoneEditor.setError(null);
2229
+ };
2230
+ const rowCount = tab === "general" ? GENERAL_ROWS : tab === "providers" ? PROVIDER_ORDER.length : trackedAccounts.length + 1;
2231
+ if (key.tab) {
2232
+ switchTab(key.shift ? -1 : 1);
2233
+ return;
2234
+ }
2235
+ if (input === "[") {
2236
+ switchTab(-1);
2237
+ return;
2238
+ }
2239
+ if (input === "]") {
2240
+ switchTab(1);
2241
+ return;
2242
+ }
2243
+ if (cursor < 0) {
2265
2244
  if (key.leftArrow) {
2266
- setAccountForm((f) => f && { ...f, caret: clampCaret(f.caret - 1, f[tf].length) });
2245
+ switchTab(-1);
2267
2246
  return;
2268
2247
  }
2269
2248
  if (key.rightArrow) {
2270
- setAccountForm((f) => f && { ...f, caret: clampCaret(f.caret + 1, f[tf].length) });
2271
- return;
2272
- }
2273
- if (key.ctrl && input === "a") {
2274
- setAccountForm((f) => f && { ...f, caret: 0 });
2249
+ switchTab(1);
2275
2250
  return;
2276
2251
  }
2277
- if (key.ctrl && input === "e") {
2278
- setAccountForm((f) => f && { ...f, caret: f[tf].length });
2252
+ if (key.downArrow || key.return) {
2253
+ setCursor(0);
2279
2254
  return;
2280
2255
  }
2281
- if (key.return) {
2282
- if (tf === "name" && accountForm.name.trim() === "") {
2283
- setAccountForm((f) => f && { ...f, error: "Name required", caret: f.name.length });
2284
- return;
2285
- }
2286
- setAccountForm((f) => f && { ...f, field: tf === "name" ? "homeDir" : "color", caret: tf === "name" ? f.homeDir.length : f.caret });
2287
- return;
2288
- }
2289
- if (key.backspace || key.delete) {
2290
- setAccountForm((f) => {
2291
- if (!f || f.field !== "name" && f.field !== "homeDir") return f;
2292
- const r = spliceBackspace(f[f.field], f.caret);
2293
- return { ...f, [f.field]: r.value, caret: r.caret, error: null };
2294
- });
2256
+ if (key.upArrow) {
2257
+ setCursor(Math.max(0, rowCount - 1));
2295
2258
  return;
2296
2259
  }
2297
- if (isPrintable(input, key)) {
2298
- const clean = sanitizeTyped(input);
2299
- if (clean) insertText(clean);
2300
- }
2301
2260
  return;
2302
2261
  }
2303
- if (showSettings && tzEdit !== null) {
2304
- if (key.escape) {
2305
- setTzEdit(null);
2306
- setTzError(null);
2307
- return;
2262
+ const selected = tab === "accounts" && cursor < trackedAccounts.length ? trackedAccounts[cursor] : null;
2263
+ if (selected?.source === "configured" && selected.explicitIndex !== void 0 && key.shift && (key.upArrow || key.downArrow)) {
2264
+ moveAccount(selected.explicitIndex, key.upArrow ? -1 : 1);
2265
+ return;
2266
+ }
2267
+ if (key.upArrow) {
2268
+ setCursor((current) => Math.max(-1, current - 1));
2269
+ return;
2270
+ }
2271
+ if (key.downArrow) {
2272
+ setCursor((current) => Math.min(rowCount - 1, current + 1));
2273
+ return;
2274
+ }
2275
+ if (tab === "general") {
2276
+ handleGeneralSetting(input, key, ctx);
2277
+ return;
2278
+ }
2279
+ if (tab === "providers") {
2280
+ if (cursor < PROVIDER_ORDER.length && (input === " " || key.return || key.leftArrow || key.rightArrow)) {
2281
+ toggleProvider(PROVIDER_ORDER[cursor]);
2308
2282
  }
2283
+ return;
2284
+ }
2285
+ if (cursor < trackedAccounts.length) {
2286
+ const row = trackedAccounts[cursor];
2309
2287
  if (key.return) {
2310
- const val = tzEdit.trim();
2311
- if (val === "" || val.toLowerCase() === "system") {
2312
- updateConfig((c) => ({ ...c, timezone: null }));
2313
- setTzEdit(null);
2314
- setTzError(null);
2315
- } else if (isValidTimezone(val)) {
2316
- updateConfig((c) => ({ ...c, timezone: val }));
2317
- setTzEdit(null);
2318
- setTzError(null);
2288
+ if (row.source === "configured") {
2289
+ const account = config.accounts.find((candidate) => candidate.id === row.explicitId);
2290
+ if (account) openEditAccount(account);
2319
2291
  } else {
2320
- setTzError(`Invalid: ${val}`);
2292
+ openConfigureAccount(row);
2321
2293
  }
2322
2294
  return;
2323
2295
  }
2324
- if (key.leftArrow) {
2325
- setTzCaret((c) => clampCaret(c - 1, tzEdit.length));
2296
+ if (row.source === "configured" && row.explicitId && (input === "d" || input === "x")) {
2297
+ deleteAccount(row.explicitId);
2326
2298
  return;
2327
2299
  }
2328
- if (key.rightArrow) {
2329
- setTzCaret((c) => clampCaret(c + 1, tzEdit.length));
2300
+ if (input === " ") {
2301
+ updateConfig((current) => ({ ...current, activeAccountId: row.id }));
2330
2302
  return;
2331
2303
  }
2332
- if (key.ctrl && input === "a") {
2333
- setTzCaret(0);
2304
+ return;
2305
+ }
2306
+ if (cursor === trackedAccounts.length && key.return) openAddAccount();
2307
+ function handleGeneralSetting(value, inputKey, context) {
2308
+ const index = context.settings.cursor;
2309
+ if (index === 0) {
2310
+ if (inputKey.leftArrow) updateConfig((current) => ({ ...current, interval: Math.max(1, current.interval - 1) }));
2311
+ if (inputKey.rightArrow) updateConfig((current) => ({ ...current, interval: current.interval + 1 }));
2334
2312
  return;
2335
2313
  }
2336
- if (key.ctrl && input === "e") {
2337
- setTzCaret(tzEdit.length);
2314
+ if (index === 1) {
2315
+ if (inputKey.leftArrow) updateConfig((current) => ({ ...current, billingInterval: Math.max(1, current.billingInterval - 1) }));
2316
+ if (inputKey.rightArrow) updateConfig((current) => ({ ...current, billingInterval: current.billingInterval + 1 }));
2338
2317
  return;
2339
2318
  }
2340
- if (key.backspace || key.delete) {
2341
- const r = spliceBackspace(tzValueRef.current, tzCaretRef.current);
2342
- tzValueRef.current = r.value;
2343
- tzCaretRef.current = r.caret;
2344
- setTzEdit(r.value);
2345
- setTzCaret(r.caret);
2346
- setTzError(null);
2319
+ if (index === 2 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2320
+ updateConfig((current) => ({ ...current, clearScreen: !current.clearScreen }));
2347
2321
  return;
2348
2322
  }
2349
- if (isPrintable(input, key)) {
2350
- const clean = sanitizeTyped(input);
2351
- if (clean) insertText(clean);
2352
- }
2353
- return;
2354
- }
2355
- if (tab === 1 && searchMode) {
2356
- if (key.return || key.escape) {
2357
- setSearchMode(false);
2358
- if (key.escape) {
2359
- setSearch("");
2360
- setSearchCaret(0);
2361
- }
2323
+ if (index === 3 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2324
+ updateConfig((current) => ({ ...current, privacyMode: !current.privacyMode }));
2362
2325
  return;
2363
2326
  }
2364
- if (key.leftArrow) {
2365
- setSearchCaret((c) => clampCaret(c - 1, search.length));
2327
+ if (index === 4) {
2328
+ if (textInput.isPrintable(value, inputKey)) {
2329
+ const clean = sanitizeTyped(value);
2330
+ if (clean.length === 1) updateConfig((current) => ({ ...current, privacyToggleKey: clean }));
2331
+ }
2332
+ if (inputKey.backspace || inputKey.delete) updateConfig((current) => ({ ...current, privacyToggleKey: "p" }));
2366
2333
  return;
2367
2334
  }
2368
- if (key.rightArrow) {
2369
- setSearchCaret((c) => clampCaret(c + 1, search.length));
2335
+ if (index === 5) {
2336
+ if (inputKey.return) {
2337
+ const initial = config.timezone ?? "";
2338
+ timezoneEditor.setValue(initial);
2339
+ timezoneEditor.setCaret(initial.length);
2340
+ timezoneEditor.setError(null);
2341
+ }
2342
+ if (inputKey.leftArrow || inputKey.rightArrow) {
2343
+ updateConfig((current) => ({ ...current, timezone: current.timezone === null ? systemTimezone() : null }));
2344
+ }
2370
2345
  return;
2371
2346
  }
2372
- if (key.ctrl && input === "a") {
2373
- setSearchCaret(0);
2347
+ if (index === 6 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2348
+ updateConfig((current) => ({ ...current, dashboardLayout: current.dashboardLayout === "grid" ? "single" : "grid" }));
2374
2349
  return;
2375
2350
  }
2376
- if (key.ctrl && input === "e") {
2377
- setSearchCaret(search.length);
2378
- return;
2351
+ if (index === 7 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2352
+ updateConfig((current) => ({ ...current, defaultFocus: current.defaultFocus === "all" ? "last" : "all" }));
2379
2353
  }
2380
- if (key.backspace || key.delete) {
2381
- const r = spliceBackspace(searchValueRef.current, searchCaretRef.current);
2382
- searchValueRef.current = r.value;
2383
- searchCaretRef.current = r.caret;
2384
- setSearch(r.value);
2385
- setSearchCaret(r.caret);
2386
- return;
2387
- }
2388
- if (isPrintable(input, key)) {
2389
- const clean = sanitizeTyped(input);
2390
- if (clean) insertText(clean);
2391
- }
2392
- return;
2393
2354
  }
2355
+ }
2356
+
2357
+ // src/ui/keybindings-navigation.ts
2358
+ function handleGlobalCommand(input, ctx) {
2359
+ const { global, table, settings } = ctx;
2394
2360
  if (input === "q") {
2395
- exit();
2396
- return;
2361
+ global.exit();
2362
+ return true;
2397
2363
  }
2398
2364
  if (input === "O") {
2399
2365
  openUrl(REPO_URL);
2400
- return;
2401
- }
2402
- if (input === "W" || input === "w" && tab !== 1 && !showSettings) {
2403
- if (showLoader || !configReady) return;
2404
- void toggleWeb();
2405
- return;
2366
+ return true;
2406
2367
  }
2407
- if (showSettings) {
2408
- if (key.escape || input === "s") {
2409
- setShowSettings(false);
2410
- return;
2411
- }
2412
- const switchSettingsTab = (dir) => {
2413
- const idx = SETTINGS_TABS.indexOf(settingsTab);
2414
- setSettingsTab(SETTINGS_TABS[(idx + dir + SETTINGS_TABS.length) % SETTINGS_TABS.length]);
2415
- setSettingsCursor(-1);
2416
- setTzEdit(null);
2417
- setTzError(null);
2418
- };
2419
- const rowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
2420
- if (key.tab) {
2421
- switchSettingsTab(key.shift ? -1 : 1);
2422
- return;
2423
- }
2424
- if (input === "[") {
2425
- switchSettingsTab(-1);
2426
- return;
2427
- }
2428
- if (input === "]") {
2429
- switchSettingsTab(1);
2430
- return;
2431
- }
2432
- if (settingsCursor < 0) {
2433
- if (key.leftArrow) {
2434
- switchSettingsTab(-1);
2435
- return;
2436
- }
2437
- if (key.rightArrow) {
2438
- switchSettingsTab(1);
2439
- return;
2440
- }
2441
- if (key.downArrow || key.return) {
2442
- setSettingsCursor(0);
2443
- return;
2444
- }
2445
- if (key.upArrow) {
2446
- setSettingsCursor(Math.max(0, rowCount - 1));
2447
- return;
2448
- }
2449
- return;
2450
- }
2451
- const selectedAccountRow = settingsTab === "accounts" && settingsCursor >= 0 && settingsCursor < trackedAccountRows.length ? trackedAccountRows[settingsCursor] : null;
2452
- if (selectedAccountRow?.source === "configured" && selectedAccountRow.explicitIndex !== void 0 && key.shift && (key.upArrow || key.downArrow)) {
2453
- moveAccount(selectedAccountRow.explicitIndex, key.upArrow ? -1 : 1);
2454
- return;
2455
- }
2456
- if (key.upArrow) {
2457
- setSettingsCursor((c) => Math.max(-1, c - 1));
2458
- return;
2459
- }
2460
- if (key.downArrow) {
2461
- setSettingsCursor((c) => Math.min(rowCount - 1, c + 1));
2462
- return;
2463
- }
2464
- if (settingsTab === "general") {
2465
- if (settingsCursor === 0) {
2466
- if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
2467
- if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
2468
- return;
2469
- }
2470
- if (settingsCursor === 1) {
2471
- if (key.leftArrow) updateConfig((c) => ({ ...c, billingInterval: Math.max(1, c.billingInterval - 1) }));
2472
- if (key.rightArrow) updateConfig((c) => ({ ...c, billingInterval: c.billingInterval + 1 }));
2473
- return;
2474
- }
2475
- if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
2476
- updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
2477
- return;
2478
- }
2479
- if (settingsCursor === 3 && (key.leftArrow || key.rightArrow || key.return)) {
2480
- updateConfig((c) => ({ ...c, privacyMode: !c.privacyMode }));
2481
- return;
2482
- }
2483
- if (settingsCursor === 4) {
2484
- if (isPrintable(input, key)) {
2485
- const clean = sanitizeTyped(input);
2486
- if (clean.length === 1) updateConfig((c) => ({ ...c, privacyToggleKey: clean }));
2487
- }
2488
- if (key.backspace || key.delete) updateConfig((c) => ({ ...c, privacyToggleKey: "p" }));
2489
- return;
2490
- }
2491
- if (settingsCursor === 5) {
2492
- if (key.return) {
2493
- const init = cfg.timezone ?? "";
2494
- setTzEdit(init);
2495
- setTzCaret(init.length);
2496
- setTzError(null);
2497
- }
2498
- if (key.leftArrow || key.rightArrow) updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
2499
- return;
2500
- }
2501
- if (settingsCursor === 6 && (key.leftArrow || key.rightArrow || key.return)) {
2502
- updateConfig((c) => ({ ...c, dashboardLayout: c.dashboardLayout === "grid" ? "single" : "grid" }));
2503
- return;
2504
- }
2505
- if (settingsCursor === 7 && (key.leftArrow || key.rightArrow || key.return)) {
2506
- updateConfig((c) => ({ ...c, defaultFocus: c.defaultFocus === "all" ? "last" : "all" }));
2507
- return;
2508
- }
2509
- return;
2510
- }
2511
- if (settingsTab === "providers") {
2512
- if (settingsCursor >= 0 && settingsCursor < PROVIDER_ORDER.length && (input === " " || key.return || key.leftArrow || key.rightArrow)) {
2513
- toggleProvider(PROVIDER_ORDER[settingsCursor]);
2514
- }
2515
- return;
2516
- }
2517
- if (settingsTab === "accounts" && settingsCursor >= 0 && settingsCursor < trackedAccountRows.length) {
2518
- const row = trackedAccountRows[settingsCursor];
2519
- if (key.return) {
2520
- if (row.source === "configured") {
2521
- const acc = cfg.accounts.find((a) => a.id === row.explicitId);
2522
- if (acc) openEditAccount(acc);
2523
- } else {
2524
- openConfigureAccount(row);
2525
- }
2526
- return;
2527
- }
2528
- if (row.source === "configured" && row.explicitId && (input === "d" || input === "x")) {
2529
- deleteAccount(row.explicitId);
2530
- return;
2531
- }
2532
- if (input === " ") {
2533
- updateConfig((c) => ({ ...c, activeAccountId: row.id }));
2534
- return;
2535
- }
2536
- return;
2537
- }
2538
- if (settingsTab === "accounts" && settingsCursor === trackedAccountRows.length && key.return) {
2539
- openAddAccount();
2540
- }
2541
- return;
2368
+ if (input === "W" || input === "w" && table.tab !== 1 && !settings.show) {
2369
+ if (!global.showLoader && global.configReady) void global.toggleWeb();
2370
+ return true;
2542
2371
  }
2543
- if (input === cfg.privacyToggleKey) {
2544
- updateConfig((c) => ({ ...c, privacyMode: !c.privacyMode }));
2372
+ return false;
2373
+ }
2374
+ function handleNavigation(input, key, ctx) {
2375
+ const { settings, table, dashboard, global } = ctx;
2376
+ const { config, updateConfig, resetView, slots } = global;
2377
+ if (input === config.privacyToggleKey) {
2378
+ updateConfig((current) => ({ ...current, privacyMode: !current.privacyMode }));
2545
2379
  return;
2546
2380
  }
2547
2381
  if (input === "s") {
2548
- setSettingsTab("general");
2549
- setShowSettings(true);
2550
- setSettingsCursor(-1);
2382
+ settings.setTab("general");
2383
+ settings.setShow(true);
2384
+ settings.setCursor(-1);
2551
2385
  return;
2552
2386
  }
2553
2387
  if (input === "a") {
2554
- cycleAccount(1);
2388
+ global.cycleAccount(1);
2555
2389
  return;
2556
2390
  }
2557
2391
  if (input === "A") {
2558
- cycleAccount(-1);
2392
+ global.cycleAccount(-1);
2559
2393
  return;
2560
2394
  }
2561
2395
  if (key.tab) {
2562
- setTab((t) => (t + 1) % TABS.length);
2396
+ global.setTab((current) => (current + 1) % TABS.length);
2563
2397
  resetView();
2564
2398
  return;
2565
2399
  }
2566
2400
  if (input && /^[0-9]$/.test(input) && slots.length > 1) {
2567
2401
  const target = slots[parseInt(input, 10)];
2568
2402
  if (target) {
2569
- updateConfig((c) => ({ ...c, activeAccountId: target.id }));
2403
+ updateConfig((current) => ({ ...current, activeAccountId: target.id }));
2570
2404
  resetView();
2571
2405
  }
2572
2406
  return;
2573
2407
  }
2574
- if (tab === 0) {
2575
- if (dashPaginated) {
2408
+ if (table.tab === 0) {
2409
+ if (dashboard.paginated) {
2576
2410
  if (input === "]" || key.downArrow || key.pageDown) {
2577
- setDashPage((p) => Math.min(dashPageCount - 1, p + 1));
2411
+ dashboard.setPage((page) => Math.min(dashboard.pageCount - 1, page + 1));
2578
2412
  return;
2579
2413
  }
2580
2414
  if (input === "[" || key.upArrow || key.pageUp) {
2581
- setDashPage((p) => Math.max(0, p - 1));
2415
+ dashboard.setPage((page) => Math.max(0, page - 1));
2582
2416
  return;
2583
2417
  }
2584
2418
  }
2585
2419
  if (key.upArrow || key.downArrow || key.pageUp || key.pageDown || input === "G" || input === "g") return;
2586
2420
  }
2587
- if (tab === 1) {
2588
- if (input === "p") {
2589
- cycleTableProvider(1);
2590
- return;
2591
- }
2592
- if (input === "P") {
2593
- cycleTableProvider(-1);
2594
- return;
2595
- }
2596
- if (input === "/") {
2597
- setSearchMode(true);
2598
- setSearchCaret(search.length);
2599
- return;
2600
- }
2601
- if (key.escape) {
2602
- if (search) {
2603
- setSearch("");
2604
- setSearchCaret(0);
2605
- } else setExpanded(-1);
2606
- return;
2607
- }
2608
- if (input === "o") {
2609
- setSort((s) => (s + 1) % SORTS_FOR.length);
2610
- resetView();
2611
- return;
2612
- }
2613
- if (!tableIsCursor) {
2614
- if (input === "d") {
2615
- setView(0);
2616
- resetView();
2617
- return;
2618
- }
2619
- if (input === "w") {
2620
- setView(1);
2621
- resetView();
2622
- return;
2623
- }
2624
- if (input === "m") {
2625
- cycleTableModel2(1);
2626
- return;
2627
- }
2628
- if (input === "M") {
2629
- cycleTableModel2(-1);
2630
- return;
2631
- }
2632
- if (key.leftArrow) {
2633
- setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
2634
- resetView();
2635
- return;
2636
- }
2637
- if (key.rightArrow) {
2638
- setView((v) => (v + 1) % VIEWS.length);
2639
- resetView();
2640
- return;
2641
- }
2642
- if (key.return) {
2643
- setExpanded((e) => e === cursor ? -1 : cursor);
2644
- return;
2645
- }
2646
- }
2647
- } else {
2421
+ if (table.tab !== 1) {
2648
2422
  if (key.leftArrow || key.rightArrow) {
2649
- setTab((t) => (t + 1) % TABS.length);
2423
+ global.setTab((current) => (current + 1) % TABS.length);
2650
2424
  resetView();
2651
- return;
2652
2425
  }
2426
+ return;
2653
2427
  }
2654
- if (tab === 1 && !tableIsCursor) {
2655
- if (key.upArrow) {
2656
- setCursor((c) => Math.max(0, c - 1));
2657
- return;
2658
- }
2659
- if (key.downArrow) {
2660
- setCursor((c) => clampRow(c + 1));
2661
- return;
2662
- }
2663
- if (key.pageDown || input === "G") {
2664
- setCursor((c) => clampRow(input === "G" ? rowCountRef.current - 1 : c + Math.max(1, rows - 12)));
2665
- return;
2666
- }
2667
- if (key.pageUp || input === "g") {
2668
- setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
2669
- return;
2670
- }
2428
+ if (input === "p") {
2429
+ table.cycleProvider(1);
2430
+ return;
2431
+ }
2432
+ if (input === "P") {
2433
+ table.cycleProvider(-1);
2434
+ return;
2435
+ }
2436
+ if (input === "/") {
2437
+ table.setSearchMode(true);
2438
+ table.setSearchCaret(table.search.length);
2439
+ return;
2440
+ }
2441
+ if (key.escape) {
2442
+ if (table.search) {
2443
+ table.setSearch("");
2444
+ table.setSearchCaret(0);
2445
+ } else table.setExpanded(-1);
2446
+ return;
2447
+ }
2448
+ if (input === "o") {
2449
+ table.setSort((sort) => (sort + 1) % table.sorts.length);
2450
+ resetView();
2451
+ return;
2452
+ }
2453
+ if (input === "d") {
2454
+ table.setView(0);
2455
+ resetView();
2456
+ return;
2457
+ }
2458
+ if (input === "w") {
2459
+ table.setView(1);
2460
+ resetView();
2461
+ return;
2462
+ }
2463
+ if (input === "m") {
2464
+ table.cycleModel(1);
2465
+ return;
2466
+ }
2467
+ if (input === "M") {
2468
+ table.cycleModel(-1);
2469
+ return;
2470
+ }
2471
+ if (key.leftArrow) {
2472
+ table.setView((view) => (view - 1 + VIEWS.length) % VIEWS.length);
2473
+ resetView();
2474
+ return;
2475
+ }
2476
+ if (key.rightArrow) {
2477
+ table.setView((view) => (view + 1) % VIEWS.length);
2478
+ resetView();
2479
+ return;
2480
+ }
2481
+ if (key.return) {
2482
+ table.setExpanded((expanded) => expanded === table.cursor ? -1 : table.cursor);
2483
+ return;
2484
+ }
2485
+ if (key.upArrow) {
2486
+ table.setCursor((cursor) => Math.max(0, cursor - 1));
2487
+ return;
2488
+ }
2489
+ if (key.downArrow) {
2490
+ table.setCursor((cursor) => table.clampRow(cursor + 1));
2491
+ return;
2492
+ }
2493
+ if (key.pageDown || input === "G") {
2494
+ table.setCursor((cursor) => table.clampRow(input === "G" ? table.rowCountRef.current - 1 : cursor + Math.max(1, table.rows - 12)));
2495
+ return;
2496
+ }
2497
+ if (key.pageUp || input === "g") {
2498
+ table.setCursor((cursor) => input === "g" ? 0 : Math.max(0, cursor - Math.max(1, table.rows - 12)));
2671
2499
  }
2672
2500
  }
2673
2501
 
2502
+ // src/ui/keybindings.ts
2503
+ function isRefreshAllShortcut(input, mode) {
2504
+ return (input === "r" || input === "R") && !mode.unavailable && !mode.showPicker && !mode.editingAccount && !mode.editingTimezone && !mode.editingSearch;
2505
+ }
2506
+ function handleOnboarding(input, key, ctx) {
2507
+ const { onboarding, global } = ctx;
2508
+ if (input === "q") {
2509
+ global.exit();
2510
+ return;
2511
+ }
2512
+ const confirmIndex = onboarding.providers.length;
2513
+ if (key.upArrow) {
2514
+ onboarding.setCursor((cursor) => Math.max(0, cursor - 1));
2515
+ return;
2516
+ }
2517
+ if (key.downArrow) {
2518
+ onboarding.setCursor((cursor) => Math.min(confirmIndex, cursor + 1));
2519
+ return;
2520
+ }
2521
+ if (input === " ") {
2522
+ onboarding.toggle(onboarding.cursor);
2523
+ return;
2524
+ }
2525
+ if (key.return) {
2526
+ if (onboarding.cursor === confirmIndex) onboarding.confirm();
2527
+ else onboarding.toggle(onboarding.cursor);
2528
+ }
2529
+ }
2530
+ function handleKey(input, key, ctx) {
2531
+ const editingAccount = ctx.settings.show && ctx.accountEditor.form !== null;
2532
+ const editingTimezone = ctx.settings.show && ctx.timezoneEditor.value !== null;
2533
+ const editingSearch = ctx.table.tab === 1 && ctx.table.searchMode;
2534
+ if (isRefreshAllShortcut(input, {
2535
+ showPicker: ctx.onboarding.show,
2536
+ editingAccount,
2537
+ editingTimezone,
2538
+ editingSearch,
2539
+ unavailable: !ctx.global.configReady
2540
+ })) {
2541
+ ctx.global.refreshAll();
2542
+ return;
2543
+ }
2544
+ if (ctx.onboarding.show) {
2545
+ handleOnboarding(input, key, ctx);
2546
+ return;
2547
+ }
2548
+ if (editingAccount) {
2549
+ handleAccountEditor(input, key, ctx.accountEditor, ctx.textInput);
2550
+ return;
2551
+ }
2552
+ if (editingTimezone) {
2553
+ handleTimezoneEditor(input, key, ctx.timezoneEditor, ctx.textInput, ctx.global.updateConfig);
2554
+ return;
2555
+ }
2556
+ if (editingSearch) {
2557
+ handleSearchEditor(input, key, ctx.table, ctx.textInput);
2558
+ return;
2559
+ }
2560
+ if (handleGlobalCommand(input, ctx)) return;
2561
+ if (ctx.settings.show) {
2562
+ handleSettings(input, key, ctx);
2563
+ return;
2564
+ }
2565
+ handleNavigation(input, key, ctx);
2566
+ }
2567
+
2674
2568
  // src/ui/hooks/use-terminal-size.ts
2675
2569
  import { useState as useState4, useEffect as useEffect4 } from "react";
2676
2570
  import { useStdout } from "ink";
@@ -2824,7 +2718,7 @@ function useLoader({ configReady, showPicker, accountsKey, allReady, tooSmall, s
2824
2718
  }
2825
2719
 
2826
2720
  // src/ui/hooks/use-degraded-polling.ts
2827
- import { useState as useState6, useEffect as useEffect6, useRef as useRef5 } from "react";
2721
+ import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react";
2828
2722
 
2829
2723
  // src/client/seed-cache.ts
2830
2724
  import { readFile } from "fs/promises";
@@ -2843,92 +2737,420 @@ async function loadSeedSnapshot() {
2843
2737
  }
2844
2738
 
2845
2739
  // src/ui/hooks/use-degraded-polling.ts
2846
- function useDegradedPolling({ degraded, configReady, showPicker, accountsKey, accountsRef, interval, billingMs, tz }) {
2740
+ function failureMessage(scope, failures) {
2741
+ const detail = failures[0]?.reason;
2742
+ const suffix = detail instanceof Error && detail.message ? `: ${detail.message}` : "";
2743
+ return new Error(`${scope} refresh failed for ${failures.length} source${failures.length === 1 ? "" : "s"}${suffix}`);
2744
+ }
2745
+ function usageProviders(accounts) {
2746
+ return [...new Set(accounts.filter((account) => PROVIDERS[account.providerId].hasUsage).map((account) => account.providerId))];
2747
+ }
2748
+ function useDegradedPolling({
2749
+ degraded,
2750
+ configReady,
2751
+ showPicker,
2752
+ accountsKey,
2753
+ accountsRef,
2754
+ interval,
2755
+ billingMs,
2756
+ tz,
2757
+ activeTableProvider,
2758
+ tableVisible
2759
+ }) {
2847
2760
  const [statsLocal, setStats] = useState6(/* @__PURE__ */ new Map());
2848
2761
  const [peakLocal, setPeak] = useState6(null);
2849
- const [error, setError] = useState6(null);
2850
2762
  const [updatedLocal, setUpdated] = useState6(/* @__PURE__ */ new Date());
2763
+ const [tables, setTables] = useState6(/* @__PURE__ */ new Map());
2764
+ const [loadingTables, setLoadingTables] = useState6(/* @__PURE__ */ new Set());
2851
2765
  const seededRef = useRef5(false);
2766
+ const epochRef = useRef5(0);
2767
+ const summaryFlightRef = useRef5(null);
2768
+ const billingFlightRef = useRef5(null);
2769
+ const peakFlightRef = useRef5(null);
2770
+ const tableFlightsRef = useRef5(/* @__PURE__ */ new Map());
2771
+ const allFlightRef = useRef5(null);
2772
+ useEffect6(() => {
2773
+ const epoch = ++epochRef.current;
2774
+ summaryFlightRef.current = null;
2775
+ billingFlightRef.current = null;
2776
+ peakFlightRef.current = null;
2777
+ tableFlightsRef.current.clear();
2778
+ allFlightRef.current = null;
2779
+ setStats(/* @__PURE__ */ new Map());
2780
+ setTables(/* @__PURE__ */ new Map());
2781
+ setLoadingTables(/* @__PURE__ */ new Set());
2782
+ return () => {
2783
+ if (epochRef.current === epoch) epochRef.current++;
2784
+ };
2785
+ }, [accountsKey, tz, degraded]);
2852
2786
  useEffect6(() => {
2853
2787
  if (!degraded) return;
2854
2788
  if (seededRef.current || !configReady || showPicker || accountsRef.current.length === 0) return;
2855
2789
  seededRef.current = true;
2856
- loadSeedSnapshot().then((snap) => {
2790
+ const epoch = epochRef.current;
2791
+ void loadSeedSnapshot().then((snap) => {
2792
+ if (epoch !== epochRef.current) return;
2857
2793
  setStats((prev) => {
2858
2794
  if (prev.size > 0) return prev;
2859
2795
  const next = new Map(prev);
2860
2796
  for (const acc of accountsRef.current) {
2861
- const s = snap[acc.id];
2862
- if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
2797
+ const cached = snap[acc.id];
2798
+ if (cached && (cached.dashboard || cached.billing)) {
2799
+ next.set(acc.id, {
2800
+ account: acc,
2801
+ dashboard: cached.dashboard ?? null,
2802
+ billing: cached.billing ?? null
2803
+ });
2804
+ }
2863
2805
  }
2864
2806
  return next;
2865
2807
  });
2866
2808
  });
2867
- }, [degraded, configReady, showPicker, accountsKey]);
2809
+ }, [degraded, configReady, showPicker, accountsKey, accountsRef]);
2810
+ const runSummary = useCallback3(() => {
2811
+ if (summaryFlightRef.current) return summaryFlightRef.current;
2812
+ const epoch = epochRef.current;
2813
+ const task = (async () => {
2814
+ const targets = accountsRef.current.filter((account) => {
2815
+ const provider = PROVIDERS[account.providerId];
2816
+ return provider.hasUsage && provider.fetchSummary;
2817
+ });
2818
+ const settled = await Promise.allSettled(targets.map(async (account) => {
2819
+ const fetchSummary = PROVIDERS[account.providerId].fetchSummary;
2820
+ if (!fetchSummary) return;
2821
+ const dashboard = await withTimeout(fetchSummary(account, tz));
2822
+ if (epoch === epochRef.current) setStats((prev) => upsert(prev, account, { dashboard }));
2823
+ }));
2824
+ if (epoch !== epochRef.current) return;
2825
+ setUpdated(/* @__PURE__ */ new Date());
2826
+ const failures = settled.filter((result) => result.status === "rejected");
2827
+ if (failures.length > 0) throw failureMessage("summary", failures);
2828
+ })();
2829
+ const owned = task.finally(() => {
2830
+ if (summaryFlightRef.current === owned) summaryFlightRef.current = null;
2831
+ });
2832
+ summaryFlightRef.current = owned;
2833
+ return owned;
2834
+ }, [accountsKey, accountsRef, tz]);
2835
+ const runBilling = useCallback3(() => {
2836
+ if (billingFlightRef.current) return billingFlightRef.current;
2837
+ const epoch = epochRef.current;
2838
+ const task = (async () => {
2839
+ const targets = accountsRef.current.filter((account) => {
2840
+ const provider = PROVIDERS[account.providerId];
2841
+ return provider.hasBilling && provider.fetchBilling;
2842
+ });
2843
+ const settled = await Promise.allSettled(targets.map(async (account) => {
2844
+ const fetchBilling = PROVIDERS[account.providerId].fetchBilling;
2845
+ if (!fetchBilling) return;
2846
+ const billing = await withTimeout(fetchBilling(account, tz));
2847
+ if (epoch === epochRef.current) setStats((prev) => upsert(prev, account, { billing }));
2848
+ }));
2849
+ if (epoch !== epochRef.current) return;
2850
+ const failures = settled.filter((result) => result.status === "rejected");
2851
+ if (failures.length > 0) throw failureMessage("billing", failures);
2852
+ })();
2853
+ const owned = task.finally(() => {
2854
+ if (billingFlightRef.current === owned) billingFlightRef.current = null;
2855
+ });
2856
+ billingFlightRef.current = owned;
2857
+ return owned;
2858
+ }, [accountsKey, accountsRef, tz]);
2859
+ const runPeak = useCallback3(() => {
2860
+ if (peakFlightRef.current) return peakFlightRef.current;
2861
+ if (!accountsRef.current.some((account) => account.providerId === "claude")) return Promise.resolve();
2862
+ const epoch = epochRef.current;
2863
+ const task = withTimeout(fetchPeak()).then((peak) => {
2864
+ if (epoch === epochRef.current && peak) setPeak(peak);
2865
+ });
2866
+ const owned = task.finally(() => {
2867
+ if (peakFlightRef.current === owned) peakFlightRef.current = null;
2868
+ });
2869
+ peakFlightRef.current = owned;
2870
+ return owned;
2871
+ }, [accountsKey, accountsRef]);
2872
+ const runTable = useCallback3((providerId) => {
2873
+ const existing = tableFlightsRef.current.get(providerId);
2874
+ if (existing) return existing;
2875
+ const epoch = epochRef.current;
2876
+ const targets = accountsRef.current.filter((account) => account.providerId === providerId);
2877
+ if (targets.length === 0 || !PROVIDERS[providerId].hasUsage) return Promise.resolve();
2878
+ setLoadingTables((prev) => new Set(prev).add(providerId));
2879
+ const task = (async () => {
2880
+ const settled = await Promise.allSettled(targets.map(async (account) => {
2881
+ const fetchTable = PROVIDERS[account.providerId].fetchTable;
2882
+ return fetchTable ? withTimeout(fetchTable(account, tz)) : null;
2883
+ }));
2884
+ if (epoch !== epochRef.current) return;
2885
+ const valid = settled.flatMap((result) => result.status === "fulfilled" && result.value ? [result.value] : []);
2886
+ if (valid.length > 0) {
2887
+ setTables((prev) => new Map(prev).set(providerId, coalesceTables(valid)));
2888
+ }
2889
+ const failures = settled.filter((result) => result.status === "rejected");
2890
+ if (failures.length > 0) throw failureMessage(`${PROVIDERS[providerId].name} history`, failures);
2891
+ })();
2892
+ const owned = task.finally(() => {
2893
+ if (tableFlightsRef.current.get(providerId) !== owned) return;
2894
+ tableFlightsRef.current.delete(providerId);
2895
+ setLoadingTables((prev) => {
2896
+ if (!prev.has(providerId)) return prev;
2897
+ const next = new Set(prev);
2898
+ next.delete(providerId);
2899
+ return next;
2900
+ });
2901
+ });
2902
+ tableFlightsRef.current.set(providerId, owned);
2903
+ return owned;
2904
+ }, [accountsKey, accountsRef, tz]);
2905
+ const refreshAll = useCallback3(() => {
2906
+ if (allFlightRef.current) return allFlightRef.current;
2907
+ const tasks = [runSummary(), runBilling(), runPeak()];
2908
+ for (const providerId of usageProviders(accountsRef.current)) tasks.push(runTable(providerId));
2909
+ const task = Promise.allSettled(tasks).then((settled) => {
2910
+ const failures = settled.filter((result) => result.status === "rejected");
2911
+ if (failures.length > 0) throw failureMessage("all-data", failures);
2912
+ setUpdated(/* @__PURE__ */ new Date());
2913
+ });
2914
+ const owned = task.finally(() => {
2915
+ if (allFlightRef.current === owned) allFlightRef.current = null;
2916
+ });
2917
+ allFlightRef.current = owned;
2918
+ return owned;
2919
+ }, [accountsRef, runBilling, runPeak, runSummary, runTable]);
2868
2920
  useEffect6(() => {
2869
2921
  if (!degraded || !configReady || showPicker) return;
2870
2922
  let active = true;
2871
2923
  let timer;
2872
- const load = async () => {
2873
- try {
2874
- await Promise.all(accountsRef.current.map(async (acc) => {
2875
- const provider = PROVIDERS[acc.providerId];
2876
- if (!provider.hasUsage || !provider.fetchSummary) return;
2877
- try {
2878
- const dashboard = await withTimeout(provider.fetchSummary(acc, tz));
2879
- if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
2880
- } catch {
2881
- }
2882
- }));
2883
- if (active) {
2884
- setError(null);
2885
- setUpdated(/* @__PURE__ */ new Date());
2886
- }
2887
- } finally {
2888
- if (active) timer = setTimeout(load, interval);
2889
- }
2924
+ const loop = async () => {
2925
+ await runSummary().catch(() => {
2926
+ });
2927
+ if (active) timer = setTimeout(loop, interval);
2890
2928
  };
2891
- load();
2929
+ void loop();
2892
2930
  return () => {
2893
2931
  active = false;
2894
- clearTimeout(timer);
2932
+ if (timer) clearTimeout(timer);
2895
2933
  };
2896
- }, [degraded, interval, tz, configReady, showPicker, accountsKey]);
2934
+ }, [degraded, configReady, showPicker, interval, runSummary]);
2897
2935
  useEffect6(() => {
2898
2936
  if (!degraded || !configReady || showPicker) return;
2899
2937
  let active = true;
2900
2938
  let timer;
2901
- const load = async () => {
2902
- try {
2903
- const peakP = accountsRef.current.some((a) => a.providerId === "claude") ? withTimeout(fetchPeak()).catch(() => null) : Promise.resolve(null);
2904
- await Promise.all(accountsRef.current.map(async (acc) => {
2905
- const provider = PROVIDERS[acc.providerId];
2906
- if (!provider.hasBilling || !provider.fetchBilling) return;
2907
- try {
2908
- const billing = await withTimeout(provider.fetchBilling(acc, tz));
2909
- if (active) setStats((prev) => upsert(prev, acc, { billing }));
2910
- } catch {
2911
- }
2912
- }));
2913
- const p = await peakP;
2914
- if (active && p) setPeak(p);
2915
- } finally {
2916
- if (active) timer = setTimeout(load, billingMs);
2917
- }
2939
+ const loop = async () => {
2940
+ await Promise.allSettled([runBilling(), runPeak()]);
2941
+ if (active) timer = setTimeout(loop, billingMs);
2918
2942
  };
2919
- load();
2943
+ void loop();
2920
2944
  return () => {
2921
2945
  active = false;
2922
- clearTimeout(timer);
2946
+ if (timer) clearTimeout(timer);
2923
2947
  };
2924
- }, [degraded, billingMs, tz, configReady, showPicker, accountsKey]);
2925
- return { statsLocal, peakLocal, updatedLocal, error };
2948
+ }, [degraded, configReady, showPicker, billingMs, runBilling, runPeak]);
2949
+ useEffect6(() => {
2950
+ if (!degraded || !configReady || showPicker || !tableVisible || !activeTableProvider) return;
2951
+ let active = true;
2952
+ let timer;
2953
+ const loop = async () => {
2954
+ await runTable(activeTableProvider).catch(() => {
2955
+ });
2956
+ if (active) timer = setTimeout(loop, Math.max(interval, 1e4));
2957
+ };
2958
+ void loop();
2959
+ return () => {
2960
+ active = false;
2961
+ if (timer) clearTimeout(timer);
2962
+ };
2963
+ }, [degraded, configReady, showPicker, tableVisible, activeTableProvider, interval, runTable]);
2964
+ return {
2965
+ statsLocal,
2966
+ peakLocal,
2967
+ updatedLocal,
2968
+ tableLocal: activeTableProvider ? tables.get(activeTableProvider) ?? null : null,
2969
+ tableLoading: activeTableProvider ? loadingTables.has(activeTableProvider) : false,
2970
+ refreshAll
2971
+ };
2972
+ }
2973
+
2974
+ // src/ui/hooks/use-refresh-all.ts
2975
+ import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef6, useState as useState7 } from "react";
2976
+ var IDLE = { phase: "idle", message: "" };
2977
+ var CONNECTED_REFRESH_TIMEOUT_MS = 10 * 6e4;
2978
+ var STATUS_TTL_MS = 4e3;
2979
+ function awaitRefreshCompletion(request, timeoutMs = CONNECTED_REFRESH_TIMEOUT_MS) {
2980
+ return new Promise((resolve, reject) => {
2981
+ const timer = setTimeout(() => {
2982
+ reject(new Error(`refresh is still running after ${Math.round(timeoutMs / 1e3)} seconds; it may finish in the background`));
2983
+ }, timeoutMs);
2984
+ timer.unref?.();
2985
+ Promise.resolve().then(request).then(
2986
+ () => {
2987
+ clearTimeout(timer);
2988
+ resolve();
2989
+ },
2990
+ (cause) => {
2991
+ clearTimeout(timer);
2992
+ reject(cause);
2993
+ }
2994
+ );
2995
+ });
2996
+ }
2997
+ function errorText(cause) {
2998
+ return cause instanceof Error && cause.message ? cause.message : "refresh failed";
2999
+ }
3000
+ function useRefreshAll({
3001
+ connected,
3002
+ requestDaemonRefresh,
3003
+ requestDegradedRefresh
3004
+ }) {
3005
+ const [status, setStatus] = useState7(IDLE);
3006
+ const flightRef = useRef6(null);
3007
+ const clearTimerRef = useRef6(null);
3008
+ const mountedRef = useRef6(true);
3009
+ useEffect7(() => {
3010
+ mountedRef.current = true;
3011
+ return () => {
3012
+ mountedRef.current = false;
3013
+ if (clearTimerRef.current) clearTimeout(clearTimerRef.current);
3014
+ };
3015
+ }, []);
3016
+ const refreshAll = useCallback4(() => {
3017
+ if (flightRef.current) return;
3018
+ if (clearTimerRef.current) {
3019
+ clearTimeout(clearTimerRef.current);
3020
+ clearTimerRef.current = null;
3021
+ }
3022
+ setStatus({ phase: "refreshing", message: "Refreshing all data" });
3023
+ const task = connected ? awaitRefreshCompletion(requestDaemonRefresh) : Promise.resolve().then(requestDegradedRefresh);
3024
+ const owned = task.then(() => {
3025
+ if (!mountedRef.current) return;
3026
+ setStatus({
3027
+ phase: "success",
3028
+ message: "All data refreshed"
3029
+ });
3030
+ }).catch((cause) => {
3031
+ if (!mountedRef.current) return;
3032
+ setStatus({ phase: "error", message: `Refresh failed: ${errorText(cause)}` });
3033
+ }).finally(() => {
3034
+ if (flightRef.current === owned) flightRef.current = null;
3035
+ if (!mountedRef.current) return;
3036
+ clearTimerRef.current = setTimeout(() => {
3037
+ clearTimerRef.current = null;
3038
+ if (mountedRef.current) setStatus(IDLE);
3039
+ }, STATUS_TTL_MS);
3040
+ clearTimerRef.current.unref?.();
3041
+ });
3042
+ flightRef.current = owned;
3043
+ }, [connected, requestDaemonRefresh, requestDegradedRefresh]);
3044
+ return { status, refreshAll };
3045
+ }
3046
+
3047
+ // src/ui/hooks/use-config-state.ts
3048
+ import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef7, useState as useState8 } from "react";
3049
+
3050
+ // src/config-sync.ts
3051
+ function describeConfigUpdateFailure(cause) {
3052
+ if (cause && typeof cause === "object") {
3053
+ const failure = cause;
3054
+ if (failure.kind === "conflict") {
3055
+ const state = failure.state;
3056
+ const revision = state?.config?.revision;
3057
+ return {
3058
+ message: typeof revision === "number" ? `configuration changed elsewhere (daemon revision ${revision})` : "settings changed elsewhere; reload before saving",
3059
+ conflictState: state?.config && typeof revision === "number" ? state : null
3060
+ };
3061
+ }
3062
+ if (failure.kind === "persistence" && typeof failure.message === "string") {
3063
+ return { message: failure.message, conflictState: null };
3064
+ }
3065
+ }
3066
+ return {
3067
+ message: cause instanceof Error ? cause.message : "config update failed",
3068
+ conflictState: null
3069
+ };
3070
+ }
3071
+
3072
+ // src/ui/hooks/use-config-state.ts
3073
+ function useConfigState({
3074
+ initialConfig,
3075
+ cliInterval,
3076
+ connected,
3077
+ daemon
3078
+ }) {
3079
+ const [config, setConfig] = useState8(
3080
+ () => initialConfig ? applyStartup(initialConfig, cliInterval) : null
3081
+ );
3082
+ const [configSaveError, setConfigSaveError] = useState8(null);
3083
+ const configRef = useRef7(config);
3084
+ const pendingRef = useRef7(null);
3085
+ const queuedRef = useRef7(null);
3086
+ configRef.current = config;
3087
+ useEffect8(() => {
3088
+ if (initialConfig) return;
3089
+ void loadConfig().then((next) => setConfig(applyStartup(next, cliInterval)));
3090
+ }, [initialConfig, cliInterval]);
3091
+ const drain = useCallback5(async (first, expectedRevision) => {
3092
+ let candidate = first;
3093
+ let revision = expectedRevision;
3094
+ try {
3095
+ for (; ; ) {
3096
+ const outgoing = { ...candidate, revision };
3097
+ pendingRef.current = { config: outgoing, expectedRevision: revision };
3098
+ const saved = await daemon.setConfig(outgoing, revision);
3099
+ const queued = queuedRef.current;
3100
+ queuedRef.current = null;
3101
+ if (queued) {
3102
+ candidate = queued;
3103
+ revision = saved.config.revision;
3104
+ continue;
3105
+ }
3106
+ pendingRef.current = null;
3107
+ configRef.current = saved.config;
3108
+ setConfig(saved.config);
3109
+ setConfigSaveError(null);
3110
+ return;
3111
+ }
3112
+ } catch (cause) {
3113
+ pendingRef.current = null;
3114
+ queuedRef.current = null;
3115
+ const failure = describeConfigUpdateFailure(cause);
3116
+ if (failure.conflictState) {
3117
+ configRef.current = failure.conflictState.config;
3118
+ setConfig(failure.conflictState.config);
3119
+ }
3120
+ setConfigSaveError(failure.message);
3121
+ }
3122
+ }, [daemon]);
3123
+ const updateConfig = useCallback5((fn) => {
3124
+ const current = configRef.current ?? DEFAULTS;
3125
+ const next = normalizeConfig({ ...fn(current), revision: current.revision });
3126
+ configRef.current = next;
3127
+ setConfig(next);
3128
+ setConfigSaveError(null);
3129
+ if (connected) {
3130
+ if (pendingRef.current) queuedRef.current = next;
3131
+ else void drain(next, current.revision);
3132
+ return;
3133
+ }
3134
+ try {
3135
+ saveConfigSync(next);
3136
+ } catch (cause) {
3137
+ setConfigSaveError(cause instanceof Error ? cause.message : "config could not be saved");
3138
+ }
3139
+ }, [connected, drain]);
3140
+ const daemonConfig = daemon.config?.config;
3141
+ useEffect8(() => {
3142
+ if (!connected || !daemonConfig || pendingRef.current) return;
3143
+ if (daemonConfig.revision < (configRef.current?.revision ?? 0)) return;
3144
+ configRef.current = daemonConfig;
3145
+ setConfig(daemonConfig);
3146
+ }, [connected, daemonConfig]);
3147
+ return { config, configSaveError, updateConfig };
2926
3148
  }
2927
3149
 
2928
3150
  // src/ui/hooks/use-account-form.ts
2929
- import { useState as useState7, useCallback as useCallback3 } from "react";
3151
+ import { useState as useState9, useCallback as useCallback6 } from "react";
2930
3152
  function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSettingsCursor }) {
2931
- const [accountForm, setAccountForm] = useState7(null);
3153
+ const [accountForm, setAccountForm] = useState9(null);
2932
3154
  function openAddAccount(defaults) {
2933
3155
  const providerId = defaults?.providerId ?? (detected[0] ?? "claude");
2934
3156
  setAccountForm({
@@ -2980,7 +3202,7 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
2980
3202
  });
2981
3203
  setAccountForm(null);
2982
3204
  }
2983
- const cycleFormField = useCallback3((dir) => {
3205
+ const cycleFormField = useCallback6((dir) => {
2984
3206
  setAccountForm((f) => {
2985
3207
  if (!f) return f;
2986
3208
  const i = FORM_FIELDS.indexOf(f.field);
@@ -2989,14 +3211,14 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
2989
3211
  return { ...f, field: next, caret };
2990
3212
  });
2991
3213
  }, []);
2992
- const cycleProvider = useCallback3((dir) => {
3214
+ const cycleProvider = useCallback6((dir) => {
2993
3215
  setAccountForm((f) => {
2994
3216
  if (!f) return f;
2995
3217
  const i = PROVIDER_ORDER.indexOf(f.providerId);
2996
3218
  return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
2997
3219
  });
2998
3220
  }, []);
2999
- const cycleColor = useCallback3((dir) => {
3221
+ const cycleColor = useCallback6((dir) => {
3000
3222
  setAccountForm((f) => {
3001
3223
  if (!f) return f;
3002
3224
  const i = COLOR_PALETTE.indexOf(f.color);
@@ -3037,40 +3259,42 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
3037
3259
  }
3038
3260
 
3039
3261
  // src/app.tsx
3040
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3262
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3041
3263
  function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = null, mode = "degraded" }) {
3042
3264
  const connected = mode === "connected" && baseUrl !== null && wsToken !== null;
3043
3265
  const degraded = !connected;
3044
3266
  const daemon = useDaemon(connected ? baseUrl : null, connected ? wsToken : null);
3045
- const [config, setConfig] = useState8(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
3046
- const [detected, setDetected] = useState8([]);
3047
- const [tableLocal, setTable] = useState8(null);
3048
- const [tableLoading, setTableLoading] = useState8(false);
3049
- const [tab, setTab] = useState8(0);
3050
- const [view, setView] = useState8(0);
3051
- const [cursor, setCursor] = useState8(0);
3052
- const [expanded, setExpanded] = useState8(-1);
3053
- const [sort, setSort] = useState8(1);
3054
- const [tableProvider, setTableProvider] = useState8(null);
3055
- const [tableModel, setTableModel] = useState8(null);
3056
- const [search, setSearch] = useState8("");
3057
- const [searchMode, setSearchMode] = useState8(false);
3058
- const [cursorRowsLocal, setCursorRows] = useState8(null);
3059
- const [showSettings, setShowSettings] = useState8(false);
3060
- const [settingsTab, setSettingsTab] = useState8("general");
3061
- const [settingsCursor, setSettingsCursor] = useState8(0);
3062
- const [tzEdit, setTzEdit] = useState8(null);
3063
- const [tzError, setTzError] = useState8(null);
3064
- const [tzCaret, setTzCaret] = useState8(0);
3065
- const [searchCaret, setSearchCaret] = useState8(0);
3066
- const [onboardSel, setOnboardSel] = useState8(null);
3067
- const [onboardCursor, setOnboardCursor] = useState8(0);
3068
- const [dashPage, setDashPage] = useState8(0);
3267
+ const { config, configSaveError, updateConfig } = useConfigState({
3268
+ initialConfig,
3269
+ cliInterval,
3270
+ connected,
3271
+ daemon
3272
+ });
3273
+ const [detected, setDetected] = useState10([]);
3274
+ const [tab, setTab] = useState10(0);
3275
+ const [view, setView] = useState10(0);
3276
+ const [cursor, setCursor] = useState10(0);
3277
+ const [expanded, setExpanded] = useState10(-1);
3278
+ const [sort, setSort] = useState10(1);
3279
+ const [tableProvider, setTableProvider] = useState10(null);
3280
+ const [tableModel, setTableModel] = useState10(null);
3281
+ const [search, setSearch] = useState10("");
3282
+ const [searchMode, setSearchMode] = useState10(false);
3283
+ const [showSettings, setShowSettings] = useState10(false);
3284
+ const [settingsTab, setSettingsTab] = useState10("general");
3285
+ const [settingsCursor, setSettingsCursor] = useState10(0);
3286
+ const [tzEdit, setTzEdit] = useState10(null);
3287
+ const [tzError, setTzError] = useState10(null);
3288
+ const [tzCaret, setTzCaret] = useState10(0);
3289
+ const [searchCaret, setSearchCaret] = useState10(0);
3290
+ const [onboardSel, setOnboardSel] = useState10(null);
3291
+ const [onboardCursor, setOnboardCursor] = useState10(0);
3292
+ const [dashPage, setDashPage] = useState10(0);
3069
3293
  const { exit } = useApp();
3070
3294
  const { cols, rows, resizing, live } = useTerminalSize();
3071
- const webRef = useRef6(null);
3072
- const webStartingRef = useRef6(false);
3073
- useEffect7(() => () => {
3295
+ const webRef = useRef8(null);
3296
+ const webStartingRef = useRef8(false);
3297
+ useEffect9(() => () => {
3074
3298
  void webRef.current?.stop();
3075
3299
  }, []);
3076
3300
  const cfg = config ?? DEFAULTS;
@@ -3081,22 +3305,38 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3081
3305
  const accounts = useMemo2(() => buildAccounts(cfg, detected), [cfg, detected]);
3082
3306
  const trackedAccountRows = useMemo2(() => getTrackedAccountRows(cfg, detected, accounts), [cfg, detected, accounts]);
3083
3307
  const settingsRowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
3084
- const accountsRef = useRef6([]);
3308
+ const accountsRef = useRef8([]);
3085
3309
  accountsRef.current = accounts;
3086
- const rowCountRef = useRef6(0);
3087
- const tabRef = useRef6(0);
3310
+ const rowCountRef = useRef8(0);
3311
+ const tabRef = useRef8(0);
3088
3312
  tabRef.current = tab;
3089
- const dashPageCountRef = useRef6(1);
3090
- const pendingLocalConfigRef = useRef6(null);
3091
- const tzValueRef = useRef6("");
3092
- const tzCaretRef = useRef6(0);
3093
- const searchValueRef = useRef6("");
3094
- const searchCaretRef = useRef6(0);
3313
+ const dashPageCountRef = useRef8(1);
3314
+ const tzValueRef = useRef8("");
3315
+ const tzCaretRef = useRef8(0);
3316
+ const searchValueRef = useRef8("");
3317
+ const searchCaretRef = useRef8(0);
3095
3318
  const accountsKey = useMemo2(() => accounts.map(acctKey).join("|"), [accounts]);
3319
+ const allGroups = useMemo2(() => accountsByProvider(accounts), [accounts]);
3320
+ const tableProvs = useMemo2(
3321
+ () => allGroups.map((g) => g.provider).filter((pid) => PROVIDERS[pid].hasUsage),
3322
+ [allGroups]
3323
+ );
3324
+ const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
3325
+ const tableAccounts = useMemo2(
3326
+ () => effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [],
3327
+ [accounts, effTableProvider]
3328
+ );
3096
3329
  const needsOnboarding = configReady && !cfg.onboarded;
3097
3330
  const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
3098
3331
  const showPicker = needsOnboarding || newProviders.length > 0;
3099
- const { statsLocal, peakLocal, updatedLocal, error } = useDegradedPolling({
3332
+ const {
3333
+ statsLocal,
3334
+ peakLocal,
3335
+ updatedLocal,
3336
+ tableLocal,
3337
+ tableLoading,
3338
+ refreshAll: refreshAllDegraded
3339
+ } = useDegradedPolling({
3100
3340
  degraded,
3101
3341
  configReady,
3102
3342
  showPicker,
@@ -3104,9 +3344,22 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3104
3344
  accountsRef,
3105
3345
  interval,
3106
3346
  billingMs,
3107
- tz
3347
+ tz,
3348
+ activeTableProvider: effTableProvider,
3349
+ tableVisible: tab === 1
3108
3350
  });
3109
3351
  const snapshot = daemon.snapshot;
3352
+ const daemonRefreshRef = useRef8(daemon.refresh);
3353
+ const degradedRefreshRef = useRef8(refreshAllDegraded);
3354
+ daemonRefreshRef.current = daemon.refresh;
3355
+ degradedRefreshRef.current = refreshAllDegraded;
3356
+ const requestDaemonRefresh = useCallback7(() => daemonRefreshRef.current("all"), []);
3357
+ const requestDegradedRefresh = useCallback7(() => degradedRefreshRef.current(), []);
3358
+ const { status: refreshStatus, refreshAll } = useRefreshAll({
3359
+ connected,
3360
+ requestDaemonRefresh,
3361
+ requestDegradedRefresh
3362
+ });
3110
3363
  const stats = useMemo2(
3111
3364
  () => connected ? toStatsMap(snapshot, accounts) : statsLocal,
3112
3365
  [connected, snapshot, accounts, statsLocal]
@@ -3131,7 +3384,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3131
3384
  [connected, snapshot, updatedLocal]
3132
3385
  );
3133
3386
  const intervalLabel = connected && snapshot?.intervalMs ? Math.round(snapshot.intervalMs / 1e3) : cfg.interval;
3134
- const readyInputFor = useCallback4((id) => {
3387
+ const readyInputFor = useCallback7((id) => {
3135
3388
  if (connected) {
3136
3389
  const wa = snapshot?.accounts.find((a) => a.id === id);
3137
3390
  if (!wa) return void 0;
@@ -3148,12 +3401,10 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3148
3401
  () => focusId === null ? accounts : accounts.filter((a) => a.id === focusId),
3149
3402
  [accounts, focusId]
3150
3403
  );
3151
- const allGroups = useMemo2(() => accountsByProvider(accounts), [accounts]);
3152
3404
  const groups = useMemo2(
3153
3405
  () => focusId === null ? allGroups : accountsByProvider(visibleAccounts),
3154
3406
  [allGroups, visibleAccounts, focusId]
3155
3407
  );
3156
- const tableProvs = useMemo2(() => allGroups.map((g) => g.provider), [allGroups]);
3157
3408
  const TOO_SMALL = cols < 40 || rows < 12;
3158
3409
  const allReady = accounts.length > 0 && accounts.every((a) => accountReady(readyInputFor(a.id), a.providerId));
3159
3410
  const { gridBudget } = useMemo2(() => computeChrome(slots, cols, rows), [slots, cols, rows]);
@@ -3192,22 +3443,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3192
3443
  }
3193
3444
  };
3194
3445
  const { handlePasteData, isPasteInput } = usePaste(insertText);
3195
- const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
3196
- const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
3197
- const tableAccounts = useMemo2(
3198
- () => effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [],
3199
- [accounts, effTableProvider]
3200
- );
3201
- const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
3446
+ const SORTS_FOR = SORTS;
3202
3447
  const tableAccountIds = useMemo2(() => tableAccounts.map((a) => a.id), [tableAccounts]);
3203
3448
  const table = useMemo2(
3204
3449
  () => connected ? pickTable(snapshot, tableAccountIds) : tableLocal,
3205
3450
  [connected, snapshot, tableAccountIds, tableLocal]
3206
3451
  );
3207
- const cursorRows = useMemo2(
3208
- () => connected ? toCursorRows(snapshot, tableAccounts[0]?.id) : cursorRowsLocal,
3209
- [connected, snapshot, tableAccounts, cursorRowsLocal]
3210
- );
3211
3452
  const { showLoader } = useLoader({
3212
3453
  configReady,
3213
3454
  showPicker,
@@ -3226,73 +3467,36 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3226
3467
  detected: detected.includes(pid),
3227
3468
  enabled: onboardEnabled.includes(pid)
3228
3469
  }));
3229
- useEffect7(() => {
3230
- if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
3231
- detectProviders().then(setDetected);
3470
+ useEffect9(() => {
3471
+ void detectProviders().then(setDetected);
3232
3472
  }, []);
3233
3473
  const tableKey = useMemo2(
3234
3474
  () => `${effTableProvider}|${tableAccounts.map(acctKey).join(",")}|${tz}`,
3235
3475
  [effTableProvider, tableAccounts, tz]
3236
3476
  );
3237
- useEffect7(() => {
3238
- setTable(null);
3239
- setCursorRows(null);
3477
+ useEffect9(() => {
3240
3478
  setCursor(0);
3241
3479
  setExpanded(-1);
3242
3480
  setTableModel(null);
3243
- setSort(tableIsCursor ? 0 : 1);
3244
- setTableLoading(false);
3481
+ setSort(1);
3245
3482
  }, [tableKey]);
3246
- useEffect7(() => {
3247
- if (!degraded || tab !== 1 || !effTableProvider) return;
3248
- let active = true;
3249
- let timer;
3250
- const fetchOnce = async () => {
3251
- try {
3252
- if (tableIsCursor) {
3253
- const s = await withTimeout(cursorModelSpend(tableAccounts[0]?.homeDir));
3254
- if (active) setCursorRows(s?.models ?? []);
3255
- } else {
3256
- const r = await withTimeout(fetchScopeTable(tableAccounts, tz));
3257
- if (active) setTable(r);
3258
- }
3259
- } catch {
3260
- }
3261
- };
3262
- const run = async () => {
3263
- setTableLoading(true);
3264
- await fetchOnce();
3265
- if (!active) return;
3266
- setTableLoading(false);
3267
- const loop = async () => {
3268
- await fetchOnce();
3269
- if (active) timer = setTimeout(loop, Math.max(interval, 1e4));
3270
- };
3271
- timer = setTimeout(loop, Math.max(interval, 1e4));
3272
- };
3273
- run();
3274
- return () => {
3275
- active = false;
3276
- clearTimeout(timer);
3277
- };
3278
- }, [degraded, tab, tableKey, interval]);
3279
- useEffect7(() => {
3483
+ useEffect9(() => {
3280
3484
  setCursor(0);
3281
3485
  setExpanded(-1);
3282
3486
  }, [search, tableModel]);
3283
- useEffect7(() => {
3487
+ useEffect9(() => {
3284
3488
  setDashPage((p) => Math.min(p, dashPageCount - 1));
3285
3489
  }, [dashPageCount]);
3286
- useEffect7(() => {
3490
+ useEffect9(() => {
3287
3491
  setSettingsCursor((c) => Math.max(-1, Math.min(c, settingsRowCount - 1)));
3288
3492
  }, [settingsRowCount]);
3289
- const resetView = useCallback4(() => {
3493
+ const resetView = useCallback7(() => {
3290
3494
  setCursor(0);
3291
3495
  setExpanded(-1);
3292
3496
  }, []);
3293
3497
  const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
3294
3498
  const mouse = useMouse();
3295
- useEffect7(() => {
3499
+ useEffect9(() => {
3296
3500
  if (!IS_TTY) return;
3297
3501
  mouse.enable();
3298
3502
  if (process.stdout.isTTY) {
@@ -3320,31 +3524,6 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3320
3524
  process.stdin.off("data", onData);
3321
3525
  };
3322
3526
  }, []);
3323
- const updateConfig = useCallback4((fn) => {
3324
- setConfig((prev) => {
3325
- const next = normalizeConfig(fn(prev ?? DEFAULTS));
3326
- pendingLocalConfigRef.current = connected ? next : null;
3327
- saveConfigSync(next);
3328
- if (connected) {
3329
- void daemon.setConfig(next).then((saved) => {
3330
- if (pendingLocalConfigRef.current && reconcileDaemonConfig(next, saved, pendingLocalConfigRef.current).pendingLocalConfig === null) {
3331
- pendingLocalConfigRef.current = null;
3332
- }
3333
- }).catch(() => {
3334
- });
3335
- }
3336
- return next;
3337
- });
3338
- }, [connected, daemon]);
3339
- const daemonConfig = daemon.config;
3340
- useEffect7(() => {
3341
- if (!connected || !daemonConfig) return;
3342
- setConfig((prev) => {
3343
- const reconciled = reconcileDaemonConfig(prev, daemonConfig, pendingLocalConfigRef.current);
3344
- pendingLocalConfigRef.current = reconciled.pendingLocalConfig;
3345
- return reconciled.config;
3346
- });
3347
- }, [connected, daemonConfig]);
3348
3527
  function toggleOnboard(i) {
3349
3528
  if (i < 0 || i >= pickerProviders.length) return;
3350
3529
  const pid = pickerProviders[i];
@@ -3381,19 +3560,18 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3381
3560
  setOnboardSel(null);
3382
3561
  setOnboardCursor(0);
3383
3562
  }
3384
- const cycleAccount = useCallback4((dir) => {
3563
+ const cycleAccount = useCallback7((dir) => {
3385
3564
  if (slots.length <= 1) return;
3386
3565
  const next = (activeSlotIdx + dir + slots.length) % slots.length;
3387
3566
  updateConfig((c) => ({ ...c, activeAccountId: slots[next].id }));
3388
3567
  resetView();
3389
3568
  }, [slots, activeSlotIdx, updateConfig, resetView]);
3390
- const cycleTableProvider = useCallback4((dir) => {
3569
+ const cycleTableProvider = useCallback7((dir) => {
3391
3570
  if (tableProvs.length <= 1) return;
3392
3571
  const cur = effTableProvider ? tableProvs.indexOf(effTableProvider) : 0;
3393
3572
  const nextProv = tableProvs[(cur + dir + tableProvs.length) % tableProvs.length];
3394
3573
  setTableProvider(nextProv);
3395
- const nextIsCursor = !!nextProv && !PROVIDERS[nextProv].hasUsage;
3396
- setSort(nextIsCursor ? 0 : 1);
3574
+ setSort(1);
3397
3575
  setCursor(0);
3398
3576
  setExpanded(-1);
3399
3577
  setTableModel(null);
@@ -3420,30 +3598,30 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3420
3598
  return;
3421
3599
  }
3422
3600
  if (webRef.current) {
3423
- openUrl(webRef.current.url);
3601
+ openUrl(webRef.current.browserUrl);
3424
3602
  return;
3425
3603
  }
3426
3604
  if (webStartingRef.current) return;
3427
3605
  webStartingRef.current = true;
3428
3606
  try {
3429
- const { startWebServer } = await import("./server-56C7MMEZ.js");
3607
+ const { startWebServer } = await import("./server-4CDZMLH6.js");
3430
3608
  const ctrl = await startWebServer({ config: cfg, log: false });
3431
3609
  webRef.current = ctrl;
3432
- openUrl(ctrl.url);
3610
+ openUrl(ctrl.browserUrl);
3433
3611
  } catch {
3434
3612
  } finally {
3435
3613
  webStartingRef.current = false;
3436
3614
  }
3437
3615
  }
3438
- const onTabSelect = useCallback4((i) => {
3616
+ const onTabSelect = useCallback7((i) => {
3439
3617
  setTab(i);
3440
3618
  resetView();
3441
3619
  }, [resetView]);
3442
- const onStripSelect = useCallback4((i) => {
3620
+ const onStripSelect = useCallback7((i) => {
3443
3621
  updateConfig((c) => ({ ...c, activeAccountId: slots[i].id }));
3444
3622
  resetView();
3445
3623
  }, [slots, updateConfig, resetView]);
3446
- const onProviderSelect = useCallback4((p) => {
3624
+ const onProviderSelect = useCallback7((p) => {
3447
3625
  setTableProvider(p);
3448
3626
  setCursor(0);
3449
3627
  setExpanded(-1);
@@ -3452,105 +3630,108 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3452
3630
  setSearchCaret(0);
3453
3631
  setSearchMode(false);
3454
3632
  }, []);
3455
- const onRowClickToken = useCallback4((idx) => {
3633
+ const onRowClickToken = useCallback7((idx) => {
3456
3634
  if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
3457
3635
  else setCursor(idx);
3458
3636
  }, [cursor]);
3459
- const onRowClickCursor = useCallback4((idx) => setCursor(idx), []);
3460
3637
  const rawTokenRows = useMemo2(
3461
- () => tab === 1 && !tableIsCursor ? table ? [table.daily, table.weekly, table.monthly][view] : [] : [],
3462
- [tab, tableIsCursor, table, view]
3638
+ () => tab === 1 ? table ? [table.daily, table.weekly, table.monthly][view] : [] : [],
3639
+ [tab, table, view]
3463
3640
  );
3464
3641
  const tokenModelOptions = useMemo2(() => tableModelOptions(rawTokenRows), [rawTokenRows]);
3465
- useEffect7(() => {
3642
+ useEffect9(() => {
3466
3643
  if (tableModel && !tokenModelOptions.includes(tableModel)) setTableModel(null);
3467
3644
  }, [tableModel, tokenModelOptions]);
3468
- const cycleTableModelFilter = useCallback4((dir) => {
3645
+ const cycleTableModelFilter = useCallback7((dir) => {
3469
3646
  setTableModel((cur) => cycleTableModel(cur, tokenModelOptions, dir));
3470
3647
  resetView();
3471
3648
  }, [tokenModelOptions, resetView]);
3472
3649
  const activeTableModel = tableModel && tokenModelOptions.includes(tableModel) ? tableModel : null;
3473
3650
  const tokenRows = useMemo2(
3474
- () => tab === 1 && !tableIsCursor ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
3475
- [tab, tableIsCursor, rawTokenRows, activeTableModel, search, sort]
3476
- );
3477
- const cursorTableRows = useMemo2(
3478
- () => tab === 1 && tableIsCursor ? sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort) : [],
3479
- [tab, tableIsCursor, cursorRows, search, sort]
3651
+ () => tab === 1 ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
3652
+ [tab, rawTokenRows, activeTableModel, search, sort]
3480
3653
  );
3481
3654
  useInput((input, key) => handleKey(input, key, {
3482
- showPicker,
3483
- pickerProviders,
3484
- onboardCursor,
3485
- setOnboardCursor,
3486
- toggleOnboard,
3487
- confirmOnboarding,
3488
- exit,
3489
- showSettings,
3490
- accountForm,
3491
- setAccountForm,
3492
- commitAccountForm,
3493
- cycleFormField,
3494
- cycleProvider,
3495
- cycleColor,
3496
- isPrintable,
3497
- insertText,
3498
- tzEdit,
3499
- setTzEdit,
3500
- setTzError,
3501
- updateConfig,
3502
- setTzCaret,
3503
- tzValueRef,
3504
- tzCaretRef,
3505
- tab,
3506
- searchMode,
3507
- setSearchMode,
3508
- search,
3509
- setSearch,
3510
- setSearchCaret,
3511
- searchValueRef,
3512
- searchCaretRef,
3513
- showLoader,
3514
- configReady,
3515
- toggleWeb,
3516
- settingsCursor,
3517
- settingsTab,
3518
- setSettingsTab,
3519
- setShowSettings,
3520
- cfg,
3521
- trackedAccountRows,
3522
- moveAccount,
3523
- setSettingsCursor,
3524
- toggleProvider,
3525
- openEditAccount,
3526
- openConfigureAccount,
3527
- deleteAccount,
3528
- openAddAccount,
3529
- cycleAccount,
3530
- setTab,
3531
- resetView,
3532
- slots,
3533
- dashPaginated,
3534
- dashPageCount,
3535
- setDashPage,
3536
- cycleTableProvider,
3537
- setExpanded,
3538
- setSort,
3539
- SORTS_FOR,
3540
- tableIsCursor,
3541
- cycleTableModel: cycleTableModelFilter,
3542
- setView,
3543
- cursor,
3544
- rowCountRef,
3545
- rows,
3546
- setCursor,
3547
- clampRow
3655
+ onboarding: {
3656
+ show: showPicker,
3657
+ providers: pickerProviders,
3658
+ cursor: onboardCursor,
3659
+ setCursor: setOnboardCursor,
3660
+ toggle: toggleOnboard,
3661
+ confirm: confirmOnboarding
3662
+ },
3663
+ accountEditor: {
3664
+ form: accountForm,
3665
+ setForm: setAccountForm,
3666
+ commit: commitAccountForm,
3667
+ cycleField: cycleFormField,
3668
+ cycleProvider,
3669
+ cycleColor
3670
+ },
3671
+ timezoneEditor: {
3672
+ value: tzEdit,
3673
+ setValue: setTzEdit,
3674
+ setError: setTzError,
3675
+ setCaret: setTzCaret,
3676
+ valueRef: tzValueRef,
3677
+ caretRef: tzCaretRef
3678
+ },
3679
+ textInput: { isPrintable, insert: insertText },
3680
+ settings: {
3681
+ show: showSettings,
3682
+ setShow: setShowSettings,
3683
+ cursor: settingsCursor,
3684
+ tab: settingsTab,
3685
+ setTab: setSettingsTab,
3686
+ setCursor: setSettingsCursor,
3687
+ trackedAccounts: trackedAccountRows,
3688
+ moveAccount,
3689
+ toggleProvider,
3690
+ openEditAccount,
3691
+ openConfigureAccount,
3692
+ deleteAccount,
3693
+ openAddAccount
3694
+ },
3695
+ table: {
3696
+ tab,
3697
+ searchMode,
3698
+ setSearchMode,
3699
+ search,
3700
+ setSearch,
3701
+ setSearchCaret,
3702
+ searchValueRef,
3703
+ searchCaretRef,
3704
+ cycleProvider: cycleTableProvider,
3705
+ setExpanded,
3706
+ setSort,
3707
+ sorts: SORTS_FOR,
3708
+ cycleModel: cycleTableModelFilter,
3709
+ setView,
3710
+ cursor,
3711
+ rowCountRef,
3712
+ rows,
3713
+ setCursor,
3714
+ clampRow
3715
+ },
3716
+ dashboard: { paginated: dashPaginated, pageCount: dashPageCount, setPage: setDashPage },
3717
+ global: {
3718
+ exit,
3719
+ showLoader,
3720
+ configReady,
3721
+ toggleWeb,
3722
+ config: cfg,
3723
+ updateConfig,
3724
+ cycleAccount,
3725
+ setTab,
3726
+ resetView,
3727
+ slots,
3728
+ refreshAll
3729
+ }
3548
3730
  }), { isActive: IS_TTY });
3549
- if (error) return /* @__PURE__ */ jsx11(Box11, { padding: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: error }) });
3550
- if (!config) return /* @__PURE__ */ jsx11(Box11, { padding: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Loading..." }) });
3551
- if (resizing) return /* @__PURE__ */ jsx11(ResizingView, { cols: live.cols, rows: live.rows });
3731
+ if (!config) return /* @__PURE__ */ jsx12(Box11, { padding: 1, children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Loading..." }) });
3732
+ if (resizing) return /* @__PURE__ */ jsx12(ResizingView, { cols: live.cols, rows: live.rows });
3552
3733
  if (showPicker) {
3553
- return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx11(
3734
+ return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx12(
3554
3735
  Onboarding,
3555
3736
  {
3556
3737
  items: onboardItems,
@@ -3563,20 +3744,20 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3563
3744
  ) });
3564
3745
  }
3565
3746
  if (showLoader) {
3566
- return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: /* @__PURE__ */ jsx11(LoadingView, { groups: allGroups, stats, cols, rows, readyInput: readyInputFor, privacyMode: cfg.privacyMode }) });
3747
+ return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: /* @__PURE__ */ jsx12(LoadingView, { groups: allGroups, stats, cols, rows, readyInput: readyInputFor, privacyMode: cfg.privacyMode }) });
3567
3748
  }
3568
3749
  if (TOO_SMALL && !showSettings) {
3569
- return /* @__PURE__ */ jsx11(TinyFallback, { groups, stats, rows, cols });
3750
+ return /* @__PURE__ */ jsx12(TinyFallback, { groups, stats, rows, cols, refreshStatus });
3570
3751
  }
3571
- rowCountRef.current = tableIsCursor ? cursorTableRows.length : tokenRows.length;
3572
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
3573
- /* @__PURE__ */ jsxs11(Box11, { justifyContent: "space-between", children: [
3574
- /* @__PURE__ */ jsxs11(Box11, { children: [
3575
- /* @__PURE__ */ jsxs11(Text11, { bold: true, color: "greenBright", children: [
3752
+ rowCountRef.current = tokenRows.length;
3753
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
3754
+ /* @__PURE__ */ jsxs12(Box11, { justifyContent: "space-between", children: [
3755
+ /* @__PURE__ */ jsxs12(Box11, { children: [
3756
+ /* @__PURE__ */ jsxs12(Text12, { bold: true, color: "greenBright", children: [
3576
3757
  glyphs().dotSel,
3577
3758
  " tokmon"
3578
3759
  ] }),
3579
- /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3760
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3580
3761
  " ",
3581
3762
  glyphs().middot,
3582
3763
  " every ",
@@ -3584,31 +3765,37 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3584
3765
  "s"
3585
3766
  ] })
3586
3767
  ] }),
3587
- /* @__PURE__ */ jsxs11(Box11, { children: [
3588
- peak && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3589
- /* @__PURE__ */ jsx11(PeakBadge, { peak }),
3590
- /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3768
+ /* @__PURE__ */ jsxs12(Box11, { children: [
3769
+ peak && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3770
+ /* @__PURE__ */ jsx12(PeakBadge, { peak }),
3771
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3591
3772
  " ",
3592
3773
  glyphs().middot,
3593
3774
  " "
3594
3775
  ] })
3595
3776
  ] }),
3596
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: time(updated, tz) })
3777
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: time(updated, tz) })
3597
3778
  ] })
3598
3779
  ] }),
3599
- degraded && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3780
+ degraded && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3600
3781
  glyphs().warn,
3601
3782
  " degraded ",
3602
3783
  glyphs().middot,
3603
3784
  " background service unavailable, running in-process"
3604
3785
  ] }),
3605
- connected && daemon.conn !== "live" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3786
+ connected && daemon.conn !== "live" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3606
3787
  glyphs().warn,
3607
3788
  " reconnecting ",
3608
3789
  glyphs().middot,
3609
3790
  " showing last known data"
3610
3791
  ] }),
3611
- showSettings ? /* @__PURE__ */ jsx11(
3792
+ configSaveError && /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
3793
+ glyphs().warn,
3794
+ " ",
3795
+ configSaveError
3796
+ ] }),
3797
+ /* @__PURE__ */ jsx12(RefreshStatusLine, { status: refreshStatus }),
3798
+ showSettings ? /* @__PURE__ */ jsx12(
3612
3799
  SettingsView,
3613
3800
  {
3614
3801
  config: cfg,
@@ -3623,20 +3810,20 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3623
3810
  trackedAccounts: trackedAccountRows,
3624
3811
  accountIdentities
3625
3812
  }
3626
- ) : /* @__PURE__ */ jsxs11(Fragment4, { children: [
3627
- /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, marginBottom: 1, children: [
3628
- /* @__PURE__ */ jsx11(TabBar, { tabs: TABS, active: tab, onSelect: onTabSelect }),
3629
- /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3813
+ ) : /* @__PURE__ */ jsxs12(Fragment4, { children: [
3814
+ /* @__PURE__ */ jsxs12(Box11, { marginTop: 1, marginBottom: 1, children: [
3815
+ /* @__PURE__ */ jsx12(TabBar, { tabs: TABS, active: tab, onSelect: onTabSelect }),
3816
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3630
3817
  " Tab/",
3631
3818
  glyphs().arrowL,
3632
3819
  glyphs().arrowR
3633
3820
  ] })
3634
3821
  ] }),
3635
- tab === 0 && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3636
- /* @__PURE__ */ jsx11(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode }),
3637
- slots.length > 1 && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
3638
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "focus " }),
3639
- /* @__PURE__ */ jsx11(
3822
+ tab === 0 && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3823
+ /* @__PURE__ */ jsx12(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode }),
3824
+ slots.length > 1 && /* @__PURE__ */ jsxs12(Box11, { marginTop: 1, children: [
3825
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "focus " }),
3826
+ /* @__PURE__ */ jsx12(
3640
3827
  AccountStrip,
3641
3828
  {
3642
3829
  slots,
@@ -3645,12 +3832,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3645
3832
  }
3646
3833
  )
3647
3834
  ] }),
3648
- /* @__PURE__ */ jsx11(TotalsRow, { groups, stats, cols })
3835
+ /* @__PURE__ */ jsx12(TotalsRow, { groups, stats, cols })
3649
3836
  ] }),
3650
- tab === 1 && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3651
- tableProvs.length > 0 && /* @__PURE__ */ jsx11(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: onProviderSelect }),
3652
- /* @__PURE__ */ jsx11(Box11, { height: 1 }),
3653
- /* @__PURE__ */ jsx11(
3837
+ tab === 1 && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3838
+ tableProvs.length > 0 && /* @__PURE__ */ jsx12(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: onProviderSelect }),
3839
+ /* @__PURE__ */ jsx12(Box11, { height: 1 }),
3840
+ /* @__PURE__ */ jsx12(
3654
3841
  ControlBar,
3655
3842
  {
3656
3843
  views: VIEWS,
@@ -3660,24 +3847,16 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3660
3847
  search,
3661
3848
  searchCaret,
3662
3849
  searching: searchMode,
3663
- showPeriod: !tableIsCursor,
3664
- showModel: !tableIsCursor
3850
+ showPeriod: true,
3851
+ showModel: true
3665
3852
  }
3666
3853
  ),
3667
- /* @__PURE__ */ jsx11(Box11, { height: 1 }),
3668
- !effTableProvider ? /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3854
+ /* @__PURE__ */ jsx12(Box11, { height: 1 }),
3855
+ !effTableProvider ? /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3669
3856
  "No providers enabled ",
3670
3857
  glyphs().emDash,
3671
3858
  " press s to pick providers."
3672
- ] }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx11(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx11(
3673
- CursorSpendTable,
3674
- {
3675
- rows: cursorTableRows,
3676
- cursor,
3677
- maxRows: Math.max(1, rows - 16),
3678
- onRowClick: onRowClickCursor
3679
- }
3680
- ) : /* @__PURE__ */ jsx11(
3859
+ ] }) : tableLoading && !table ? /* @__PURE__ */ jsx12(Spinner, { label: "Loading history" }) : /* @__PURE__ */ jsx12(
3681
3860
  TokenTable,
3682
3861
  {
3683
3862
  rows: tokenRows,
@@ -3690,12 +3869,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3690
3869
  )
3691
3870
  ] })
3692
3871
  ] }),
3693
- (tab === 0 || showSettings) && /* @__PURE__ */ jsx11(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols })
3872
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx12(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols })
3694
3873
  ] });
3695
3874
  }
3696
3875
 
3697
3876
  // src/bootstrap-ink.tsx
3698
- import { jsx as jsx12 } from "react/jsx-runtime";
3877
+ import { jsx as jsx13 } from "react/jsx-runtime";
3699
3878
  function enterAltScreen() {
3700
3879
  process.stdout.write("\x1B[?1049h\x1B[H");
3701
3880
  }
@@ -3724,7 +3903,7 @@ async function bootstrapInk({ interval, config, daemon, mode }) {
3724
3903
  if (altScreen) leaveAltScreen();
3725
3904
  });
3726
3905
  const { waitUntilExit } = render(
3727
- /* @__PURE__ */ jsx12(MouseProvider, { children: /* @__PURE__ */ jsx12(
3906
+ /* @__PURE__ */ jsx13(MouseProvider, { children: /* @__PURE__ */ jsx13(
3728
3907
  App,
3729
3908
  {
3730
3909
  interval,