tokmon 0.24.0 → 0.26.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 (36) hide show
  1. package/README.md +5 -3
  2. package/dist/{bootstrap-ink-HL2BYNW3.js → bootstrap-ink-5UFPNDV4.js} +53 -44
  3. package/dist/{chunk-45ZP755H.js → chunk-4CY725NY.js} +1 -1
  4. package/dist/{chunk-W2WGBXZG.js → chunk-AMD4PXDG.js} +1 -1
  5. package/dist/{chunk-5CWOJMAH.js → chunk-E2YXYU73.js} +2 -0
  6. package/dist/{chunk-REO4O5VE.js → chunk-OWPJUG4C.js} +33 -13
  7. package/dist/{chunk-3TJVFKXV.js → chunk-QM5E5RJZ.js} +1 -1
  8. package/dist/{chunk-O27I2XFN.js → chunk-RTGMDE7U.js} +39 -18
  9. package/dist/{chunk-42H7R376.js → chunk-VGYBQH5I.js} +4 -3
  10. package/dist/{cli-command-TLGIGF63.js → cli-command-U7RZ2GTD.js} +5 -5
  11. package/dist/cli.js +6 -6
  12. package/dist/{config-QC5QSP3G.js → config-2PXUENQL.js} +1 -1
  13. package/dist/{daemon-F4EGY77J.js → daemon-PQWKWRC6.js} +5 -5
  14. package/dist/{daemon-handle-NLCLYWFU.js → daemon-handle-2GVZT55B.js} +3 -3
  15. package/dist/server-5WIWS27N.js +11 -0
  16. package/dist/web/assets/{Area-DCES17aZ.js → Area-Tx1mUuqM.js} +1 -1
  17. package/dist/web/assets/{analytics-DDW6zcnj.js → analytics-CAzklhXc.js} +2 -2
  18. package/dist/web/assets/{breakdown-D5LPPtqh.js → breakdown-BoPnOv7Y.js} +1 -1
  19. package/dist/web/assets/{chart-bHY7MQV0.js → chart-jK7A9m7R.js} +1 -1
  20. package/dist/web/assets/{explore-DdWB-Pu7.js → explore-e8wtwk7s.js} +1 -1
  21. package/dist/web/assets/{index-C9jxb2o3.js → index-C8oe6EfM.js} +24 -24
  22. package/dist/web/assets/index-NnKaHxPO.css +1 -0
  23. package/dist/web/assets/{models-o1wJnG6w.js → models-_4IZNDxW.js} +2 -2
  24. package/dist/web/assets/overview-BBOfd_Fd.js +2 -0
  25. package/dist/web/assets/{panel-DBnK1oy0.js → panel-CstB4hPm.js} +1 -1
  26. package/dist/web/assets/{primitives-CBmteetD.js → primitives-G9ALG7ih.js} +1 -1
  27. package/dist/web/assets/settings-sheet-rq6vbYXM.js +1 -0
  28. package/dist/web/assets/{share-sheet-DUee3FlK.js → share-sheet-BwmnebRB.js} +2 -2
  29. package/dist/web/assets/{timeline-BxHwOJA_.js → timeline-DkYWH6XW.js} +1 -1
  30. package/dist/web/assets/{use-dialog-trap-30EqxYF-.js → use-dialog-trap-Jci2xaMX.js} +1 -1
  31. package/dist/web/index.html +2 -2
  32. package/package.json +1 -1
  33. package/dist/server-X63FGHIZ.js +0 -11
  34. package/dist/web/assets/index-m0KfWnN2.css +0 -1
  35. package/dist/web/assets/overview-wq2uAEk4.js +0 -2
  36. package/dist/web/assets/settings-sheet-CZ6GEPl3.js +0 -1
package/README.md CHANGED
@@ -76,7 +76,7 @@ A responsive grid of provider cards (or one card at a time — see **Dashboard l
76
76
  - **Today / This Week / This Month** — cost and token summaries
77
77
  - **Burn rate** — current $/hr
78
78
  - **Cache saved** — what caching has saved you
79
- - **Rate limits** — live utilization bars with reset countdowns
79
+ - **Rate limits** — live utilization bars with reset countdowns or exact reset dates
80
80
  - **Sparkline** — recent daily activity
81
81
 
82
82
  The grid reflows to fit your terminal — more columns when it's wide, compacting cards when it's short. With more providers than fit on screen, it splits into **pages**; **scroll** (mouse wheel) to move between them (or `↑`/`↓` / `[` `]`). When you track more than one account, a focus strip lets you view **All** together or zoom into a single account.
@@ -105,7 +105,7 @@ tokmon serve --port 8080
105
105
  tokmon serve --no-open # don't auto-open the browser
106
106
  ```
107
107
 
108
- It binds to `127.0.0.1` by default and reads the same local daemon state — nothing leaves your machine. The browser connects directly over a same-origin WebSocket; dashboard URLs have no token or login step. Optional LAN access can be enabled in settings, with an explicit unsafe-access warning, and takes effect after the daemon restarts. The dashboard renders instantly from a cached snapshot, then streams live updates and goes idle when no tab is open. Filter by provider, model, account, and period, flip between dark and light, and export any panel — or a summary card — as a PNG with the **Share** button.
108
+ It binds to `127.0.0.1` by default and reads the same local daemon state — nothing leaves your machine. The browser connects directly over a same-origin WebSocket; dashboard URLs have no token or login step. Optional LAN access can be enabled in settings, with an explicit unsafe-access warning, and takes effect after the daemon restarts. The dashboard renders instantly from a cached snapshot, then streams live updates and goes idle when no tab is open. Press `R` or use the visible **Refresh** control to update it. Filter by provider, model, account, and period, flip between dark and light, and export any panel — or a summary card — as a PNG with the **Share** button.
109
109
 
110
110
  ### Overview
111
111
 
@@ -192,6 +192,8 @@ Press `s` to open.
192
192
  - **Dashboard layout** — `grid` (all providers at once) or `single` (one at a time)
193
193
  - **Default focus** — start on `all`, or remember your `last` focused account
194
194
  - **ASCII glyphs** — `auto` (detect), `on` (force ASCII), or `off` (force Unicode)
195
+ - **Network access** — loopback-only by default; optional LAN access is explicitly marked unsafe
196
+ - **Reset times** — show time remaining or the exact reset date/time in the configured timezone
195
197
 
196
198
  **Providers** — toggle each provider on or off.
197
199
 
@@ -253,7 +255,7 @@ tokmon runs a small local **daemon** that does all the data collection. The term
253
255
  - Each enabled provider is detected automatically, and its real account identity — email and plan — is read from local auth (e.g. Claude `~/.claude.json`, the Codex `id_token`, Cursor's state DB). Extra accounts, like additional Claude homes, are auto-discovered too.
254
256
 
255
257
  **Limits & billing**
256
- - Rate limits and remaining spend/quota come from each provider's own official API, refreshed on the billing poll interval and when the terminal regains focus. Opening a dashboard also catches up stale limits immediately.
258
+ - Rate limits and remaining spend/quota come from each provider's own official API. Tokmon refreshes them on the configured billing interval; terminal focus and extra dashboard viewers reuse fresh data instead of issuing extra provider requests.
257
259
 
258
260
  **Responsiveness**
259
261
  - Dashboard summaries and table history load independently and refresh on separate intervals, so the UI stays responsive even on large histories.
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createDaemonRpcClient
4
- } from "./chunk-45ZP755H.js";
4
+ } from "./chunk-4CY725NY.js";
5
5
  import {
6
6
  accountsByProvider,
7
7
  buildAccounts,
8
8
  fetchPeak,
9
9
  modelColor
10
- } from "./chunk-42H7R376.js";
10
+ } from "./chunk-VGYBQH5I.js";
11
11
  import {
12
12
  PROVIDERS,
13
13
  PROVIDER_ORDER,
@@ -15,13 +15,15 @@ import {
15
15
  col,
16
16
  currency,
17
17
  detectProviders,
18
+ formatResetAt,
19
+ resetAt,
18
20
  resolveTimezone,
19
21
  shortDate,
20
22
  systemTimezone,
21
23
  time,
22
24
  tokens,
23
25
  withTimeout
24
- } from "./chunk-O27I2XFN.js";
26
+ } from "./chunk-RTGMDE7U.js";
25
27
  import {
26
28
  COLOR_PALETTE,
27
29
  DEFAULTS,
@@ -36,7 +38,7 @@ import {
36
38
  sanitizeTyped,
37
39
  saveConfigSync,
38
40
  snapshotCacheFile
39
- } from "./chunk-5CWOJMAH.js";
41
+ } from "./chunk-E2YXYU73.js";
40
42
  import {
41
43
  glyphs
42
44
  } from "./chunk-RF4GGQGM.js";
@@ -161,27 +163,22 @@ var TabBar = memo(function TabBar2({ tabs, active, onSelect }) {
161
163
  " "
162
164
  ] }) }, t)) });
163
165
  });
164
- var PeakBadge = memo(function PeakBadge2({ peak }) {
166
+ var PeakBadge = memo(function PeakBadge2({ peak, resetDisplay = "relative", tz }) {
165
167
  const color = peak.state === "peak" ? "red" : "green";
168
+ const changesAt = peak.changesAt ?? (peak.minutesUntilChange !== null ? new Date(Date.now() + peak.minutesUntilChange * 6e4).toISOString() : null);
166
169
  return /* @__PURE__ */ jsxs(Box, { children: [
167
170
  /* @__PURE__ */ jsxs(Text, { color, children: [
168
171
  glyphs().dot,
169
172
  " "
170
173
  ] }),
171
174
  /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
172
- peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
175
+ changesAt && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
173
176
  " (",
174
- fmtMinutes(peak.minutesUntilChange),
177
+ formatResetAt(changesAt, resetDisplay, Date.now(), tz),
175
178
  ")"
176
179
  ] })
177
180
  ] });
178
181
  });
179
- function fmtMinutes(mins) {
180
- if (mins < 60) return `${mins}m`;
181
- const h = Math.floor(mins / 60);
182
- const m = mins % 60;
183
- return m === 0 ? `${h}h` : `${h}h ${m}m`;
184
- }
185
182
  function currencySymbol(cur) {
186
183
  return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
187
184
  }
@@ -292,7 +289,7 @@ function computeDashLayout(groups, stats, cols, budget, focusId, layoutPref) {
292
289
  }
293
290
  return chooseLayout(content, budget, groups.length, single, cols, heights);
294
291
  }
295
- var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget, focusId, layout, page = 0, privacyMode = false }) {
292
+ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget, focusId, layout, page = 0, privacyMode = false, resetDisplay = "relative", tz }) {
296
293
  if (groups.length === 0) {
297
294
  return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
298
295
  "No providers enabled ",
@@ -307,7 +304,7 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
307
304
  const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
308
305
  const visible = pageCount > 1 ? groups.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : groups;
309
306
  return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
310
- /* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, alignItems: "flex-start", children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant, privacyMode }) }, g.provider)) }),
307
+ /* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, alignItems: "flex-start", children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant, privacyMode, resetDisplay, tz }) }, g.provider)) }),
311
308
  pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
312
309
  " ",
313
310
  glyphs().middot,
@@ -323,7 +320,7 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
323
320
  ] })
324
321
  ] });
325
322
  });
326
- function ProviderCard({ provider, accounts, stats, width, variant, privacyMode = false }) {
323
+ function ProviderCard({ provider, accounts, stats, width, variant, privacyMode = false, resetDisplay, tz }) {
327
324
  const meta = PROVIDERS[provider];
328
325
  const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
329
326
  const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
@@ -359,7 +356,7 @@ function ProviderCard({ provider, accounts, stats, width, variant, privacyMode =
359
356
  ] })),
360
357
  meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
361
358
  meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
362
- /* @__PURE__ */ jsx2(LimitsBlock, { items, inner, privacyMode })
359
+ /* @__PURE__ */ jsx2(LimitsBlock, { items, inner, privacyMode, resetDisplay, tz })
363
360
  ] }),
364
361
  meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items, privacyMode }),
365
362
  hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
@@ -432,11 +429,12 @@ function accountTitle(account, billing, privacyMode = false) {
432
429
  const title = email && !account.name.includes("@") ? `${account.name} ${email}` : account.name;
433
430
  return privacyMode ? redactEmail(title) : title;
434
431
  }
435
- function LimitsBlock({ items, inner, privacyMode }) {
432
+ function LimitsBlock({ items, inner, privacyMode, resetDisplay, tz }) {
436
433
  const showName = items.length > 1;
437
434
  const labels = items.flatMap((i) => i.s?.billing?.metrics ?? []).map((m) => m.label.length);
438
435
  const labelW = Math.min(Math.max(7, ...labels) + 1, 14);
439
- const barW = Math.max(10, Math.min(46, inner - labelW - 13));
436
+ const resetW = resetDisplay === "absolute" ? 17 : 8;
437
+ const barW = Math.max(10, Math.min(46, inner - labelW - resetW - 5));
440
438
  return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
441
439
  const billing = s?.billing;
442
440
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
@@ -450,11 +448,11 @@ function LimitsBlock({ items, inner, privacyMode }) {
450
448
  !billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
451
449
  "Fetching",
452
450
  glyphs().ellipsis
453
- ] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", wrap: "truncate-end", children: privacyMode ? redactEmail(billing.error) : billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW, labelW }, `${m.label}${i}`))
451
+ ] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", wrap: "truncate-end", children: privacyMode ? redactEmail(billing.error) : billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW, labelW, resetW, resetDisplay, tz }, `${m.label}${i}`))
454
452
  ] }, account.id);
455
453
  }) });
456
454
  }
457
- function MetricRow({ m, color, barW, labelW }) {
455
+ function MetricRow({ m, color, barW, labelW, resetW, resetDisplay, tz }) {
458
456
  if (m.format.kind === "percent") {
459
457
  const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
460
458
  return /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -464,7 +462,7 @@ function MetricRow({ m, color, barW, labelW }) {
464
462
  Math.round(m.used),
465
463
  "%"
466
464
  ] }) }),
467
- /* @__PURE__ */ jsx2(Box2, { width: 8, justifyContent: "flex-end", children: m.resetsAt ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: m.resetsAt }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
465
+ /* @__PURE__ */ jsx2(Box2, { width: resetW, justifyContent: "flex-end", children: m.resetsAt ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: resetAt(m.resetsAt, resetDisplay, Date.now(), tz) }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
468
466
  ] });
469
467
  }
470
468
  return /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -483,12 +481,13 @@ function SparkFooter({ series, month, color }) {
483
481
  ] });
484
482
  }
485
483
  function aggregate(list) {
486
- const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
484
+ const zero = () => ({ cost: 0, tokens: 0, input: 0, cacheRead: 0, cacheSavings: 0 });
487
485
  const z = { today: zero(), week: zero(), month: zero(), burnRate: 0, series: [] };
488
486
  for (const d of list) {
489
487
  for (const k of ["today", "week", "month"]) {
490
488
  z[k].cost += d[k].cost;
491
489
  z[k].tokens += d[k].tokens;
490
+ z[k].input += d[k].input ?? 0;
492
491
  z[k].cacheRead += d[k].cacheRead;
493
492
  z[k].cacheSavings += d[k].cacheSavings;
494
493
  }
@@ -500,7 +499,7 @@ function aggregate(list) {
500
499
  return z;
501
500
  }
502
501
  var TotalsRow = memo2(function TotalsRow2({ groups, stats, cols }) {
503
- const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
502
+ const zero = () => ({ cost: 0, tokens: 0, input: 0, cacheRead: 0, cacheSavings: 0 });
504
503
  const t = zero(), w = zero(), m = zero();
505
504
  for (const g of groups) {
506
505
  if (!PROVIDERS[g.provider].hasUsage) continue;
@@ -509,18 +508,23 @@ var TotalsRow = memo2(function TotalsRow2({ groups, stats, cols }) {
509
508
  if (!d) continue;
510
509
  t.cost += d.today.cost;
511
510
  t.tokens += d.today.tokens;
511
+ t.input += d.today.input ?? 0;
512
512
  w.cost += d.week.cost;
513
513
  w.tokens += d.week.tokens;
514
+ w.input += d.week.input ?? 0;
514
515
  m.cost += d.month.cost;
515
516
  m.tokens += d.month.tokens;
517
+ m.input += d.month.input ?? 0;
516
518
  }
517
519
  }
518
520
  const inner = cols - 4;
519
521
  const dot = glyphs().middot;
520
- const full = `${glyphs().dotAll} Today ${currency(t.cost)} (${tokens(t.tokens)} tok) ${dot} Week ${currency(w.cost)} (${tokens(w.tokens)} tok) ${dot} Month ${currency(m.cost)} (${tokens(m.tokens)} tok)`;
521
- const noTok = `${glyphs().dotAll} Today ${currency(t.cost)} ${dot} Week ${currency(w.cost)} ${dot} Month ${currency(m.cost)}`;
522
+ const monthNonCached = m.input > 0 ? `, ${tokens(m.input)} non-cached` : "";
523
+ const full = `${glyphs().dotAll} Today ${currency(t.cost)} (${tokens(t.tokens)} tok) ${dot} Week ${currency(w.cost)} (${tokens(w.tokens)} tok) ${dot} Month ${currency(m.cost)} (${tokens(m.tokens)} tok${monthNonCached})`;
524
+ const monthOnly = `${glyphs().dotAll} This Month ${currency(m.cost)} (${tokens(m.tokens)} tok${monthNonCached})`;
525
+ const noTok = `${glyphs().dotAll} Today ${currency(t.cost)} ${dot} Week ${currency(w.cost)} ${dot} Month ${currency(m.cost)}${m.input > 0 ? ` (${tokens(m.input)} non-cached)` : ""}`;
522
526
  const tight = `${glyphs().dotAll} ${currency(t.cost)} ${dot} ${currency(w.cost)} ${dot} ${currency(m.cost)}`;
523
- const text = full.length <= inner ? full : noTok.length <= inner ? noTok : tight;
527
+ const text = full.length <= inner ? full : monthOnly.length <= inner ? monthOnly : noTok.length <= inner ? noTok : tight;
524
528
  return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: text }) });
525
529
  });
526
530
 
@@ -532,7 +536,7 @@ import { Box as Box4, Text as Text4 } from "ink";
532
536
  import { memo as memo3 } from "react";
533
537
  import { Box as Box3, Text as Text3 } from "ink";
534
538
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
535
- var GENERAL_ROWS = 9;
539
+ var GENERAL_ROWS = 10;
536
540
  var SETTINGS_TABS = ["general", "providers", "accounts"];
537
541
  var SETTINGS_TAB_LABELS = {
538
542
  general: "General",
@@ -628,7 +632,18 @@ var SettingsView = memo3(function SettingsView2({
628
632
  ] })
629
633
  ] }),
630
634
  /* @__PURE__ */ jsx3(Row, { cursor, idx: 8, label: "Network access", children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: config.allowNetworkAccess ? "red" : "green", children: config.allowNetworkAccess ? "LAN (unsafe)" : "local only" }) }),
631
- config.allowNetworkAccess && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " Warning: dashboard data and settings will be exposed to your local network after daemon restart." })
635
+ config.allowNetworkAccess && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " Warning: dashboard data and settings will be exposed to your local network after daemon restart." }),
636
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 9, label: "Reset times", children: [
637
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
638
+ glyphs().caretL,
639
+ " "
640
+ ] }),
641
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: config.resetDisplay === "relative" ? "time remaining" : "exact date/time" }),
642
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
643
+ " ",
644
+ glyphs().caretR
645
+ ] })
646
+ ] })
632
647
  ] }),
633
648
  activeTab === "providers" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
634
649
  /* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Providers" }),
@@ -2123,6 +2138,10 @@ function handleSettings(input, key, ctx) {
2123
2138
  }
2124
2139
  if (index === 8 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2125
2140
  updateConfig((current) => ({ ...current, allowNetworkAccess: !current.allowNetworkAccess }));
2141
+ return;
2142
+ }
2143
+ if (index === 9 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2144
+ updateConfig((current) => ({ ...current, resetDisplay: current.resetDisplay === "relative" ? "absolute" : "relative" }));
2126
2145
  }
2127
2146
  }
2128
2147
  }
@@ -2278,10 +2297,9 @@ function terminalFocusEvent(input) {
2278
2297
  if (input === "[O") return "out";
2279
2298
  return null;
2280
2299
  }
2281
- function handleTerminalFocusInput(input, onFocusIn) {
2300
+ function handleTerminalFocusInput(input) {
2282
2301
  const event = terminalFocusEvent(input);
2283
2302
  if (!event) return false;
2284
- if (event === "in") onFocusIn();
2285
2303
  return true;
2286
2304
  }
2287
2305
  function isRefreshAllShortcut(input, mode) {
@@ -3389,7 +3407,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3389
3407
  if (webStartingRef.current) return;
3390
3408
  webStartingRef.current = true;
3391
3409
  try {
3392
- const { startWebServer } = await import("./server-X63FGHIZ.js");
3410
+ const { startWebServer } = await import("./server-5WIWS27N.js");
3393
3411
  const ctrl = await startWebServer({ config: cfg, log: false });
3394
3412
  webRef.current = ctrl;
3395
3413
  openUrl(ctrl.url);
@@ -3436,17 +3454,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3436
3454
  () => tab === 1 ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
3437
3455
  [tab, rawTokenRows, activeTableModel, search, sort]
3438
3456
  );
3439
- const lastFocusRefreshRef = useRef8(Date.now());
3440
- const refreshOnFocus = useCallback7(() => {
3441
- const now = Date.now();
3442
- if (now - lastFocusRefreshRef.current < 3e4) return;
3443
- lastFocusRefreshRef.current = now;
3444
- const task = connected ? daemonRefreshRef.current("billing") : degradedRefreshRef.current();
3445
- void task.catch(() => {
3446
- });
3447
- }, [connected]);
3448
3457
  useInput((input, key) => {
3449
- if (handleTerminalFocusInput(input, refreshOnFocus)) return;
3458
+ if (handleTerminalFocusInput(input)) return;
3450
3459
  handleKey(input, key, {
3451
3460
  onboarding: {
3452
3461
  show: showPicker,
@@ -3568,7 +3577,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3568
3577
  ] }),
3569
3578
  /* @__PURE__ */ jsxs12(Box11, { children: [
3570
3579
  peak && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3571
- /* @__PURE__ */ jsx12(PeakBadge, { peak }),
3580
+ /* @__PURE__ */ jsx12(PeakBadge, { peak, resetDisplay: cfg.resetDisplay, tz }),
3572
3581
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3573
3582
  " ",
3574
3583
  glyphs().middot,
@@ -3627,7 +3636,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3627
3636
  ] })
3628
3637
  ] }),
3629
3638
  tab === 0 && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3630
- /* @__PURE__ */ jsx12(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode }),
3639
+ /* @__PURE__ */ jsx12(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode, resetDisplay: cfg.resetDisplay, tz }),
3631
3640
  slots.length > 1 && /* @__PURE__ */ jsxs12(Box11, { marginTop: 1, children: [
3632
3641
  /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "focus " }),
3633
3642
  /* @__PURE__ */ jsx12(
@@ -3,7 +3,7 @@ import {
3
3
  TOKMON_WS_METHODS,
4
4
  TOKMON_WS_PATH,
5
5
  TokmonRpcGroup
6
- } from "./chunk-O27I2XFN.js";
6
+ } from "./chunk-RTGMDE7U.js";
7
7
 
8
8
  // src/client/daemon-rpc-client.ts
9
9
  import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
@@ -5,7 +5,7 @@ import {
5
5
  readLock,
6
6
  reclaimDeadLock,
7
7
  verifyLock
8
- } from "./chunk-3TJVFKXV.js";
8
+ } from "./chunk-QM5E5RJZ.js";
9
9
  import {
10
10
  appVersion
11
11
  } from "./chunk-SMPY52EV.js";
@@ -26,6 +26,7 @@ var DEFAULTS = {
26
26
  defaultFocus: "all",
27
27
  ascii: "auto",
28
28
  allowNetworkAccess: false,
29
+ resetDisplay: "relative",
29
30
  knownProviders: []
30
31
  };
31
32
  var LEGACY_KNOWN = ["claude", "codex", "cursor"];
@@ -216,6 +217,7 @@ function repairConfig(input) {
216
217
  defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
217
218
  ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
218
219
  allowNetworkAccess: parsed.allowNetworkAccess === true,
220
+ resetDisplay: parsed.resetDisplay === "absolute" ? "absolute" : "relative",
219
221
  knownProviders
220
222
  };
221
223
  for (const key of Object.keys(DEFAULTS)) {
@@ -11,7 +11,7 @@ import {
11
11
  colorHex,
12
12
  fetchPeak,
13
13
  namedHex
14
- } from "./chunk-42H7R376.js";
14
+ } from "./chunk-VGYBQH5I.js";
15
15
  import {
16
16
  PROVIDERS,
17
17
  TOKMON_CAPABILITIES,
@@ -22,7 +22,7 @@ import {
22
22
  detectProviders,
23
23
  resolveTimezone,
24
24
  withTimeout
25
- } from "./chunk-O27I2XFN.js";
25
+ } from "./chunk-RTGMDE7U.js";
26
26
  import {
27
27
  cacheDir,
28
28
  expandHome,
@@ -30,7 +30,7 @@ import {
30
30
  normalizeConfig,
31
31
  saveConfig,
32
32
  snapshotCacheFile
33
- } from "./chunk-5CWOJMAH.js";
33
+ } from "./chunk-E2YXYU73.js";
34
34
 
35
35
  // src/web/server.ts
36
36
  import { createServer } from "http";
@@ -189,7 +189,7 @@ function deferred() {
189
189
  });
190
190
  return { promise, resolve, reject };
191
191
  }
192
- function createRefreshQueue(perform, skipAutomatic = () => false) {
192
+ function createRefreshQueue(perform, skipAutomatic = () => false, options = {}) {
193
193
  let activePass = null;
194
194
  let activeResult = null;
195
195
  let queued = null;
@@ -226,6 +226,7 @@ function createRefreshQueue(perform, skipAutomatic = () => false) {
226
226
  if (stopped || !force && skipAutomatic()) return Promise.resolve();
227
227
  if (!activePass) return launch();
228
228
  if (!force) return activeResult.promise;
229
+ if (options.forceWhileActive === "join") return activeResult.promise;
229
230
  if (!queued) queued = deferred();
230
231
  return queued.promise;
231
232
  };
@@ -252,10 +253,21 @@ var IDLE_PAUSE_MS = 6e4;
252
253
  var SNAPSHOT_CACHE_THROTTLE_MS = 2e4;
253
254
  var REVEAL_THROTTLE_MS = 500;
254
255
  var FETCH_TIMEOUT_MS = 3e4;
255
- var ATTACH_BILLING_MAX_AGE_MS = 3e4;
256
- function billingNeedsCatchUp(accounts, updatedAt, now = Date.now(), maxAgeMs = ATTACH_BILLING_MAX_AGE_MS) {
256
+ var DEFAULT_BILLING_STALE_MS = 5 * 6e4;
257
+ function billingNeedsCatchUp(accounts, updatedAt, now = Date.now(), maxAgeMs = DEFAULT_BILLING_STALE_MS) {
257
258
  return accounts.some(({ account }) => now - (updatedAt.get(account.id) ?? 0) >= maxAgeMs);
258
259
  }
260
+ async function forEachProviderSequentially(values, task) {
261
+ const groups = /* @__PURE__ */ new Map();
262
+ for (const value of values) {
263
+ const group = groups.get(value.account.providerId) ?? [];
264
+ group.push(value);
265
+ groups.set(value.account.providerId, group);
266
+ }
267
+ await Promise.all([...groups.values()].map(async (group) => {
268
+ for (const value of group) await task(value);
269
+ }));
270
+ }
259
271
  function runInBackground(task) {
260
272
  void task.catch(() => {
261
273
  });
@@ -405,7 +417,14 @@ function createDataEngine(opts) {
405
417
  }
406
418
  reveal();
407
419
  };
408
- if (opts2.concurrent) {
420
+ if (opts2.concurrent === "provider") {
421
+ await forEachProviderSequentially(opts2.accounts(), async (r) => {
422
+ const result = await fetchOne(r);
423
+ if (stopped || epoch !== configEpoch) return;
424
+ applyResult(result);
425
+ });
426
+ if (stopped || epoch !== configEpoch) return;
427
+ } else if (opts2.concurrent) {
409
428
  await Promise.all(opts2.accounts().map(async (r) => {
410
429
  const result = await fetchOne(r);
411
430
  if (stopped || epoch !== configEpoch) return;
@@ -423,7 +442,8 @@ function createDataEngine(opts) {
423
442
  rebuild();
424
443
  throwIfRefreshFailures(opts2.scope, failures);
425
444
  },
426
- idle
445
+ idle,
446
+ { forceWhileActive: opts2.forceWhileActive }
427
447
  );
428
448
  const refreshSummary = makeRefreshLoop({
429
449
  scope: "summary",
@@ -454,8 +474,10 @@ function createDataEngine(opts) {
454
474
  },
455
475
  state: billingState,
456
476
  updatedAt: billingUpdatedAt,
457
- concurrent: true
477
+ concurrent: "provider",
478
+ forceWhileActive: "join"
458
479
  });
480
+ const refreshBillingIfStale = () => billingNeedsCatchUp(billingAccounts, billingUpdatedAt, Date.now(), billingIntervalMs) ? refreshBilling.run(true) : Promise.resolve();
459
481
  const refreshPeak = createRefreshQueue(
460
482
  async () => {
461
483
  if (stopped || !hasClaude) return;
@@ -549,7 +571,7 @@ function createDataEngine(opts) {
549
571
  rebuild();
550
572
  runInBackground(refreshSummary.run(true));
551
573
  runInBackground(refreshTable.run(true));
552
- runInBackground(refreshBilling.run(true));
574
+ runInBackground(refreshBillingIfStale());
553
575
  if (hasClaude) runInBackground(refreshPeak.run(true));
554
576
  startTimers();
555
577
  },
@@ -576,9 +598,7 @@ function createDataEngine(opts) {
576
598
  runInBackground(refreshSummary.run(true));
577
599
  runInBackground(refreshTable.run(true));
578
600
  }
579
- if (billingNeedsCatchUp(billingAccounts, billingUpdatedAt)) {
580
- runInBackground(refreshBilling.run());
581
- }
601
+ runInBackground(refreshBillingIfStale());
582
602
  return () => {
583
603
  snapshotSubscribers.delete(onSnapshot);
584
604
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  cacheDir
4
- } from "./chunk-5CWOJMAH.js";
4
+ } from "./chunk-E2YXYU73.js";
5
5
 
6
6
  // src/web/lockfile.ts
7
7
  import { closeSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -5,7 +5,7 @@ import {
5
5
  envDir,
6
6
  expandHome,
7
7
  isValidTimezone
8
- } from "./chunk-5CWOJMAH.js";
8
+ } from "./chunk-E2YXYU73.js";
9
9
 
10
10
  // src/async.ts
11
11
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -590,15 +590,16 @@ function summarize(entries, tz) {
590
590
  const todayStart = startOfDay(now, tz);
591
591
  const weekStart = startOfWeek(now, tz);
592
592
  const monthStart = startOfMonth(now, tz);
593
- const today = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
594
- const week = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
595
- const month = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
593
+ const today = { cost: 0, tokens: 0, input: 0, cacheRead: 0, cacheSavings: 0 };
594
+ const week = { cost: 0, tokens: 0, input: 0, cacheRead: 0, cacheSavings: 0 };
595
+ const month = { cost: 0, tokens: 0, input: 0, cacheRead: 0, cacheSavings: 0 };
596
596
  const byDay = /* @__PURE__ */ new Map();
597
597
  let oldestToday = now;
598
598
  let hadToday = false;
599
599
  const add = (s, e) => {
600
600
  s.cost += e.cost;
601
601
  s.tokens += e.input + e.output + e.cacheCreate + e.cacheRead;
602
+ s.input += e.input;
602
603
  s.cacheRead += e.cacheRead;
603
604
  s.cacheSavings += e.cacheSavings;
604
605
  };
@@ -786,6 +787,7 @@ var ConfigSchema = Schema.Struct({
786
787
  defaultFocus: Schema.Literals(["all", "last"]),
787
788
  ascii: Schema.Literals(["auto", "on", "off"]),
788
789
  allowNetworkAccess: Schema.Boolean,
790
+ resetDisplay: Schema.Literals(["relative", "absolute"]),
789
791
  knownProviders: Schema.Array(ProviderIdSchema)
790
792
  });
791
793
  var ProtocolInfoSchema = Schema.Struct({
@@ -824,6 +826,7 @@ var FsListingSchema = Schema.Struct({
824
826
  var UsageSummarySchema = Schema.Struct({
825
827
  cost: Schema.Finite,
826
828
  tokens: Schema.Finite,
829
+ input: Schema.Finite,
827
830
  cacheRead: Schema.Finite,
828
831
  cacheSavings: Schema.Finite
829
832
  });
@@ -930,7 +933,8 @@ var WebSnapshotSchema = Schema.Struct({
930
933
  label: Schema.String,
931
934
  // The upstream clock is only normalized to a finite number; do not make
932
935
  // the wire contract narrower than the producer's declared type.
933
- minutesUntilChange: Schema.NullOr(Schema.Finite)
936
+ minutesUntilChange: Schema.NullOr(Schema.Finite),
937
+ changesAt: Schema.optionalKey(Schema.NullOr(Schema.String))
934
938
  }))
935
939
  });
936
940
  var EmptyPayloadSchema = Schema.Struct({});
@@ -1017,6 +1021,18 @@ function formatResetIn(iso, now = Date.now()) {
1017
1021
  const hrs = hours % 24;
1018
1022
  return `${days}d ${hrs}h`;
1019
1023
  }
1024
+ function formatResetAt(iso, display, now = Date.now(), tz) {
1025
+ const timestamp = Date.parse(iso);
1026
+ if (!Number.isFinite(timestamp)) return iso;
1027
+ if (display === "relative") return formatResetIn(iso, now);
1028
+ return new Intl.DateTimeFormat(void 0, {
1029
+ month: "short",
1030
+ day: "numeric",
1031
+ hour: "numeric",
1032
+ minute: "2-digit",
1033
+ timeZone: tz
1034
+ }).format(new Date(timestamp));
1035
+ }
1020
1036
 
1021
1037
  // src/format.ts
1022
1038
  var currency = formatCurrency;
@@ -1030,7 +1046,7 @@ function col(s, w, align = "right") {
1030
1046
  const spaces = " ".repeat(w - s.length);
1031
1047
  return align === "right" ? spaces + s : s + spaces;
1032
1048
  }
1033
- var resetIn = formatResetIn;
1049
+ var resetAt = formatResetAt;
1034
1050
 
1035
1051
  // src/providers/claude/usage.ts
1036
1052
  import { stat as fsStat, access } from "fs/promises";
@@ -1432,11 +1448,13 @@ function boolValue(value) {
1432
1448
  return false;
1433
1449
  }
1434
1450
  function resetFrom(value) {
1435
- if (typeof value === "string" && value.trim()) return resetIn(value);
1451
+ if (typeof value === "string" && value.trim()) {
1452
+ const timestamp = Date.parse(value);
1453
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
1454
+ }
1436
1455
  const n = numberValue(value);
1437
1456
  if (n === void 0) return null;
1438
- const iso = msToIso(Math.abs(n) < 1e10 ? n * 1e3 : n);
1439
- return iso ? resetIn(iso) : null;
1457
+ return msToIso(Math.abs(n) < 1e10 ? n * 1e3 : n);
1440
1458
  }
1441
1459
  function usageMetric(label, window, primary) {
1442
1460
  const used = numberValue(window?.utilization);
@@ -1989,8 +2007,7 @@ function resetDateMs(window) {
1989
2007
  }
1990
2008
  function resetFrom2(window) {
1991
2009
  const ms = resetDateMs(window);
1992
- const iso = ms === null ? null : msToIso(ms);
1993
- return iso ? resetIn(iso) : null;
2010
+ return ms === null ? null : msToIso(ms);
1994
2011
  }
1995
2012
  function windowSeconds(window) {
1996
2013
  const seconds = numberValue(window?.limit_window_seconds ?? window?.period_seconds ?? window?.window_seconds);
@@ -2569,7 +2586,7 @@ async function cursorBillingCore(account) {
2569
2586
  const rawEnd = usage.billingCycleEnd;
2570
2587
  const endMs = timestampMs(rawEnd);
2571
2588
  const iso = endMs === null ? null : msToIso(endMs);
2572
- const resets = iso ? resetIn(iso) : null;
2589
+ const resets = iso;
2573
2590
  const limit = numberValue(pu.limit);
2574
2591
  const planUsedCents = numberValue(pu.totalSpend) ?? (limit !== void 0 && numberValue(pu.remaining) !== void 0 ? limit - numberValue(pu.remaining) : void 0);
2575
2592
  const computedPercent = limit !== void 0 && limit > 0 && planUsedCents !== void 0 ? planUsedCents / limit * 100 : void 0;
@@ -3118,7 +3135,9 @@ function redactToken(token) {
3118
3135
  return token.length <= 4 ? "****" : `****${token.slice(-4)}`;
3119
3136
  }
3120
3137
  function resetDate(value) {
3121
- return typeof value === "string" && value.trim() ? resetIn(value) : null;
3138
+ if (typeof value !== "string" || !value.trim()) return null;
3139
+ const timestamp = Date.parse(value);
3140
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
3122
3141
  }
3123
3142
  function boolValue2(value) {
3124
3143
  if (typeof value === "boolean") return value;
@@ -3625,7 +3644,7 @@ function cloudCodeBucketsToMetrics(buckets, options = {}) {
3625
3644
  used: Math.round((1 - clamped) * 100),
3626
3645
  limit: 100,
3627
3646
  format: { kind: "percent" },
3628
- resetsAt: bucket.resetTime && Number.isFinite(Date.parse(bucket.resetTime)) ? resetIn(bucket.resetTime) : null,
3647
+ resetsAt: bucket.resetTime && Number.isFinite(Date.parse(bucket.resetTime)) ? new Date(bucket.resetTime).toISOString() : null,
3629
3648
  primary: i === 0
3630
3649
  };
3631
3650
  });
@@ -4556,15 +4575,17 @@ export {
4556
4575
  flushDisk,
4557
4576
  coalesceTables,
4558
4577
  claudeConfigDirs,
4578
+ readJson,
4579
+ readClaudeIdentity,
4580
+ codexHomes,
4581
+ identityFromIdToken,
4582
+ formatResetAt,
4559
4583
  currency,
4560
4584
  tokens,
4561
4585
  time,
4562
4586
  shortDate,
4563
4587
  col,
4564
- readJson,
4565
- readClaudeIdentity,
4566
- codexHomes,
4567
- identityFromIdToken,
4588
+ resetAt,
4568
4589
  cursorStateDb,
4569
4590
  piSessionsDir,
4570
4591
  opencodeDbPaths,