tokmon 0.22.7 → 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 +1 -0
  2. package/dist/{bootstrap-ink-OHPQZKGB.js → bootstrap-ink-UD3KDTOB.js} +1113 -818
  3. package/dist/{chunk-EMQL5P5J.js → chunk-AON6GE7D.js} +368 -280
  4. package/dist/{chunk-TNIADJ3W.js → chunk-DJPUYMZM.js} +49 -16
  5. package/dist/chunk-HV6AYQDB.js +192 -0
  6. package/dist/chunk-HXDAOOSI.js +154 -0
  7. package/dist/{chunk-I4USANOF.js → chunk-PGQK7MOE.js} +879 -391
  8. package/dist/cli.js +93 -54
  9. package/dist/{config-WLRQHLVJ.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 +9 -5
  30. package/dist/daemon-IP3UCTPB.js +0 -213
  31. package/dist/daemon-handle-HLSKLMWU.js +0 -142
  32. package/dist/server-GA2FKYDQ.js +0 -9
  33. package/dist/web/assets/breakdown-DU2j1sQ8.js +0 -4
  34. package/dist/web/assets/index-BCaPGYQA.css +0 -1
  35. package/dist/web/assets/index-ygZyXDLT.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-CO2B2c-0.js +0 -1
@@ -19,7 +19,10 @@ import {
19
19
  time,
20
20
  tokens,
21
21
  withTimeout
22
- } from "./chunk-I4USANOF.js";
22
+ } from "./chunk-PGQK7MOE.js";
23
+ import {
24
+ glyphs
25
+ } from "./chunk-RF4GGQGM.js";
23
26
  import {
24
27
  COLOR_PALETTE,
25
28
  DEFAULTS,
@@ -34,54 +37,17 @@ import {
34
37
  sanitizeTyped,
35
38
  saveConfigSync,
36
39
  snapshotCacheFile
37
- } from "./chunk-TNIADJ3W.js";
38
- import {
39
- glyphs
40
- } from "./chunk-RF4GGQGM.js";
40
+ } from "./chunk-DJPUYMZM.js";
41
41
 
42
42
  // src/bootstrap-ink.tsx
43
43
  import { render } from "ink";
44
44
  import { MouseProvider } from "@zenobius/ink-mouse";
45
45
 
46
46
  // src/app.tsx
47
- import { useState as useState8, useEffect as useEffect7, useCallback as useCallback4, useRef as useRef6, useMemo as useMemo2 } from "react";
48
- 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";
49
49
  import { useMouse } from "@zenobius/ink-mouse";
50
50
 
51
- // src/config-sync.ts
52
- function deepEqual(a, b) {
53
- if (a === b) return true;
54
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
55
- if (Array.isArray(a) || Array.isArray(b)) {
56
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
57
- for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
58
- return true;
59
- }
60
- const ka = Object.keys(a);
61
- const kb = Object.keys(b);
62
- if (ka.length !== kb.length) return false;
63
- for (const k of ka) {
64
- if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
65
- if (!deepEqual(a[k], b[k])) return false;
66
- }
67
- return true;
68
- }
69
- function reconcileDaemonConfig(previous, daemonConfig, pendingLocalConfig) {
70
- if (pendingLocalConfig) {
71
- if (deepEqual(daemonConfig, pendingLocalConfig)) {
72
- return { config: previous && deepEqual(previous, daemonConfig) ? previous : daemonConfig, pendingLocalConfig: null };
73
- }
74
- return { config: previous ?? pendingLocalConfig, pendingLocalConfig };
75
- }
76
- if (previous && deepEqual(previous, daemonConfig)) {
77
- return { config: previous, pendingLocalConfig: null };
78
- }
79
- if (previous?.onboarded === true && daemonConfig.onboarded === false) {
80
- return { config: previous, pendingLocalConfig: null };
81
- }
82
- return { config: daemonConfig, pendingLocalConfig: null };
83
- }
84
-
85
51
  // src/ui/shared.tsx
86
52
  import { appendFileSync } from "fs";
87
53
  import { memo, useEffect, useRef, useState } from "react";
@@ -1541,7 +1507,7 @@ function osc8(text, url) {
1541
1507
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1542
1508
  var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1543
1509
  const inner = cols - 4;
1544
- 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;
1545
1511
  const optHint = (glyphs().shift === "\u21E7" ? "\u2325" : "opt") + "-click links ";
1546
1512
  const OPT = IS_APPLE_TERMINAL ? optHint.length : 0;
1547
1513
  const JUMP = "0-9=jump a/A=cycle ".length;
@@ -1550,7 +1516,7 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1550
1516
  const showJump = hasAccounts && inner >= BASE + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
1551
1517
  const showPage = paginated && inner >= BASE + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
1552
1518
  if (inner < BASE) {
1553
- 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" }) });
1554
1520
  }
1555
1521
  return /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexWrap: "nowrap", children: [
1556
1522
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "by " }),
@@ -1560,7 +1526,7 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1560
1526
  /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1561
1527
  ") ",
1562
1528
  glyphs().middot,
1563
- " O=repo W=web s=settings "
1529
+ " r/R=refresh O=repo W=web s=settings "
1564
1530
  ] }),
1565
1531
  showOpt && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: optHint }),
1566
1532
  showJump && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "0-9=jump a/A=cycle " }),
@@ -1570,30 +1536,52 @@ var Footer = memo6(function Footer2({ hasAccounts, paginated, cols }) {
1570
1536
  });
1571
1537
 
1572
1538
  // src/ui/tiny-fallback.tsx
1573
- 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";
1574
1544
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1575
- 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 }) {
1576
1563
  const maxLines = Math.max(1, rows - 4);
1577
1564
  const visible = groups.slice(0, maxLines);
1578
1565
  const hidden = groups.length - visible.length;
1579
1566
  const w = Math.max(8, cols - 2);
1580
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
1581
- /* @__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: [
1582
1569
  glyphs().dotSel,
1583
1570
  " tokmon"
1584
1571
  ] }),
1585
- groups.length === 0 ? /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
1572
+ groups.length === 0 ? /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
1586
1573
  "No providers ",
1587
1574
  glyphs().emDash,
1588
1575
  " s=settings"
1589
- ] }) : visible.map((g) => /* @__PURE__ */ jsx10(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
1590
- 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: [
1591
1578
  "+",
1592
1579
  hidden,
1593
1580
  " more (enlarge terminal)"
1594
1581
  ] }),
1595
- /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1596
- /* @__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" })
1597
1585
  ] });
1598
1586
  }
1599
1587
  function TinyRow({ provider, accounts, stats, width }) {
@@ -1604,14 +1592,14 @@ function TinyRow({ provider, accounts, stats, width }) {
1604
1592
  const pctMetric = billings.flatMap((b) => b?.metrics ?? []).find((m) => m.format.kind === "percent");
1605
1593
  const detail = meta.hasUsage ? `${currency(todayCost)} today` : pctMetric ? `${Math.round(pctMetric.used)}%` : "billing";
1606
1594
  const name = truncateName(meta.name, Math.max(4, width - 18));
1607
- return /* @__PURE__ */ jsxs10(Box10, { width, children: [
1608
- /* @__PURE__ */ jsxs10(Text10, { color: meta.color, children: [
1595
+ return /* @__PURE__ */ jsxs11(Box10, { width, children: [
1596
+ /* @__PURE__ */ jsxs11(Text11, { color: meta.color, children: [
1609
1597
  glyphs().dot,
1610
1598
  " "
1611
1599
  ] }),
1612
- /* @__PURE__ */ jsx10(Text10, { bold: true, color: meta.color, children: name }),
1613
- /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1614
- /* @__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 })
1615
1603
  ] });
1616
1604
  }
1617
1605
 
@@ -1635,6 +1623,8 @@ function toWsUrl(baseUrl, token) {
1635
1623
  if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1636
1624
  throw new Error(`unsupported daemon RPC protocol: ${url.protocol}`);
1637
1625
  }
1626
+ url.hash = "";
1627
+ url.searchParams.delete("tokmonToken");
1638
1628
  url.pathname = TOKMON_WS_PATH;
1639
1629
  if (token) url.searchParams.set("wsToken", token);
1640
1630
  return url.toString();
@@ -1821,12 +1811,12 @@ function createDaemonRpcClient(baseUrl, options = {}) {
1821
1811
  };
1822
1812
  };
1823
1813
  return {
1824
- getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})),
1825
- 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),
1826
1816
  refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
1827
1817
  browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
1828
- subscribeSnapshot: (onSnapshot) => subscribe((client) => client[TOKMON_WS_METHODS.snapshot]({}), onSnapshot),
1829
- 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),
1830
1820
  async close() {
1831
1821
  if (closed) return;
1832
1822
  closed = true;
@@ -1883,7 +1873,7 @@ function useDaemon(baseUrl, wsToken) {
1883
1873
  snapshot,
1884
1874
  conn,
1885
1875
  config,
1886
- setConfig: (next) => requireClient().setConfig(next),
1876
+ setConfig: (next, expectedRevision) => requireClient().setConfig({ config: next, expectedRevision }),
1887
1877
  refresh: (scope) => requireClient().refresh(scope),
1888
1878
  browse: (path) => requireClient().browseFs(path)
1889
1879
  };
@@ -1950,19 +1940,6 @@ function upsert(prev, account, patch) {
1950
1940
  next.set(account.id, { ...cur, account, ...patch });
1951
1941
  return next;
1952
1942
  }
1953
- async function fetchScopeTable(scope, tz) {
1954
- const tables = await Promise.all(scope.map(async (acc) => {
1955
- const provider = PROVIDERS[acc.providerId];
1956
- if (!provider.fetchTable) return null;
1957
- try {
1958
- return await provider.fetchTable(acc, tz);
1959
- } catch {
1960
- return null;
1961
- }
1962
- }));
1963
- const valid = tables.filter((t) => t !== null);
1964
- return coalesceTables(valid);
1965
- }
1966
1943
  function sortLabel(entry) {
1967
1944
  if (entry.dir === "up") return `${entry.label} ${glyphs().arrowU}`;
1968
1945
  if (entry.dir === "down") return `${entry.label} ${glyphs().arrowD}`;
@@ -2028,555 +2005,564 @@ function filterTokenRows(rows, q) {
2028
2005
  return rows.filter((r) => r.label.toLowerCase().includes(s) || r.models.some((m) => m.toLowerCase().includes(s)));
2029
2006
  }
2030
2007
 
2031
- // src/ui/keybindings.ts
2032
- function handleKey(input, key, ctx) {
2033
- const {
2034
- showPicker,
2035
- pickerProviders,
2036
- onboardCursor,
2037
- setOnboardCursor,
2038
- toggleOnboard,
2039
- confirmOnboarding,
2040
- exit,
2041
- showSettings,
2042
- accountForm,
2043
- setAccountForm,
2044
- commitAccountForm,
2045
- cycleFormField,
2046
- cycleProvider,
2047
- cycleColor,
2048
- isPrintable,
2049
- insertText,
2050
- tzEdit,
2051
- setTzEdit,
2052
- setTzError,
2053
- updateConfig,
2054
- setTzCaret,
2055
- tzValueRef,
2056
- tzCaretRef,
2057
- tab,
2058
- searchMode,
2059
- setSearchMode,
2060
- search,
2061
- setSearch,
2062
- setSearchCaret,
2063
- searchValueRef,
2064
- searchCaretRef,
2065
- showLoader,
2066
- configReady,
2067
- toggleWeb,
2068
- settingsCursor,
2069
- settingsTab,
2070
- setSettingsTab,
2071
- setShowSettings,
2072
- cfg,
2073
- trackedAccountRows,
2074
- moveAccount,
2075
- setSettingsCursor,
2076
- toggleProvider,
2077
- openEditAccount,
2078
- openConfigureAccount,
2079
- deleteAccount,
2080
- openAddAccount,
2081
- cycleAccount,
2082
- setTab,
2083
- resetView,
2084
- slots,
2085
- dashPaginated,
2086
- dashPageCount,
2087
- setDashPage,
2088
- cycleTableProvider,
2089
- setExpanded,
2090
- setSort,
2091
- SORTS_FOR,
2092
- cycleTableModel: cycleTableModel2,
2093
- setView,
2094
- cursor,
2095
- rowCountRef,
2096
- rows,
2097
- setCursor,
2098
- clampRow
2099
- } = ctx;
2100
- if (showPicker) {
2101
- if (input === "q") {
2102
- exit();
2103
- return;
2104
- }
2105
- const startIdx = pickerProviders.length;
2106
- if (key.upArrow) {
2107
- setOnboardCursor((c) => Math.max(0, c - 1));
2108
- return;
2109
- }
2110
- if (key.downArrow) {
2111
- 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);
2112
2035
  return;
2113
2036
  }
2114
- if (input === " ") {
2115
- toggleOnboard(onboardCursor);
2037
+ if (key.rightArrow) {
2038
+ cycleProvider(1);
2116
2039
  return;
2117
2040
  }
2118
2041
  if (key.return) {
2119
- if (onboardCursor === startIdx) confirmOnboarding();
2120
- else toggleOnboard(onboardCursor);
2042
+ setForm((value) => value && { ...value, field: "name", caret: value.name.length });
2121
2043
  return;
2122
2044
  }
2123
2045
  return;
2124
2046
  }
2125
- if (showSettings && accountForm) {
2126
- if (key.escape) {
2127
- setAccountForm(null);
2128
- return;
2129
- }
2130
- if (key.ctrl && input === "s") {
2131
- commitAccountForm();
2047
+ if (form.field === "color") {
2048
+ if (key.leftArrow) {
2049
+ cycleColor(-1);
2132
2050
  return;
2133
2051
  }
2134
- if (key.tab) {
2135
- cycleFormField(key.shift ? -1 : 1);
2052
+ if (key.rightArrow) {
2053
+ cycleColor(1);
2136
2054
  return;
2137
2055
  }
2138
- if (key.upArrow) {
2139
- cycleFormField(-1);
2056
+ if (key.return) {
2057
+ commit();
2140
2058
  return;
2141
2059
  }
2142
- if (key.downArrow) {
2143
- 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 });
2144
2082
  return;
2145
2083
  }
2146
- if (accountForm.field === "provider") {
2147
- if (key.leftArrow) {
2148
- cycleProvider(-1);
2149
- return;
2150
- }
2151
- if (key.rightArrow) {
2152
- cycleProvider(1);
2153
- return;
2154
- }
2155
- if (key.return) {
2156
- setAccountForm((f) => f && { ...f, field: "name", caret: f.name.length });
2157
- return;
2158
- }
2159
- 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}`);
2160
2124
  }
2161
- if (accountForm.field === "color") {
2162
- if (key.leftArrow) {
2163
- cycleColor(-1);
2164
- return;
2165
- }
2166
- if (key.rightArrow) {
2167
- cycleColor(1);
2168
- return;
2169
- }
2170
- if (key.return) {
2171
- commitAccountForm();
2172
- return;
2173
- }
2174
- 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);
2175
2164
  }
2176
- 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) {
2177
2244
  if (key.leftArrow) {
2178
- setAccountForm((f) => f && { ...f, caret: clampCaret(f.caret - 1, f[tf].length) });
2245
+ switchTab(-1);
2179
2246
  return;
2180
2247
  }
2181
2248
  if (key.rightArrow) {
2182
- setAccountForm((f) => f && { ...f, caret: clampCaret(f.caret + 1, f[tf].length) });
2183
- return;
2184
- }
2185
- if (key.ctrl && input === "a") {
2186
- setAccountForm((f) => f && { ...f, caret: 0 });
2249
+ switchTab(1);
2187
2250
  return;
2188
2251
  }
2189
- if (key.ctrl && input === "e") {
2190
- setAccountForm((f) => f && { ...f, caret: f[tf].length });
2191
- return;
2192
- }
2193
- if (key.return) {
2194
- if (tf === "name" && accountForm.name.trim() === "") {
2195
- setAccountForm((f) => f && { ...f, error: "Name required", caret: f.name.length });
2196
- return;
2197
- }
2198
- setAccountForm((f) => f && { ...f, field: tf === "name" ? "homeDir" : "color", caret: tf === "name" ? f.homeDir.length : f.caret });
2252
+ if (key.downArrow || key.return) {
2253
+ setCursor(0);
2199
2254
  return;
2200
2255
  }
2201
- if (key.backspace || key.delete) {
2202
- setAccountForm((f) => {
2203
- if (!f || f.field !== "name" && f.field !== "homeDir") return f;
2204
- const r = spliceBackspace(f[f.field], f.caret);
2205
- return { ...f, [f.field]: r.value, caret: r.caret, error: null };
2206
- });
2256
+ if (key.upArrow) {
2257
+ setCursor(Math.max(0, rowCount - 1));
2207
2258
  return;
2208
2259
  }
2209
- if (isPrintable(input, key)) {
2210
- const clean = sanitizeTyped(input);
2211
- if (clean) insertText(clean);
2212
- }
2213
2260
  return;
2214
2261
  }
2215
- if (showSettings && tzEdit !== null) {
2216
- if (key.escape) {
2217
- setTzEdit(null);
2218
- setTzError(null);
2219
- 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]);
2220
2282
  }
2283
+ return;
2284
+ }
2285
+ if (cursor < trackedAccounts.length) {
2286
+ const row = trackedAccounts[cursor];
2221
2287
  if (key.return) {
2222
- const val = tzEdit.trim();
2223
- if (val === "" || val.toLowerCase() === "system") {
2224
- updateConfig((c) => ({ ...c, timezone: null }));
2225
- setTzEdit(null);
2226
- setTzError(null);
2227
- } else if (isValidTimezone(val)) {
2228
- updateConfig((c) => ({ ...c, timezone: val }));
2229
- setTzEdit(null);
2230
- setTzError(null);
2288
+ if (row.source === "configured") {
2289
+ const account = config.accounts.find((candidate) => candidate.id === row.explicitId);
2290
+ if (account) openEditAccount(account);
2231
2291
  } else {
2232
- setTzError(`Invalid: ${val}`);
2292
+ openConfigureAccount(row);
2233
2293
  }
2234
2294
  return;
2235
2295
  }
2236
- if (key.leftArrow) {
2237
- setTzCaret((c) => clampCaret(c - 1, tzEdit.length));
2238
- return;
2239
- }
2240
- if (key.rightArrow) {
2241
- setTzCaret((c) => clampCaret(c + 1, tzEdit.length));
2242
- return;
2243
- }
2244
- if (key.ctrl && input === "a") {
2245
- setTzCaret(0);
2246
- return;
2247
- }
2248
- if (key.ctrl && input === "e") {
2249
- setTzCaret(tzEdit.length);
2296
+ if (row.source === "configured" && row.explicitId && (input === "d" || input === "x")) {
2297
+ deleteAccount(row.explicitId);
2250
2298
  return;
2251
2299
  }
2252
- if (key.backspace || key.delete) {
2253
- const r = spliceBackspace(tzValueRef.current, tzCaretRef.current);
2254
- tzValueRef.current = r.value;
2255
- tzCaretRef.current = r.caret;
2256
- setTzEdit(r.value);
2257
- setTzCaret(r.caret);
2258
- setTzError(null);
2300
+ if (input === " ") {
2301
+ updateConfig((current) => ({ ...current, activeAccountId: row.id }));
2259
2302
  return;
2260
2303
  }
2261
- if (isPrintable(input, key)) {
2262
- const clean = sanitizeTyped(input);
2263
- if (clean) insertText(clean);
2264
- }
2265
2304
  return;
2266
2305
  }
2267
- if (tab === 1 && searchMode) {
2268
- if (key.return || key.escape) {
2269
- setSearchMode(false);
2270
- if (key.escape) {
2271
- setSearch("");
2272
- setSearchCaret(0);
2273
- }
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 }));
2274
2312
  return;
2275
2313
  }
2276
- if (key.leftArrow) {
2277
- setSearchCaret((c) => clampCaret(c - 1, search.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 }));
2278
2317
  return;
2279
2318
  }
2280
- if (key.rightArrow) {
2281
- setSearchCaret((c) => clampCaret(c + 1, search.length));
2319
+ if (index === 2 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2320
+ updateConfig((current) => ({ ...current, clearScreen: !current.clearScreen }));
2282
2321
  return;
2283
2322
  }
2284
- if (key.ctrl && input === "a") {
2285
- setSearchCaret(0);
2323
+ if (index === 3 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2324
+ updateConfig((current) => ({ ...current, privacyMode: !current.privacyMode }));
2286
2325
  return;
2287
2326
  }
2288
- if (key.ctrl && input === "e") {
2289
- setSearchCaret(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" }));
2290
2333
  return;
2291
2334
  }
2292
- if (key.backspace || key.delete) {
2293
- const r = spliceBackspace(searchValueRef.current, searchCaretRef.current);
2294
- searchValueRef.current = r.value;
2295
- searchCaretRef.current = r.caret;
2296
- setSearch(r.value);
2297
- setSearchCaret(r.caret);
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
+ }
2298
2345
  return;
2299
2346
  }
2300
- if (isPrintable(input, key)) {
2301
- const clean = sanitizeTyped(input);
2302
- if (clean) insertText(clean);
2347
+ if (index === 6 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2348
+ updateConfig((current) => ({ ...current, dashboardLayout: current.dashboardLayout === "grid" ? "single" : "grid" }));
2349
+ return;
2350
+ }
2351
+ if (index === 7 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2352
+ updateConfig((current) => ({ ...current, defaultFocus: current.defaultFocus === "all" ? "last" : "all" }));
2303
2353
  }
2304
- return;
2305
2354
  }
2355
+ }
2356
+
2357
+ // src/ui/keybindings-navigation.ts
2358
+ function handleGlobalCommand(input, ctx) {
2359
+ const { global, table, settings } = ctx;
2306
2360
  if (input === "q") {
2307
- exit();
2308
- return;
2361
+ global.exit();
2362
+ return true;
2309
2363
  }
2310
2364
  if (input === "O") {
2311
2365
  openUrl(REPO_URL);
2366
+ return true;
2367
+ }
2368
+ if (input === "W" || input === "w" && table.tab !== 1 && !settings.show) {
2369
+ if (!global.showLoader && global.configReady) void global.toggleWeb();
2370
+ return true;
2371
+ }
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 }));
2312
2379
  return;
2313
2380
  }
2314
- if (input === "W" || input === "w" && tab !== 1 && !showSettings) {
2315
- if (showLoader || !configReady) return;
2316
- void toggleWeb();
2381
+ if (input === "s") {
2382
+ settings.setTab("general");
2383
+ settings.setShow(true);
2384
+ settings.setCursor(-1);
2317
2385
  return;
2318
2386
  }
2319
- if (showSettings) {
2320
- if (key.escape || input === "s") {
2321
- setShowSettings(false);
2322
- return;
2323
- }
2324
- const switchSettingsTab = (dir) => {
2325
- const idx = SETTINGS_TABS.indexOf(settingsTab);
2326
- setSettingsTab(SETTINGS_TABS[(idx + dir + SETTINGS_TABS.length) % SETTINGS_TABS.length]);
2327
- setSettingsCursor(-1);
2328
- setTzEdit(null);
2329
- setTzError(null);
2330
- };
2331
- const rowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
2332
- if (key.tab) {
2333
- switchSettingsTab(key.shift ? -1 : 1);
2334
- return;
2387
+ if (input === "a") {
2388
+ global.cycleAccount(1);
2389
+ return;
2390
+ }
2391
+ if (input === "A") {
2392
+ global.cycleAccount(-1);
2393
+ return;
2394
+ }
2395
+ if (key.tab) {
2396
+ global.setTab((current) => (current + 1) % TABS.length);
2397
+ resetView();
2398
+ return;
2399
+ }
2400
+ if (input && /^[0-9]$/.test(input) && slots.length > 1) {
2401
+ const target = slots[parseInt(input, 10)];
2402
+ if (target) {
2403
+ updateConfig((current) => ({ ...current, activeAccountId: target.id }));
2404
+ resetView();
2335
2405
  }
2336
- if (input === "[") {
2337
- switchSettingsTab(-1);
2338
- return;
2339
- }
2340
- if (input === "]") {
2341
- switchSettingsTab(1);
2342
- return;
2343
- }
2344
- if (settingsCursor < 0) {
2345
- if (key.leftArrow) {
2346
- switchSettingsTab(-1);
2347
- return;
2348
- }
2349
- if (key.rightArrow) {
2350
- switchSettingsTab(1);
2351
- return;
2352
- }
2353
- if (key.downArrow || key.return) {
2354
- setSettingsCursor(0);
2355
- return;
2356
- }
2357
- if (key.upArrow) {
2358
- setSettingsCursor(Math.max(0, rowCount - 1));
2359
- return;
2360
- }
2361
- return;
2362
- }
2363
- const selectedAccountRow = settingsTab === "accounts" && settingsCursor >= 0 && settingsCursor < trackedAccountRows.length ? trackedAccountRows[settingsCursor] : null;
2364
- if (selectedAccountRow?.source === "configured" && selectedAccountRow.explicitIndex !== void 0 && key.shift && (key.upArrow || key.downArrow)) {
2365
- moveAccount(selectedAccountRow.explicitIndex, key.upArrow ? -1 : 1);
2366
- return;
2367
- }
2368
- if (key.upArrow) {
2369
- setSettingsCursor((c) => Math.max(-1, c - 1));
2370
- return;
2371
- }
2372
- if (key.downArrow) {
2373
- setSettingsCursor((c) => Math.min(rowCount - 1, c + 1));
2374
- return;
2375
- }
2376
- if (settingsTab === "general") {
2377
- if (settingsCursor === 0) {
2378
- if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
2379
- if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
2380
- return;
2381
- }
2382
- if (settingsCursor === 1) {
2383
- if (key.leftArrow) updateConfig((c) => ({ ...c, billingInterval: Math.max(1, c.billingInterval - 1) }));
2384
- if (key.rightArrow) updateConfig((c) => ({ ...c, billingInterval: c.billingInterval + 1 }));
2385
- return;
2386
- }
2387
- if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
2388
- updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
2389
- return;
2390
- }
2391
- if (settingsCursor === 3 && (key.leftArrow || key.rightArrow || key.return)) {
2392
- updateConfig((c) => ({ ...c, privacyMode: !c.privacyMode }));
2393
- return;
2394
- }
2395
- if (settingsCursor === 4) {
2396
- if (isPrintable(input, key)) {
2397
- const clean = sanitizeTyped(input);
2398
- if (clean.length === 1) updateConfig((c) => ({ ...c, privacyToggleKey: clean }));
2399
- }
2400
- if (key.backspace || key.delete) updateConfig((c) => ({ ...c, privacyToggleKey: "p" }));
2401
- return;
2402
- }
2403
- if (settingsCursor === 5) {
2404
- if (key.return) {
2405
- const init = cfg.timezone ?? "";
2406
- setTzEdit(init);
2407
- setTzCaret(init.length);
2408
- setTzError(null);
2409
- }
2410
- if (key.leftArrow || key.rightArrow) updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
2411
- return;
2412
- }
2413
- if (settingsCursor === 6 && (key.leftArrow || key.rightArrow || key.return)) {
2414
- updateConfig((c) => ({ ...c, dashboardLayout: c.dashboardLayout === "grid" ? "single" : "grid" }));
2415
- return;
2416
- }
2417
- if (settingsCursor === 7 && (key.leftArrow || key.rightArrow || key.return)) {
2418
- updateConfig((c) => ({ ...c, defaultFocus: c.defaultFocus === "all" ? "last" : "all" }));
2419
- return;
2420
- }
2421
- return;
2422
- }
2423
- if (settingsTab === "providers") {
2424
- if (settingsCursor >= 0 && settingsCursor < PROVIDER_ORDER.length && (input === " " || key.return || key.leftArrow || key.rightArrow)) {
2425
- toggleProvider(PROVIDER_ORDER[settingsCursor]);
2426
- }
2427
- return;
2428
- }
2429
- if (settingsTab === "accounts" && settingsCursor >= 0 && settingsCursor < trackedAccountRows.length) {
2430
- const row = trackedAccountRows[settingsCursor];
2431
- if (key.return) {
2432
- if (row.source === "configured") {
2433
- const acc = cfg.accounts.find((a) => a.id === row.explicitId);
2434
- if (acc) openEditAccount(acc);
2435
- } else {
2436
- openConfigureAccount(row);
2437
- }
2438
- return;
2439
- }
2440
- if (row.source === "configured" && row.explicitId && (input === "d" || input === "x")) {
2441
- deleteAccount(row.explicitId);
2406
+ return;
2407
+ }
2408
+ if (table.tab === 0) {
2409
+ if (dashboard.paginated) {
2410
+ if (input === "]" || key.downArrow || key.pageDown) {
2411
+ dashboard.setPage((page) => Math.min(dashboard.pageCount - 1, page + 1));
2442
2412
  return;
2443
2413
  }
2444
- if (input === " ") {
2445
- updateConfig((c) => ({ ...c, activeAccountId: row.id }));
2414
+ if (input === "[" || key.upArrow || key.pageUp) {
2415
+ dashboard.setPage((page) => Math.max(0, page - 1));
2446
2416
  return;
2447
2417
  }
2448
- return;
2449
2418
  }
2450
- if (settingsTab === "accounts" && settingsCursor === trackedAccountRows.length && key.return) {
2451
- openAddAccount();
2419
+ if (key.upArrow || key.downArrow || key.pageUp || key.pageDown || input === "G" || input === "g") return;
2420
+ }
2421
+ if (table.tab !== 1) {
2422
+ if (key.leftArrow || key.rightArrow) {
2423
+ global.setTab((current) => (current + 1) % TABS.length);
2424
+ resetView();
2452
2425
  }
2453
2426
  return;
2454
2427
  }
2455
- if (input === cfg.privacyToggleKey) {
2456
- updateConfig((c) => ({ ...c, privacyMode: !c.privacyMode }));
2428
+ if (input === "p") {
2429
+ table.cycleProvider(1);
2457
2430
  return;
2458
2431
  }
2459
- if (input === "s") {
2460
- setSettingsTab("general");
2461
- setShowSettings(true);
2462
- setSettingsCursor(-1);
2432
+ if (input === "P") {
2433
+ table.cycleProvider(-1);
2463
2434
  return;
2464
2435
  }
2465
- if (input === "a") {
2466
- cycleAccount(1);
2436
+ if (input === "/") {
2437
+ table.setSearchMode(true);
2438
+ table.setSearchCaret(table.search.length);
2467
2439
  return;
2468
2440
  }
2469
- if (input === "A") {
2470
- cycleAccount(-1);
2441
+ if (key.escape) {
2442
+ if (table.search) {
2443
+ table.setSearch("");
2444
+ table.setSearchCaret(0);
2445
+ } else table.setExpanded(-1);
2471
2446
  return;
2472
2447
  }
2473
- if (key.tab) {
2474
- setTab((t) => (t + 1) % TABS.length);
2448
+ if (input === "o") {
2449
+ table.setSort((sort) => (sort + 1) % table.sorts.length);
2475
2450
  resetView();
2476
2451
  return;
2477
2452
  }
2478
- if (input && /^[0-9]$/.test(input) && slots.length > 1) {
2479
- const target = slots[parseInt(input, 10)];
2480
- if (target) {
2481
- updateConfig((c) => ({ ...c, activeAccountId: target.id }));
2482
- resetView();
2483
- }
2453
+ if (input === "d") {
2454
+ table.setView(0);
2455
+ resetView();
2484
2456
  return;
2485
2457
  }
2486
- if (tab === 0) {
2487
- if (dashPaginated) {
2488
- if (input === "]" || key.downArrow || key.pageDown) {
2489
- setDashPage((p) => Math.min(dashPageCount - 1, p + 1));
2490
- return;
2491
- }
2492
- if (input === "[" || key.upArrow || key.pageUp) {
2493
- setDashPage((p) => Math.max(0, p - 1));
2494
- return;
2495
- }
2496
- }
2497
- if (key.upArrow || key.downArrow || key.pageUp || key.pageDown || input === "G" || input === "g") return;
2458
+ if (input === "w") {
2459
+ table.setView(1);
2460
+ resetView();
2461
+ return;
2498
2462
  }
2499
- if (tab === 1) {
2500
- if (input === "p") {
2501
- cycleTableProvider(1);
2502
- return;
2503
- }
2504
- if (input === "P") {
2505
- cycleTableProvider(-1);
2506
- return;
2507
- }
2508
- if (input === "/") {
2509
- setSearchMode(true);
2510
- setSearchCaret(search.length);
2511
- return;
2512
- }
2513
- if (key.escape) {
2514
- if (search) {
2515
- setSearch("");
2516
- setSearchCaret(0);
2517
- } else setExpanded(-1);
2518
- return;
2519
- }
2520
- if (input === "o") {
2521
- setSort((s) => (s + 1) % SORTS_FOR.length);
2522
- resetView();
2523
- return;
2524
- }
2525
- if (input === "d") {
2526
- setView(0);
2527
- resetView();
2528
- return;
2529
- }
2530
- if (input === "w") {
2531
- setView(1);
2532
- resetView();
2533
- return;
2534
- }
2535
- if (input === "m") {
2536
- cycleTableModel2(1);
2537
- return;
2538
- }
2539
- if (input === "M") {
2540
- cycleTableModel2(-1);
2541
- return;
2542
- }
2543
- if (key.leftArrow) {
2544
- setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
2545
- resetView();
2546
- return;
2547
- }
2548
- if (key.rightArrow) {
2549
- setView((v) => (v + 1) % VIEWS.length);
2550
- resetView();
2551
- return;
2552
- }
2553
- if (key.return) {
2554
- setExpanded((e) => e === cursor ? -1 : cursor);
2555
- return;
2556
- }
2557
- if (key.upArrow) {
2558
- setCursor((c) => Math.max(0, c - 1));
2559
- return;
2560
- }
2561
- if (key.downArrow) {
2562
- setCursor((c) => clampRow(c + 1));
2563
- return;
2564
- }
2565
- if (key.pageDown || input === "G") {
2566
- setCursor((c) => clampRow(input === "G" ? rowCountRef.current - 1 : c + Math.max(1, rows - 12)));
2567
- return;
2568
- }
2569
- if (key.pageUp || input === "g") {
2570
- setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
2571
- return;
2572
- }
2573
- } else {
2574
- if (key.leftArrow || key.rightArrow) {
2575
- setTab((t) => (t + 1) % TABS.length);
2576
- resetView();
2577
- return;
2578
- }
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)));
2499
+ }
2500
+ }
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;
2579
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);
2580
2566
  }
2581
2567
 
2582
2568
  // src/ui/hooks/use-terminal-size.ts
@@ -2732,7 +2718,7 @@ function useLoader({ configReady, showPicker, accountsKey, allReady, tooSmall, s
2732
2718
  }
2733
2719
 
2734
2720
  // src/ui/hooks/use-degraded-polling.ts
2735
- 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";
2736
2722
 
2737
2723
  // src/client/seed-cache.ts
2738
2724
  import { readFile } from "fs/promises";
@@ -2751,92 +2737,420 @@ async function loadSeedSnapshot() {
2751
2737
  }
2752
2738
 
2753
2739
  // src/ui/hooks/use-degraded-polling.ts
2754
- 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
+ }) {
2755
2760
  const [statsLocal, setStats] = useState6(/* @__PURE__ */ new Map());
2756
2761
  const [peakLocal, setPeak] = useState6(null);
2757
- const [error, setError] = useState6(null);
2758
2762
  const [updatedLocal, setUpdated] = useState6(/* @__PURE__ */ new Date());
2763
+ const [tables, setTables] = useState6(/* @__PURE__ */ new Map());
2764
+ const [loadingTables, setLoadingTables] = useState6(/* @__PURE__ */ new Set());
2759
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]);
2760
2786
  useEffect6(() => {
2761
2787
  if (!degraded) return;
2762
2788
  if (seededRef.current || !configReady || showPicker || accountsRef.current.length === 0) return;
2763
2789
  seededRef.current = true;
2764
- loadSeedSnapshot().then((snap) => {
2790
+ const epoch = epochRef.current;
2791
+ void loadSeedSnapshot().then((snap) => {
2792
+ if (epoch !== epochRef.current) return;
2765
2793
  setStats((prev) => {
2766
2794
  if (prev.size > 0) return prev;
2767
2795
  const next = new Map(prev);
2768
2796
  for (const acc of accountsRef.current) {
2769
- const s = snap[acc.id];
2770
- 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
+ }
2771
2805
  }
2772
2806
  return next;
2773
2807
  });
2774
2808
  });
2775
- }, [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]);
2776
2920
  useEffect6(() => {
2777
2921
  if (!degraded || !configReady || showPicker) return;
2778
2922
  let active = true;
2779
2923
  let timer;
2780
- const load = async () => {
2781
- try {
2782
- await Promise.all(accountsRef.current.map(async (acc) => {
2783
- const provider = PROVIDERS[acc.providerId];
2784
- if (!provider.hasUsage || !provider.fetchSummary) return;
2785
- try {
2786
- const dashboard = await withTimeout(provider.fetchSummary(acc, tz));
2787
- if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
2788
- } catch {
2789
- }
2790
- }));
2791
- if (active) {
2792
- setError(null);
2793
- setUpdated(/* @__PURE__ */ new Date());
2794
- }
2795
- } finally {
2796
- if (active) timer = setTimeout(load, interval);
2797
- }
2924
+ const loop = async () => {
2925
+ await runSummary().catch(() => {
2926
+ });
2927
+ if (active) timer = setTimeout(loop, interval);
2798
2928
  };
2799
- load();
2929
+ void loop();
2800
2930
  return () => {
2801
2931
  active = false;
2802
- clearTimeout(timer);
2932
+ if (timer) clearTimeout(timer);
2803
2933
  };
2804
- }, [degraded, interval, tz, configReady, showPicker, accountsKey]);
2934
+ }, [degraded, configReady, showPicker, interval, runSummary]);
2805
2935
  useEffect6(() => {
2806
2936
  if (!degraded || !configReady || showPicker) return;
2807
2937
  let active = true;
2808
2938
  let timer;
2809
- const load = async () => {
2810
- try {
2811
- const peakP = accountsRef.current.some((a) => a.providerId === "claude") ? withTimeout(fetchPeak()).catch(() => null) : Promise.resolve(null);
2812
- await Promise.all(accountsRef.current.map(async (acc) => {
2813
- const provider = PROVIDERS[acc.providerId];
2814
- if (!provider.hasBilling || !provider.fetchBilling) return;
2815
- try {
2816
- const billing = await withTimeout(provider.fetchBilling(acc, tz));
2817
- if (active) setStats((prev) => upsert(prev, acc, { billing }));
2818
- } catch {
2819
- }
2820
- }));
2821
- const p = await peakP;
2822
- if (active && p) setPeak(p);
2823
- } finally {
2824
- if (active) timer = setTimeout(load, billingMs);
2825
- }
2939
+ const loop = async () => {
2940
+ await Promise.allSettled([runBilling(), runPeak()]);
2941
+ if (active) timer = setTimeout(loop, billingMs);
2942
+ };
2943
+ void loop();
2944
+ return () => {
2945
+ active = false;
2946
+ if (timer) clearTimeout(timer);
2947
+ };
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));
2826
2957
  };
2827
- load();
2958
+ void loop();
2828
2959
  return () => {
2829
2960
  active = false;
2830
- clearTimeout(timer);
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);
2831
3014
  };
2832
- }, [degraded, billingMs, tz, configReady, showPicker, accountsKey]);
2833
- return { statsLocal, peakLocal, updatedLocal, error };
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 };
2834
3148
  }
2835
3149
 
2836
3150
  // src/ui/hooks/use-account-form.ts
2837
- import { useState as useState7, useCallback as useCallback3 } from "react";
3151
+ import { useState as useState9, useCallback as useCallback6 } from "react";
2838
3152
  function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSettingsCursor }) {
2839
- const [accountForm, setAccountForm] = useState7(null);
3153
+ const [accountForm, setAccountForm] = useState9(null);
2840
3154
  function openAddAccount(defaults) {
2841
3155
  const providerId = defaults?.providerId ?? (detected[0] ?? "claude");
2842
3156
  setAccountForm({
@@ -2888,7 +3202,7 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
2888
3202
  });
2889
3203
  setAccountForm(null);
2890
3204
  }
2891
- const cycleFormField = useCallback3((dir) => {
3205
+ const cycleFormField = useCallback6((dir) => {
2892
3206
  setAccountForm((f) => {
2893
3207
  if (!f) return f;
2894
3208
  const i = FORM_FIELDS.indexOf(f.field);
@@ -2897,14 +3211,14 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
2897
3211
  return { ...f, field: next, caret };
2898
3212
  });
2899
3213
  }, []);
2900
- const cycleProvider = useCallback3((dir) => {
3214
+ const cycleProvider = useCallback6((dir) => {
2901
3215
  setAccountForm((f) => {
2902
3216
  if (!f) return f;
2903
3217
  const i = PROVIDER_ORDER.indexOf(f.providerId);
2904
3218
  return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
2905
3219
  });
2906
3220
  }, []);
2907
- const cycleColor = useCallback3((dir) => {
3221
+ const cycleColor = useCallback6((dir) => {
2908
3222
  setAccountForm((f) => {
2909
3223
  if (!f) return f;
2910
3224
  const i = COLOR_PALETTE.indexOf(f.color);
@@ -2945,39 +3259,42 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
2945
3259
  }
2946
3260
 
2947
3261
  // src/app.tsx
2948
- 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";
2949
3263
  function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = null, mode = "degraded" }) {
2950
3264
  const connected = mode === "connected" && baseUrl !== null && wsToken !== null;
2951
3265
  const degraded = !connected;
2952
3266
  const daemon = useDaemon(connected ? baseUrl : null, connected ? wsToken : null);
2953
- const [config, setConfig] = useState8(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
2954
- const [detected, setDetected] = useState8([]);
2955
- const [tableLocal, setTable] = useState8(null);
2956
- const [tableLoading, setTableLoading] = useState8(false);
2957
- const [tab, setTab] = useState8(0);
2958
- const [view, setView] = useState8(0);
2959
- const [cursor, setCursor] = useState8(0);
2960
- const [expanded, setExpanded] = useState8(-1);
2961
- const [sort, setSort] = useState8(1);
2962
- const [tableProvider, setTableProvider] = useState8(null);
2963
- const [tableModel, setTableModel] = useState8(null);
2964
- const [search, setSearch] = useState8("");
2965
- const [searchMode, setSearchMode] = useState8(false);
2966
- const [showSettings, setShowSettings] = useState8(false);
2967
- const [settingsTab, setSettingsTab] = useState8("general");
2968
- const [settingsCursor, setSettingsCursor] = useState8(0);
2969
- const [tzEdit, setTzEdit] = useState8(null);
2970
- const [tzError, setTzError] = useState8(null);
2971
- const [tzCaret, setTzCaret] = useState8(0);
2972
- const [searchCaret, setSearchCaret] = useState8(0);
2973
- const [onboardSel, setOnboardSel] = useState8(null);
2974
- const [onboardCursor, setOnboardCursor] = useState8(0);
2975
- 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);
2976
3293
  const { exit } = useApp();
2977
3294
  const { cols, rows, resizing, live } = useTerminalSize();
2978
- const webRef = useRef6(null);
2979
- const webStartingRef = useRef6(false);
2980
- useEffect7(() => () => {
3295
+ const webRef = useRef8(null);
3296
+ const webStartingRef = useRef8(false);
3297
+ useEffect9(() => () => {
2981
3298
  void webRef.current?.stop();
2982
3299
  }, []);
2983
3300
  const cfg = config ?? DEFAULTS;
@@ -2988,22 +3305,38 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
2988
3305
  const accounts = useMemo2(() => buildAccounts(cfg, detected), [cfg, detected]);
2989
3306
  const trackedAccountRows = useMemo2(() => getTrackedAccountRows(cfg, detected, accounts), [cfg, detected, accounts]);
2990
3307
  const settingsRowCount = settingsTab === "general" ? GENERAL_ROWS : settingsTab === "providers" ? PROVIDER_ORDER.length : trackedAccountRows.length + 1;
2991
- const accountsRef = useRef6([]);
3308
+ const accountsRef = useRef8([]);
2992
3309
  accountsRef.current = accounts;
2993
- const rowCountRef = useRef6(0);
2994
- const tabRef = useRef6(0);
3310
+ const rowCountRef = useRef8(0);
3311
+ const tabRef = useRef8(0);
2995
3312
  tabRef.current = tab;
2996
- const dashPageCountRef = useRef6(1);
2997
- const pendingLocalConfigRef = useRef6(null);
2998
- const tzValueRef = useRef6("");
2999
- const tzCaretRef = useRef6(0);
3000
- const searchValueRef = useRef6("");
3001
- 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);
3002
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
+ );
3003
3329
  const needsOnboarding = configReady && !cfg.onboarded;
3004
3330
  const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
3005
3331
  const showPicker = needsOnboarding || newProviders.length > 0;
3006
- const { statsLocal, peakLocal, updatedLocal, error } = useDegradedPolling({
3332
+ const {
3333
+ statsLocal,
3334
+ peakLocal,
3335
+ updatedLocal,
3336
+ tableLocal,
3337
+ tableLoading,
3338
+ refreshAll: refreshAllDegraded
3339
+ } = useDegradedPolling({
3007
3340
  degraded,
3008
3341
  configReady,
3009
3342
  showPicker,
@@ -3011,9 +3344,22 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3011
3344
  accountsRef,
3012
3345
  interval,
3013
3346
  billingMs,
3014
- tz
3347
+ tz,
3348
+ activeTableProvider: effTableProvider,
3349
+ tableVisible: tab === 1
3015
3350
  });
3016
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
+ });
3017
3363
  const stats = useMemo2(
3018
3364
  () => connected ? toStatsMap(snapshot, accounts) : statsLocal,
3019
3365
  [connected, snapshot, accounts, statsLocal]
@@ -3038,7 +3384,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3038
3384
  [connected, snapshot, updatedLocal]
3039
3385
  );
3040
3386
  const intervalLabel = connected && snapshot?.intervalMs ? Math.round(snapshot.intervalMs / 1e3) : cfg.interval;
3041
- const readyInputFor = useCallback4((id) => {
3387
+ const readyInputFor = useCallback7((id) => {
3042
3388
  if (connected) {
3043
3389
  const wa = snapshot?.accounts.find((a) => a.id === id);
3044
3390
  if (!wa) return void 0;
@@ -3055,15 +3401,10 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3055
3401
  () => focusId === null ? accounts : accounts.filter((a) => a.id === focusId),
3056
3402
  [accounts, focusId]
3057
3403
  );
3058
- const allGroups = useMemo2(() => accountsByProvider(accounts), [accounts]);
3059
3404
  const groups = useMemo2(
3060
3405
  () => focusId === null ? allGroups : accountsByProvider(visibleAccounts),
3061
3406
  [allGroups, visibleAccounts, focusId]
3062
3407
  );
3063
- const tableProvs = useMemo2(
3064
- () => allGroups.map((g) => g.provider).filter((pid) => PROVIDERS[pid].hasUsage),
3065
- [allGroups]
3066
- );
3067
3408
  const TOO_SMALL = cols < 40 || rows < 12;
3068
3409
  const allReady = accounts.length > 0 && accounts.every((a) => accountReady(readyInputFor(a.id), a.providerId));
3069
3410
  const { gridBudget } = useMemo2(() => computeChrome(slots, cols, rows), [slots, cols, rows]);
@@ -3102,11 +3443,6 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3102
3443
  }
3103
3444
  };
3104
3445
  const { handlePasteData, isPasteInput } = usePaste(insertText);
3105
- const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
3106
- const tableAccounts = useMemo2(
3107
- () => effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [],
3108
- [accounts, effTableProvider]
3109
- );
3110
3446
  const SORTS_FOR = SORTS;
3111
3447
  const tableAccountIds = useMemo2(() => tableAccounts.map((a) => a.id), [tableAccounts]);
3112
3448
  const table = useMemo2(
@@ -3131,67 +3467,36 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3131
3467
  detected: detected.includes(pid),
3132
3468
  enabled: onboardEnabled.includes(pid)
3133
3469
  }));
3134
- useEffect7(() => {
3135
- if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
3136
- detectProviders().then(setDetected);
3470
+ useEffect9(() => {
3471
+ void detectProviders().then(setDetected);
3137
3472
  }, []);
3138
3473
  const tableKey = useMemo2(
3139
3474
  () => `${effTableProvider}|${tableAccounts.map(acctKey).join(",")}|${tz}`,
3140
3475
  [effTableProvider, tableAccounts, tz]
3141
3476
  );
3142
- useEffect7(() => {
3143
- setTable(null);
3477
+ useEffect9(() => {
3144
3478
  setCursor(0);
3145
3479
  setExpanded(-1);
3146
3480
  setTableModel(null);
3147
3481
  setSort(1);
3148
- setTableLoading(false);
3149
3482
  }, [tableKey]);
3150
- useEffect7(() => {
3151
- if (!degraded || tab !== 1 || !effTableProvider) return;
3152
- let active = true;
3153
- let timer;
3154
- const fetchOnce = async () => {
3155
- try {
3156
- const r = await withTimeout(fetchScopeTable(tableAccounts, tz));
3157
- if (active) setTable(r);
3158
- } catch {
3159
- }
3160
- };
3161
- const run = async () => {
3162
- setTableLoading(true);
3163
- await fetchOnce();
3164
- if (!active) return;
3165
- setTableLoading(false);
3166
- const loop = async () => {
3167
- await fetchOnce();
3168
- if (active) timer = setTimeout(loop, Math.max(interval, 1e4));
3169
- };
3170
- timer = setTimeout(loop, Math.max(interval, 1e4));
3171
- };
3172
- run();
3173
- return () => {
3174
- active = false;
3175
- clearTimeout(timer);
3176
- };
3177
- }, [degraded, tab, tableKey, interval]);
3178
- useEffect7(() => {
3483
+ useEffect9(() => {
3179
3484
  setCursor(0);
3180
3485
  setExpanded(-1);
3181
3486
  }, [search, tableModel]);
3182
- useEffect7(() => {
3487
+ useEffect9(() => {
3183
3488
  setDashPage((p) => Math.min(p, dashPageCount - 1));
3184
3489
  }, [dashPageCount]);
3185
- useEffect7(() => {
3490
+ useEffect9(() => {
3186
3491
  setSettingsCursor((c) => Math.max(-1, Math.min(c, settingsRowCount - 1)));
3187
3492
  }, [settingsRowCount]);
3188
- const resetView = useCallback4(() => {
3493
+ const resetView = useCallback7(() => {
3189
3494
  setCursor(0);
3190
3495
  setExpanded(-1);
3191
3496
  }, []);
3192
3497
  const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
3193
3498
  const mouse = useMouse();
3194
- useEffect7(() => {
3499
+ useEffect9(() => {
3195
3500
  if (!IS_TTY) return;
3196
3501
  mouse.enable();
3197
3502
  if (process.stdout.isTTY) {
@@ -3219,31 +3524,6 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3219
3524
  process.stdin.off("data", onData);
3220
3525
  };
3221
3526
  }, []);
3222
- const updateConfig = useCallback4((fn) => {
3223
- setConfig((prev) => {
3224
- const next = normalizeConfig(fn(prev ?? DEFAULTS));
3225
- pendingLocalConfigRef.current = connected ? next : null;
3226
- saveConfigSync(next);
3227
- if (connected) {
3228
- void daemon.setConfig(next).then((saved) => {
3229
- if (pendingLocalConfigRef.current && reconcileDaemonConfig(next, saved, pendingLocalConfigRef.current).pendingLocalConfig === null) {
3230
- pendingLocalConfigRef.current = null;
3231
- }
3232
- }).catch(() => {
3233
- });
3234
- }
3235
- return next;
3236
- });
3237
- }, [connected, daemon]);
3238
- const daemonConfig = daemon.config;
3239
- useEffect7(() => {
3240
- if (!connected || !daemonConfig) return;
3241
- setConfig((prev) => {
3242
- const reconciled = reconcileDaemonConfig(prev, daemonConfig, pendingLocalConfigRef.current);
3243
- pendingLocalConfigRef.current = reconciled.pendingLocalConfig;
3244
- return reconciled.config;
3245
- });
3246
- }, [connected, daemonConfig]);
3247
3527
  function toggleOnboard(i) {
3248
3528
  if (i < 0 || i >= pickerProviders.length) return;
3249
3529
  const pid = pickerProviders[i];
@@ -3280,13 +3560,13 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3280
3560
  setOnboardSel(null);
3281
3561
  setOnboardCursor(0);
3282
3562
  }
3283
- const cycleAccount = useCallback4((dir) => {
3563
+ const cycleAccount = useCallback7((dir) => {
3284
3564
  if (slots.length <= 1) return;
3285
3565
  const next = (activeSlotIdx + dir + slots.length) % slots.length;
3286
3566
  updateConfig((c) => ({ ...c, activeAccountId: slots[next].id }));
3287
3567
  resetView();
3288
3568
  }, [slots, activeSlotIdx, updateConfig, resetView]);
3289
- const cycleTableProvider = useCallback4((dir) => {
3569
+ const cycleTableProvider = useCallback7((dir) => {
3290
3570
  if (tableProvs.length <= 1) return;
3291
3571
  const cur = effTableProvider ? tableProvs.indexOf(effTableProvider) : 0;
3292
3572
  const nextProv = tableProvs[(cur + dir + tableProvs.length) % tableProvs.length];
@@ -3318,30 +3598,30 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3318
3598
  return;
3319
3599
  }
3320
3600
  if (webRef.current) {
3321
- openUrl(webRef.current.url);
3601
+ openUrl(webRef.current.browserUrl);
3322
3602
  return;
3323
3603
  }
3324
3604
  if (webStartingRef.current) return;
3325
3605
  webStartingRef.current = true;
3326
3606
  try {
3327
- const { startWebServer } = await import("./server-GA2FKYDQ.js");
3607
+ const { startWebServer } = await import("./server-4CDZMLH6.js");
3328
3608
  const ctrl = await startWebServer({ config: cfg, log: false });
3329
3609
  webRef.current = ctrl;
3330
- openUrl(ctrl.url);
3610
+ openUrl(ctrl.browserUrl);
3331
3611
  } catch {
3332
3612
  } finally {
3333
3613
  webStartingRef.current = false;
3334
3614
  }
3335
3615
  }
3336
- const onTabSelect = useCallback4((i) => {
3616
+ const onTabSelect = useCallback7((i) => {
3337
3617
  setTab(i);
3338
3618
  resetView();
3339
3619
  }, [resetView]);
3340
- const onStripSelect = useCallback4((i) => {
3620
+ const onStripSelect = useCallback7((i) => {
3341
3621
  updateConfig((c) => ({ ...c, activeAccountId: slots[i].id }));
3342
3622
  resetView();
3343
3623
  }, [slots, updateConfig, resetView]);
3344
- const onProviderSelect = useCallback4((p) => {
3624
+ const onProviderSelect = useCallback7((p) => {
3345
3625
  setTableProvider(p);
3346
3626
  setCursor(0);
3347
3627
  setExpanded(-1);
@@ -3350,7 +3630,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3350
3630
  setSearchCaret(0);
3351
3631
  setSearchMode(false);
3352
3632
  }, []);
3353
- const onRowClickToken = useCallback4((idx) => {
3633
+ const onRowClickToken = useCallback7((idx) => {
3354
3634
  if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
3355
3635
  else setCursor(idx);
3356
3636
  }, [cursor]);
@@ -3359,10 +3639,10 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3359
3639
  [tab, table, view]
3360
3640
  );
3361
3641
  const tokenModelOptions = useMemo2(() => tableModelOptions(rawTokenRows), [rawTokenRows]);
3362
- useEffect7(() => {
3642
+ useEffect9(() => {
3363
3643
  if (tableModel && !tokenModelOptions.includes(tableModel)) setTableModel(null);
3364
3644
  }, [tableModel, tokenModelOptions]);
3365
- const cycleTableModelFilter = useCallback4((dir) => {
3645
+ const cycleTableModelFilter = useCallback7((dir) => {
3366
3646
  setTableModel((cur) => cycleTableModel(cur, tokenModelOptions, dir));
3367
3647
  resetView();
3368
3648
  }, [tokenModelOptions, resetView]);
@@ -3372,77 +3652,86 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3372
3652
  [tab, rawTokenRows, activeTableModel, search, sort]
3373
3653
  );
3374
3654
  useInput((input, key) => handleKey(input, key, {
3375
- showPicker,
3376
- pickerProviders,
3377
- onboardCursor,
3378
- setOnboardCursor,
3379
- toggleOnboard,
3380
- confirmOnboarding,
3381
- exit,
3382
- showSettings,
3383
- accountForm,
3384
- setAccountForm,
3385
- commitAccountForm,
3386
- cycleFormField,
3387
- cycleProvider,
3388
- cycleColor,
3389
- isPrintable,
3390
- insertText,
3391
- tzEdit,
3392
- setTzEdit,
3393
- setTzError,
3394
- updateConfig,
3395
- setTzCaret,
3396
- tzValueRef,
3397
- tzCaretRef,
3398
- tab,
3399
- searchMode,
3400
- setSearchMode,
3401
- search,
3402
- setSearch,
3403
- setSearchCaret,
3404
- searchValueRef,
3405
- searchCaretRef,
3406
- showLoader,
3407
- configReady,
3408
- toggleWeb,
3409
- settingsCursor,
3410
- settingsTab,
3411
- setSettingsTab,
3412
- setShowSettings,
3413
- cfg,
3414
- trackedAccountRows,
3415
- moveAccount,
3416
- setSettingsCursor,
3417
- toggleProvider,
3418
- openEditAccount,
3419
- openConfigureAccount,
3420
- deleteAccount,
3421
- openAddAccount,
3422
- cycleAccount,
3423
- setTab,
3424
- resetView,
3425
- slots,
3426
- dashPaginated,
3427
- dashPageCount,
3428
- setDashPage,
3429
- cycleTableProvider,
3430
- setExpanded,
3431
- setSort,
3432
- SORTS_FOR,
3433
- cycleTableModel: cycleTableModelFilter,
3434
- setView,
3435
- cursor,
3436
- rowCountRef,
3437
- rows,
3438
- setCursor,
3439
- 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
+ }
3440
3730
  }), { isActive: IS_TTY });
3441
- if (error) return /* @__PURE__ */ jsx11(Box11, { padding: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: error }) });
3442
- if (!config) return /* @__PURE__ */ jsx11(Box11, { padding: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Loading..." }) });
3443
- 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 });
3444
3733
  if (showPicker) {
3445
- 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(
3446
3735
  Onboarding,
3447
3736
  {
3448
3737
  items: onboardItems,
@@ -3455,20 +3744,20 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3455
3744
  ) });
3456
3745
  }
3457
3746
  if (showLoader) {
3458
- 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 }) });
3459
3748
  }
3460
3749
  if (TOO_SMALL && !showSettings) {
3461
- return /* @__PURE__ */ jsx11(TinyFallback, { groups, stats, rows, cols });
3750
+ return /* @__PURE__ */ jsx12(TinyFallback, { groups, stats, rows, cols, refreshStatus });
3462
3751
  }
3463
3752
  rowCountRef.current = tokenRows.length;
3464
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
3465
- /* @__PURE__ */ jsxs11(Box11, { justifyContent: "space-between", children: [
3466
- /* @__PURE__ */ jsxs11(Box11, { children: [
3467
- /* @__PURE__ */ jsxs11(Text11, { bold: true, color: "greenBright", children: [
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: [
3468
3757
  glyphs().dotSel,
3469
3758
  " tokmon"
3470
3759
  ] }),
3471
- /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3760
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3472
3761
  " ",
3473
3762
  glyphs().middot,
3474
3763
  " every ",
@@ -3476,31 +3765,37 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3476
3765
  "s"
3477
3766
  ] })
3478
3767
  ] }),
3479
- /* @__PURE__ */ jsxs11(Box11, { children: [
3480
- peak && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3481
- /* @__PURE__ */ jsx11(PeakBadge, { peak }),
3482
- /* @__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: [
3483
3772
  " ",
3484
3773
  glyphs().middot,
3485
3774
  " "
3486
3775
  ] })
3487
3776
  ] }),
3488
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: time(updated, tz) })
3777
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: time(updated, tz) })
3489
3778
  ] })
3490
3779
  ] }),
3491
- degraded && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3780
+ degraded && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3492
3781
  glyphs().warn,
3493
3782
  " degraded ",
3494
3783
  glyphs().middot,
3495
3784
  " background service unavailable, running in-process"
3496
3785
  ] }),
3497
- connected && daemon.conn !== "live" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3786
+ connected && daemon.conn !== "live" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3498
3787
  glyphs().warn,
3499
3788
  " reconnecting ",
3500
3789
  glyphs().middot,
3501
3790
  " showing last known data"
3502
3791
  ] }),
3503
- 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(
3504
3799
  SettingsView,
3505
3800
  {
3506
3801
  config: cfg,
@@ -3515,20 +3810,20 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3515
3810
  trackedAccounts: trackedAccountRows,
3516
3811
  accountIdentities
3517
3812
  }
3518
- ) : /* @__PURE__ */ jsxs11(Fragment4, { children: [
3519
- /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, marginBottom: 1, children: [
3520
- /* @__PURE__ */ jsx11(TabBar, { tabs: TABS, active: tab, onSelect: onTabSelect }),
3521
- /* @__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: [
3522
3817
  " Tab/",
3523
3818
  glyphs().arrowL,
3524
3819
  glyphs().arrowR
3525
3820
  ] })
3526
3821
  ] }),
3527
- tab === 0 && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3528
- /* @__PURE__ */ jsx11(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode }),
3529
- slots.length > 1 && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
3530
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "focus " }),
3531
- /* @__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(
3532
3827
  AccountStrip,
3533
3828
  {
3534
3829
  slots,
@@ -3537,12 +3832,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3537
3832
  }
3538
3833
  )
3539
3834
  ] }),
3540
- /* @__PURE__ */ jsx11(TotalsRow, { groups, stats, cols })
3835
+ /* @__PURE__ */ jsx12(TotalsRow, { groups, stats, cols })
3541
3836
  ] }),
3542
- tab === 1 && /* @__PURE__ */ jsxs11(Fragment4, { children: [
3543
- tableProvs.length > 0 && /* @__PURE__ */ jsx11(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: onProviderSelect }),
3544
- /* @__PURE__ */ jsx11(Box11, { height: 1 }),
3545
- /* @__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(
3546
3841
  ControlBar,
3547
3842
  {
3548
3843
  views: VIEWS,
@@ -3556,12 +3851,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3556
3851
  showModel: true
3557
3852
  }
3558
3853
  ),
3559
- /* @__PURE__ */ jsx11(Box11, { height: 1 }),
3560
- !effTableProvider ? /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3854
+ /* @__PURE__ */ jsx12(Box11, { height: 1 }),
3855
+ !effTableProvider ? /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3561
3856
  "No providers enabled ",
3562
3857
  glyphs().emDash,
3563
3858
  " press s to pick providers."
3564
- ] }) : tableLoading && !table ? /* @__PURE__ */ jsx11(Spinner, { label: "Loading history" }) : /* @__PURE__ */ jsx11(
3859
+ ] }) : tableLoading && !table ? /* @__PURE__ */ jsx12(Spinner, { label: "Loading history" }) : /* @__PURE__ */ jsx12(
3565
3860
  TokenTable,
3566
3861
  {
3567
3862
  rows: tokenRows,
@@ -3574,12 +3869,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3574
3869
  )
3575
3870
  ] })
3576
3871
  ] }),
3577
- (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 })
3578
3873
  ] });
3579
3874
  }
3580
3875
 
3581
3876
  // src/bootstrap-ink.tsx
3582
- import { jsx as jsx12 } from "react/jsx-runtime";
3877
+ import { jsx as jsx13 } from "react/jsx-runtime";
3583
3878
  function enterAltScreen() {
3584
3879
  process.stdout.write("\x1B[?1049h\x1B[H");
3585
3880
  }
@@ -3608,7 +3903,7 @@ async function bootstrapInk({ interval, config, daemon, mode }) {
3608
3903
  if (altScreen) leaveAltScreen();
3609
3904
  });
3610
3905
  const { waitUntilExit } = render(
3611
- /* @__PURE__ */ jsx12(MouseProvider, { children: /* @__PURE__ */ jsx12(
3906
+ /* @__PURE__ */ jsx13(MouseProvider, { children: /* @__PURE__ */ jsx13(
3612
3907
  App,
3613
3908
  {
3614
3909
  interval,