tokmon 0.24.0 → 0.25.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-O55MPGXJ.js} +42 -39
  3. package/dist/{chunk-42H7R376.js → chunk-45YYP2FJ.js} +4 -3
  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-O27I2XFN.js → chunk-INBZLHJQ.js} +34 -15
  7. package/dist/{chunk-REO4O5VE.js → chunk-IOIAXKW2.js} +33 -13
  8. package/dist/{chunk-3TJVFKXV.js → chunk-QM5E5RJZ.js} +1 -1
  9. package/dist/{chunk-45ZP755H.js → chunk-QUGH2XA6.js} +1 -1
  10. package/dist/{cli-command-TLGIGF63.js → cli-command-YQ5T4R73.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-QBOXJFRA.js} +5 -5
  14. package/dist/{daemon-handle-NLCLYWFU.js → daemon-handle-2GVZT55B.js} +3 -3
  15. package/dist/server-23ERUOJ7.js +11 -0
  16. package/dist/web/assets/{Area-DCES17aZ.js → Area-DbVPPS7M.js} +1 -1
  17. package/dist/web/assets/{analytics-DDW6zcnj.js → analytics-DS5p9Duq.js} +2 -2
  18. package/dist/web/assets/{breakdown-D5LPPtqh.js → breakdown-zYF0TXN5.js} +1 -1
  19. package/dist/web/assets/{chart-bHY7MQV0.js → chart-BZHliIXk.js} +1 -1
  20. package/dist/web/assets/{explore-DdWB-Pu7.js → explore-DYv8bb2r.js} +1 -1
  21. package/dist/web/assets/{index-C9jxb2o3.js → index-BO8m4dq1.js} +24 -24
  22. package/dist/web/assets/index-NnKaHxPO.css +1 -0
  23. package/dist/web/assets/{models-o1wJnG6w.js → models-P3OwakgO.js} +2 -2
  24. package/dist/web/assets/overview-5WswYjWK.js +2 -0
  25. package/dist/web/assets/{panel-DBnK1oy0.js → panel-CpwRWsNg.js} +1 -1
  26. package/dist/web/assets/{primitives-CBmteetD.js → primitives-Do0zWLZ4.js} +1 -1
  27. package/dist/web/assets/settings-sheet-uD6-A3tV.js +1 -0
  28. package/dist/web/assets/{share-sheet-DUee3FlK.js → share-sheet-CimegH2K.js} +2 -2
  29. package/dist/web/assets/{timeline-BxHwOJA_.js → timeline-Ch94Yv4z.js} +1 -1
  30. package/dist/web/assets/{use-dialog-trap-30EqxYF-.js → use-dialog-trap-LxEFLutK.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-QUGH2XA6.js";
5
5
  import {
6
6
  accountsByProvider,
7
7
  buildAccounts,
8
8
  fetchPeak,
9
9
  modelColor
10
- } from "./chunk-42H7R376.js";
10
+ } from "./chunk-45YYP2FJ.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-INBZLHJQ.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: [
@@ -532,7 +530,7 @@ import { Box as Box4, Text as Text4 } from "ink";
532
530
  import { memo as memo3 } from "react";
533
531
  import { Box as Box3, Text as Text3 } from "ink";
534
532
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
535
- var GENERAL_ROWS = 9;
533
+ var GENERAL_ROWS = 10;
536
534
  var SETTINGS_TABS = ["general", "providers", "accounts"];
537
535
  var SETTINGS_TAB_LABELS = {
538
536
  general: "General",
@@ -628,7 +626,18 @@ var SettingsView = memo3(function SettingsView2({
628
626
  ] })
629
627
  ] }),
630
628
  /* @__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." })
629
+ config.allowNetworkAccess && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " Warning: dashboard data and settings will be exposed to your local network after daemon restart." }),
630
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 9, label: "Reset times", children: [
631
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
632
+ glyphs().caretL,
633
+ " "
634
+ ] }),
635
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: config.resetDisplay === "relative" ? "time remaining" : "exact date/time" }),
636
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
637
+ " ",
638
+ glyphs().caretR
639
+ ] })
640
+ ] })
632
641
  ] }),
633
642
  activeTab === "providers" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
634
643
  /* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Providers" }),
@@ -2123,6 +2132,10 @@ function handleSettings(input, key, ctx) {
2123
2132
  }
2124
2133
  if (index === 8 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2125
2134
  updateConfig((current) => ({ ...current, allowNetworkAccess: !current.allowNetworkAccess }));
2135
+ return;
2136
+ }
2137
+ if (index === 9 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2138
+ updateConfig((current) => ({ ...current, resetDisplay: current.resetDisplay === "relative" ? "absolute" : "relative" }));
2126
2139
  }
2127
2140
  }
2128
2141
  }
@@ -2278,10 +2291,9 @@ function terminalFocusEvent(input) {
2278
2291
  if (input === "[O") return "out";
2279
2292
  return null;
2280
2293
  }
2281
- function handleTerminalFocusInput(input, onFocusIn) {
2294
+ function handleTerminalFocusInput(input) {
2282
2295
  const event = terminalFocusEvent(input);
2283
2296
  if (!event) return false;
2284
- if (event === "in") onFocusIn();
2285
2297
  return true;
2286
2298
  }
2287
2299
  function isRefreshAllShortcut(input, mode) {
@@ -3389,7 +3401,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3389
3401
  if (webStartingRef.current) return;
3390
3402
  webStartingRef.current = true;
3391
3403
  try {
3392
- const { startWebServer } = await import("./server-X63FGHIZ.js");
3404
+ const { startWebServer } = await import("./server-23ERUOJ7.js");
3393
3405
  const ctrl = await startWebServer({ config: cfg, log: false });
3394
3406
  webRef.current = ctrl;
3395
3407
  openUrl(ctrl.url);
@@ -3436,17 +3448,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3436
3448
  () => tab === 1 ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
3437
3449
  [tab, rawTokenRows, activeTableModel, search, sort]
3438
3450
  );
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
3451
  useInput((input, key) => {
3449
- if (handleTerminalFocusInput(input, refreshOnFocus)) return;
3452
+ if (handleTerminalFocusInput(input)) return;
3450
3453
  handleKey(input, key, {
3451
3454
  onboarding: {
3452
3455
  show: showPicker,
@@ -3568,7 +3571,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3568
3571
  ] }),
3569
3572
  /* @__PURE__ */ jsxs12(Box11, { children: [
3570
3573
  peak && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3571
- /* @__PURE__ */ jsx12(PeakBadge, { peak }),
3574
+ /* @__PURE__ */ jsx12(PeakBadge, { peak, resetDisplay: cfg.resetDisplay, tz }),
3572
3575
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3573
3576
  " ",
3574
3577
  glyphs().middot,
@@ -3627,7 +3630,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3627
3630
  ] })
3628
3631
  ] }),
3629
3632
  tab === 0 && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3630
- /* @__PURE__ */ jsx12(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode }),
3633
+ /* @__PURE__ */ jsx12(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode, resetDisplay: cfg.resetDisplay, tz }),
3631
3634
  slots.length > 1 && /* @__PURE__ */ jsxs12(Box11, { marginTop: 1, children: [
3632
3635
  /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "focus " }),
3633
3636
  /* @__PURE__ */ jsx12(
@@ -5,11 +5,11 @@ import {
5
5
  identityFromIdToken,
6
6
  readClaudeIdentity,
7
7
  readJson
8
- } from "./chunk-O27I2XFN.js";
8
+ } from "./chunk-INBZLHJQ.js";
9
9
  import {
10
10
  expandHome,
11
11
  slugify
12
- } from "./chunk-5CWOJMAH.js";
12
+ } from "./chunk-E2YXYU73.js";
13
13
 
14
14
  // src/accounts.ts
15
15
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
@@ -281,7 +281,8 @@ async function fetchPeak() {
281
281
  return {
282
282
  state,
283
283
  label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
284
- minutesUntilChange
284
+ minutesUntilChange,
285
+ changesAt: minutesUntilChange !== null ? new Date(Date.now() + minutesUntilChange * 6e4).toISOString() : null
285
286
  };
286
287
  } catch {
287
288
  return null;
@@ -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)) {
@@ -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;
@@ -786,6 +786,7 @@ var ConfigSchema = Schema.Struct({
786
786
  defaultFocus: Schema.Literals(["all", "last"]),
787
787
  ascii: Schema.Literals(["auto", "on", "off"]),
788
788
  allowNetworkAccess: Schema.Boolean,
789
+ resetDisplay: Schema.Literals(["relative", "absolute"]),
789
790
  knownProviders: Schema.Array(ProviderIdSchema)
790
791
  });
791
792
  var ProtocolInfoSchema = Schema.Struct({
@@ -930,7 +931,8 @@ var WebSnapshotSchema = Schema.Struct({
930
931
  label: Schema.String,
931
932
  // The upstream clock is only normalized to a finite number; do not make
932
933
  // the wire contract narrower than the producer's declared type.
933
- minutesUntilChange: Schema.NullOr(Schema.Finite)
934
+ minutesUntilChange: Schema.NullOr(Schema.Finite),
935
+ changesAt: Schema.optionalKey(Schema.NullOr(Schema.String))
934
936
  }))
935
937
  });
936
938
  var EmptyPayloadSchema = Schema.Struct({});
@@ -1017,6 +1019,18 @@ function formatResetIn(iso, now = Date.now()) {
1017
1019
  const hrs = hours % 24;
1018
1020
  return `${days}d ${hrs}h`;
1019
1021
  }
1022
+ function formatResetAt(iso, display, now = Date.now(), tz) {
1023
+ const timestamp = Date.parse(iso);
1024
+ if (!Number.isFinite(timestamp)) return iso;
1025
+ if (display === "relative") return formatResetIn(iso, now);
1026
+ return new Intl.DateTimeFormat(void 0, {
1027
+ month: "short",
1028
+ day: "numeric",
1029
+ hour: "numeric",
1030
+ minute: "2-digit",
1031
+ timeZone: tz
1032
+ }).format(new Date(timestamp));
1033
+ }
1020
1034
 
1021
1035
  // src/format.ts
1022
1036
  var currency = formatCurrency;
@@ -1030,7 +1044,7 @@ function col(s, w, align = "right") {
1030
1044
  const spaces = " ".repeat(w - s.length);
1031
1045
  return align === "right" ? spaces + s : s + spaces;
1032
1046
  }
1033
- var resetIn = formatResetIn;
1047
+ var resetAt = formatResetAt;
1034
1048
 
1035
1049
  // src/providers/claude/usage.ts
1036
1050
  import { stat as fsStat, access } from "fs/promises";
@@ -1432,11 +1446,13 @@ function boolValue(value) {
1432
1446
  return false;
1433
1447
  }
1434
1448
  function resetFrom(value) {
1435
- if (typeof value === "string" && value.trim()) return resetIn(value);
1449
+ if (typeof value === "string" && value.trim()) {
1450
+ const timestamp = Date.parse(value);
1451
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
1452
+ }
1436
1453
  const n = numberValue(value);
1437
1454
  if (n === void 0) return null;
1438
- const iso = msToIso(Math.abs(n) < 1e10 ? n * 1e3 : n);
1439
- return iso ? resetIn(iso) : null;
1455
+ return msToIso(Math.abs(n) < 1e10 ? n * 1e3 : n);
1440
1456
  }
1441
1457
  function usageMetric(label, window, primary) {
1442
1458
  const used = numberValue(window?.utilization);
@@ -1989,8 +2005,7 @@ function resetDateMs(window) {
1989
2005
  }
1990
2006
  function resetFrom2(window) {
1991
2007
  const ms = resetDateMs(window);
1992
- const iso = ms === null ? null : msToIso(ms);
1993
- return iso ? resetIn(iso) : null;
2008
+ return ms === null ? null : msToIso(ms);
1994
2009
  }
1995
2010
  function windowSeconds(window) {
1996
2011
  const seconds = numberValue(window?.limit_window_seconds ?? window?.period_seconds ?? window?.window_seconds);
@@ -2569,7 +2584,7 @@ async function cursorBillingCore(account) {
2569
2584
  const rawEnd = usage.billingCycleEnd;
2570
2585
  const endMs = timestampMs(rawEnd);
2571
2586
  const iso = endMs === null ? null : msToIso(endMs);
2572
- const resets = iso ? resetIn(iso) : null;
2587
+ const resets = iso;
2573
2588
  const limit = numberValue(pu.limit);
2574
2589
  const planUsedCents = numberValue(pu.totalSpend) ?? (limit !== void 0 && numberValue(pu.remaining) !== void 0 ? limit - numberValue(pu.remaining) : void 0);
2575
2590
  const computedPercent = limit !== void 0 && limit > 0 && planUsedCents !== void 0 ? planUsedCents / limit * 100 : void 0;
@@ -3118,7 +3133,9 @@ function redactToken(token) {
3118
3133
  return token.length <= 4 ? "****" : `****${token.slice(-4)}`;
3119
3134
  }
3120
3135
  function resetDate(value) {
3121
- return typeof value === "string" && value.trim() ? resetIn(value) : null;
3136
+ if (typeof value !== "string" || !value.trim()) return null;
3137
+ const timestamp = Date.parse(value);
3138
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
3122
3139
  }
3123
3140
  function boolValue2(value) {
3124
3141
  if (typeof value === "boolean") return value;
@@ -3625,7 +3642,7 @@ function cloudCodeBucketsToMetrics(buckets, options = {}) {
3625
3642
  used: Math.round((1 - clamped) * 100),
3626
3643
  limit: 100,
3627
3644
  format: { kind: "percent" },
3628
- resetsAt: bucket.resetTime && Number.isFinite(Date.parse(bucket.resetTime)) ? resetIn(bucket.resetTime) : null,
3645
+ resetsAt: bucket.resetTime && Number.isFinite(Date.parse(bucket.resetTime)) ? new Date(bucket.resetTime).toISOString() : null,
3629
3646
  primary: i === 0
3630
3647
  };
3631
3648
  });
@@ -4556,15 +4573,17 @@ export {
4556
4573
  flushDisk,
4557
4574
  coalesceTables,
4558
4575
  claudeConfigDirs,
4576
+ readJson,
4577
+ readClaudeIdentity,
4578
+ codexHomes,
4579
+ identityFromIdToken,
4580
+ formatResetAt,
4559
4581
  currency,
4560
4582
  tokens,
4561
4583
  time,
4562
4584
  shortDate,
4563
4585
  col,
4564
- readJson,
4565
- readClaudeIdentity,
4566
- codexHomes,
4567
- identityFromIdToken,
4586
+ resetAt,
4568
4587
  cursorStateDb,
4569
4588
  piSessionsDir,
4570
4589
  opencodeDbPaths,
@@ -11,7 +11,7 @@ import {
11
11
  colorHex,
12
12
  fetchPeak,
13
13
  namedHex
14
- } from "./chunk-42H7R376.js";
14
+ } from "./chunk-45YYP2FJ.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-INBZLHJQ.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";
@@ -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-INBZLHJQ.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";
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  attachOrSpawn
4
- } from "./chunk-W2WGBXZG.js";
5
- import "./chunk-3TJVFKXV.js";
4
+ } from "./chunk-AMD4PXDG.js";
5
+ import "./chunk-QM5E5RJZ.js";
6
6
  import "./chunk-SMPY52EV.js";
7
7
  import {
8
8
  createDaemonRpcClient
9
- } from "./chunk-45ZP755H.js";
9
+ } from "./chunk-QUGH2XA6.js";
10
10
  import {
11
11
  PROVIDERS,
12
12
  antigravityStateDb,
@@ -26,11 +26,11 @@ import {
26
26
  startOfMonth,
27
27
  startOfWeek,
28
28
  withTimeout
29
- } from "./chunk-O27I2XFN.js";
29
+ } from "./chunk-INBZLHJQ.js";
30
30
  import {
31
31
  PROVIDER_IDS,
32
32
  configLocation
33
- } from "./chunk-5CWOJMAH.js";
33
+ } from "./chunk-E2YXYU73.js";
34
34
 
35
35
  // src/provider-locations.ts
36
36
  import { access } from "fs/promises";