tokmon 0.23.5 → 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 +25 -3
  2. package/dist/{bootstrap-ink-WA3ENW2M.js → bootstrap-ink-O55MPGXJ.js} +52 -314
  3. package/dist/chunk-45YYP2FJ.js +299 -0
  4. package/dist/{daemon-handle-Y2N2NA6D.js → chunk-AMD4PXDG.js} +2 -2
  5. package/dist/{chunk-5CWOJMAH.js → chunk-E2YXYU73.js} +2 -0
  6. package/dist/{chunk-IELQ5EY3.js → chunk-INBZLHJQ.js} +306 -549
  7. package/dist/{chunk-MJWFCDBB.js → chunk-IOIAXKW2.js} +38 -16
  8. package/dist/{chunk-3TJVFKXV.js → chunk-QM5E5RJZ.js} +1 -1
  9. package/dist/chunk-QUGH2XA6.js +275 -0
  10. package/dist/cli-command-YQ5T4R73.js +526 -0
  11. package/dist/cli.js +30 -5
  12. package/dist/{config-QC5QSP3G.js → config-2PXUENQL.js} +1 -1
  13. package/dist/{daemon-PVYTXJMS.js → daemon-QBOXJFRA.js} +7 -6
  14. package/dist/daemon-handle-2GVZT55B.js +10 -0
  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-XUCQWX6N.js +0 -10
  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
@@ -26,6 +26,22 @@ npm install -g tokmon
26
26
 
27
27
  Then run `tokmon`. On first launch you'll pick which providers to track; press `q` to quit any time.
28
28
 
29
+ ## CLI Data Queries
30
+
31
+ Scripts and coding agents can query the same daemon directly without opening the interactive dashboard:
32
+
33
+ ```bash
34
+ tokmon usage # current-month usage by provider/account/model
35
+ tokmon usage --period week --provider codex
36
+ tokmon usage --model opus --json # stable machine-readable schema
37
+ tokmon usage --period all --json --compact
38
+ tokmon providers --json # accounts plus local config/auth/usage paths
39
+ tokmon snapshot --refresh # complete raw daemon snapshot
40
+ tokmon config path # tokmon config file location
41
+ ```
42
+
43
+ `usage` refreshes local history by default; use `--cached` for the fastest cached answer or `--refresh` to refresh billing as well. JSON reports include `schemaVersion`, exact provider/account source IDs, per-model token and cost fields, and a `sources` collection that maps every model row to its provider home and discovered local paths. Run `tokmon usage --help` or `tokmon providers --help` for every filter and example.
44
+
29
45
  ## Providers
30
46
 
31
47
  **Usage providers** — full cost & token history (Today / Week / Month, sparkline, per-model table):
@@ -60,7 +76,7 @@ A responsive grid of provider cards (or one card at a time — see **Dashboard l
60
76
  - **Today / This Week / This Month** — cost and token summaries
61
77
  - **Burn rate** — current $/hr
62
78
  - **Cache saved** — what caching has saved you
63
- - **Rate limits** — live utilization bars with reset countdowns
79
+ - **Rate limits** — live utilization bars with reset countdowns or exact reset dates
64
80
  - **Sparkline** — recent daily activity
65
81
 
66
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.
@@ -89,7 +105,7 @@ tokmon serve --port 8080
89
105
  tokmon serve --no-open # don't auto-open the browser
90
106
  ```
91
107
 
92
- 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.
93
109
 
94
110
  ### Overview
95
111
 
@@ -176,6 +192,8 @@ Press `s` to open.
176
192
  - **Dashboard layout** — `grid` (all providers at once) or `single` (one at a time)
177
193
  - **Default focus** — start on `all`, or remember your `last` focused account
178
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
179
197
 
180
198
  **Providers** — toggle each provider on or off.
181
199
 
@@ -186,6 +204,10 @@ Press `s` to open.
186
204
  ```
187
205
  tokmon [options] Launch the terminal dashboard
188
206
  tokmon serve [options] Launch the web dashboard (http://127.0.0.1:4317)
207
+ tokmon usage [options] Query usage by model (human or JSON)
208
+ tokmon providers [options] Show accounts and local provider paths
209
+ tokmon snapshot [options] Print the raw daemon snapshot as JSON
210
+ tokmon config [path] Print the tokmon config file location
189
211
 
190
212
  Options:
191
213
  -i, --interval <seconds> Refresh interval in seconds (default: from config, or 2)
@@ -233,7 +255,7 @@ tokmon runs a small local **daemon** that does all the data collection. The term
233
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.
234
256
 
235
257
  **Limits & billing**
236
- - 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.
237
259
 
238
260
  **Responsiveness**
239
261
  - Dashboard summaries and table history load independently and refresh on separate intervals, so the UI stays responsive even on large histories.
@@ -1,28 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- PROVIDERS,
4
- PROVIDER_ORDER,
5
- TOKMON_WS_METHODS,
6
- TOKMON_WS_PATH,
7
- TokmonRpcGroup,
3
+ createDaemonRpcClient
4
+ } from "./chunk-QUGH2XA6.js";
5
+ import {
8
6
  accountsByProvider,
9
7
  buildAccounts,
8
+ fetchPeak,
9
+ modelColor
10
+ } from "./chunk-45YYP2FJ.js";
11
+ import {
12
+ PROVIDERS,
13
+ PROVIDER_ORDER,
10
14
  coalesceTables,
11
15
  col,
12
16
  currency,
13
17
  detectProviders,
14
- fetchPeak,
15
- modelColor,
18
+ formatResetAt,
19
+ resetAt,
16
20
  resolveTimezone,
17
21
  shortDate,
18
22
  systemTimezone,
19
23
  time,
20
24
  tokens,
21
25
  withTimeout
22
- } from "./chunk-IELQ5EY3.js";
23
- import {
24
- glyphs
25
- } from "./chunk-RF4GGQGM.js";
26
+ } from "./chunk-INBZLHJQ.js";
26
27
  import {
27
28
  COLOR_PALETTE,
28
29
  DEFAULTS,
@@ -37,7 +38,10 @@ import {
37
38
  sanitizeTyped,
38
39
  saveConfigSync,
39
40
  snapshotCacheFile
40
- } from "./chunk-5CWOJMAH.js";
41
+ } from "./chunk-E2YXYU73.js";
42
+ import {
43
+ glyphs
44
+ } from "./chunk-RF4GGQGM.js";
41
45
 
42
46
  // src/bootstrap-ink.tsx
43
47
  import { render } from "ink";
@@ -159,27 +163,22 @@ var TabBar = memo(function TabBar2({ tabs, active, onSelect }) {
159
163
  " "
160
164
  ] }) }, t)) });
161
165
  });
162
- var PeakBadge = memo(function PeakBadge2({ peak }) {
166
+ var PeakBadge = memo(function PeakBadge2({ peak, resetDisplay = "relative", tz }) {
163
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);
164
169
  return /* @__PURE__ */ jsxs(Box, { children: [
165
170
  /* @__PURE__ */ jsxs(Text, { color, children: [
166
171
  glyphs().dot,
167
172
  " "
168
173
  ] }),
169
174
  /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
170
- peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
175
+ changesAt && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
171
176
  " (",
172
- fmtMinutes(peak.minutesUntilChange),
177
+ formatResetAt(changesAt, resetDisplay, Date.now(), tz),
173
178
  ")"
174
179
  ] })
175
180
  ] });
176
181
  });
177
- function fmtMinutes(mins) {
178
- if (mins < 60) return `${mins}m`;
179
- const h = Math.floor(mins / 60);
180
- const m = mins % 60;
181
- return m === 0 ? `${h}h` : `${h}h ${m}m`;
182
- }
183
182
  function currencySymbol(cur) {
184
183
  return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
185
184
  }
@@ -290,7 +289,7 @@ function computeDashLayout(groups, stats, cols, budget, focusId, layoutPref) {
290
289
  }
291
290
  return chooseLayout(content, budget, groups.length, single, cols, heights);
292
291
  }
293
- 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 }) {
294
293
  if (groups.length === 0) {
295
294
  return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
296
295
  "No providers enabled ",
@@ -305,7 +304,7 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
305
304
  const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
306
305
  const visible = pageCount > 1 ? groups.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : groups;
307
306
  return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
308
- /* @__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)) }),
309
308
  pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
310
309
  " ",
311
310
  glyphs().middot,
@@ -321,7 +320,7 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
321
320
  ] })
322
321
  ] });
323
322
  });
324
- function ProviderCard({ provider, accounts, stats, width, variant, privacyMode = false }) {
323
+ function ProviderCard({ provider, accounts, stats, width, variant, privacyMode = false, resetDisplay, tz }) {
325
324
  const meta = PROVIDERS[provider];
326
325
  const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
327
326
  const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
@@ -357,7 +356,7 @@ function ProviderCard({ provider, accounts, stats, width, variant, privacyMode =
357
356
  ] })),
358
357
  meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
359
358
  meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
360
- /* @__PURE__ */ jsx2(LimitsBlock, { items, inner, privacyMode })
359
+ /* @__PURE__ */ jsx2(LimitsBlock, { items, inner, privacyMode, resetDisplay, tz })
361
360
  ] }),
362
361
  meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items, privacyMode }),
363
362
  hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
@@ -430,11 +429,12 @@ function accountTitle(account, billing, privacyMode = false) {
430
429
  const title = email && !account.name.includes("@") ? `${account.name} ${email}` : account.name;
431
430
  return privacyMode ? redactEmail(title) : title;
432
431
  }
433
- function LimitsBlock({ items, inner, privacyMode }) {
432
+ function LimitsBlock({ items, inner, privacyMode, resetDisplay, tz }) {
434
433
  const showName = items.length > 1;
435
434
  const labels = items.flatMap((i) => i.s?.billing?.metrics ?? []).map((m) => m.label.length);
436
435
  const labelW = Math.min(Math.max(7, ...labels) + 1, 14);
437
- 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));
438
438
  return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
439
439
  const billing = s?.billing;
440
440
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
@@ -448,11 +448,11 @@ function LimitsBlock({ items, inner, privacyMode }) {
448
448
  !billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
449
449
  "Fetching",
450
450
  glyphs().ellipsis
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 }, `${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}`))
452
452
  ] }, account.id);
453
453
  }) });
454
454
  }
455
- function MetricRow({ m, color, barW, labelW }) {
455
+ function MetricRow({ m, color, barW, labelW, resetW, resetDisplay, tz }) {
456
456
  if (m.format.kind === "percent") {
457
457
  const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
458
458
  return /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -462,7 +462,7 @@ function MetricRow({ m, color, barW, labelW }) {
462
462
  Math.round(m.used),
463
463
  "%"
464
464
  ] }) }),
465
- /* @__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: " " }) })
466
466
  ] });
467
467
  }
468
468
  return /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -530,7 +530,7 @@ import { Box as Box4, Text as Text4 } from "ink";
530
530
  import { memo as memo3 } from "react";
531
531
  import { Box as Box3, Text as Text3 } from "ink";
532
532
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
533
- var GENERAL_ROWS = 9;
533
+ var GENERAL_ROWS = 10;
534
534
  var SETTINGS_TABS = ["general", "providers", "accounts"];
535
535
  var SETTINGS_TAB_LABELS = {
536
536
  general: "General",
@@ -626,7 +626,18 @@ var SettingsView = memo3(function SettingsView2({
626
626
  ] })
627
627
  ] }),
628
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" }) }),
629
- 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
+ ] })
630
641
  ] }),
631
642
  activeTab === "providers" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
632
643
  /* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Providers" }),
@@ -1607,273 +1618,6 @@ function TinyRow({ provider, accounts, stats, width }) {
1607
1618
 
1608
1619
  // src/client/use-daemon.ts
1609
1620
  import { useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState3 } from "react";
1610
-
1611
- // src/client/daemon-rpc-client.ts
1612
- import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
1613
- import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
1614
- import * as Socket from "effect/unstable/socket/Socket";
1615
- var TokmonRpcClient = class extends Context.Service()(
1616
- "tokmon/client/DaemonRpcClient/TokmonRpcClient"
1617
- ) {
1618
- };
1619
- var dynamicImport = new Function("specifier", "return import(specifier)");
1620
- function toWsUrl(baseUrl) {
1621
- const base = typeof window === "undefined" ? void 0 : window.location.origin;
1622
- const url = new URL(baseUrl, base);
1623
- if (url.protocol === "http:") url.protocol = "ws:";
1624
- else if (url.protocol === "https:") url.protocol = "wss:";
1625
- if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1626
- throw new Error(`unsupported daemon RPC protocol: ${url.protocol}`);
1627
- }
1628
- url.hash = "";
1629
- url.searchParams.delete("tokmonToken");
1630
- url.searchParams.delete("wsToken");
1631
- url.pathname = TOKMON_WS_PATH;
1632
- return url.toString();
1633
- }
1634
- function shouldUseNodeTransport(transport) {
1635
- if (transport === "node") return true;
1636
- if (transport === "browser") return false;
1637
- return typeof window === "undefined";
1638
- }
1639
- async function socketLayerFor(url, transport) {
1640
- if (shouldUseNodeTransport(transport)) {
1641
- const NodeSocket = await dynamicImport("@effect/platform-node/NodeSocket");
1642
- return NodeSocket.layerWebSocket(url);
1643
- }
1644
- return Socket.layerWebSocket(url).pipe(
1645
- Layer.provide(Socket.layerWebSocketConstructorGlobal)
1646
- );
1647
- }
1648
- function retryPolicy(options) {
1649
- const baseDelay = options.reconnectBaseDelayMs ?? 250;
1650
- const policy = Schedule.exponential(Duration.millis(baseDelay), 1.5).pipe(
1651
- Schedule.either(Schedule.spaced(Duration.millis(2500)))
1652
- );
1653
- return typeof options.reconnectAttempts === "number" ? policy.pipe(Schedule.both(Schedule.recurs(options.reconnectAttempts))) : policy;
1654
- }
1655
- function createDaemonRpcClient(baseUrl, options = {}) {
1656
- const url = toWsUrl(baseUrl);
1657
- const fibers = /* @__PURE__ */ new Set();
1658
- let session = null;
1659
- let sessionPromise = null;
1660
- let closed = false;
1661
- const setConn = (state, error) => {
1662
- options.onConn?.(state, error);
1663
- };
1664
- const resetSession = (active) => {
1665
- if (active !== void 0 && active !== null && active !== session) {
1666
- void active.runtime.dispose().catch(() => {
1667
- });
1668
- return;
1669
- }
1670
- const dead = active ?? session;
1671
- session = null;
1672
- sessionPromise = null;
1673
- if (dead) void dead.runtime.dispose().catch(() => {
1674
- });
1675
- };
1676
- const makeProtocolLayer = async () => {
1677
- const socketLayer = await socketLayerFor(url, options.transport);
1678
- const connectionHooksLayer = Layer.succeed(
1679
- RpcClient.ConnectionHooks,
1680
- RpcClient.ConnectionHooks.of({
1681
- onConnect: Effect.sync(() => {
1682
- setConn("live");
1683
- }),
1684
- onDisconnect: Effect.sync(() => {
1685
- if (!closed) {
1686
- setConn("reconnecting");
1687
- }
1688
- })
1689
- })
1690
- );
1691
- return Layer.effect(
1692
- RpcClient.Protocol,
1693
- RpcClient.makeProtocolSocket({
1694
- retryPolicy: retryPolicy(options),
1695
- retryTransientErrors: true
1696
- })
1697
- ).pipe(
1698
- Layer.provide(Layer.mergeAll(socketLayer, RpcSerialization.layerJson, connectionHooksLayer))
1699
- );
1700
- };
1701
- const ensureSession = async () => {
1702
- if (closed) throw new Error("daemon RPC client is closed");
1703
- if (session) return session;
1704
- if (sessionPromise) return sessionPromise;
1705
- setConn("connecting");
1706
- sessionPromise = (async () => {
1707
- let runtime;
1708
- try {
1709
- const protocolLayer = await makeProtocolLayer();
1710
- const clientLayer = Layer.effect(
1711
- TokmonRpcClient,
1712
- RpcClient.make(TokmonRpcGroup)
1713
- ).pipe(
1714
- Layer.provide(protocolLayer)
1715
- );
1716
- runtime = ManagedRuntime.make(clientLayer);
1717
- await runtime.runPromise(TokmonRpcClient.asEffect());
1718
- if (closed) {
1719
- await runtime.dispose();
1720
- throw new Error("daemon RPC client is closed");
1721
- }
1722
- session = { runtime };
1723
- return session;
1724
- } catch (error) {
1725
- sessionPromise = null;
1726
- await runtime?.dispose().catch(() => {
1727
- });
1728
- if (!closed) setConn("error", error);
1729
- throw error;
1730
- }
1731
- })();
1732
- return sessionPromise;
1733
- };
1734
- const run = async (effectFor) => {
1735
- const active = await ensureSession();
1736
- try {
1737
- return await active.runtime.runPromise(
1738
- TokmonRpcClient.use((client) => effectFor(client))
1739
- );
1740
- } catch (error) {
1741
- if (!closed) {
1742
- resetSession(active);
1743
- setConn("error", error);
1744
- }
1745
- throw error;
1746
- }
1747
- };
1748
- const subscribe = (streamFor, onValue, staleAfterFor) => {
1749
- if (closed) return () => {
1750
- };
1751
- let fiber = null;
1752
- let unsubscribed = false;
1753
- let retryTimer = null;
1754
- let watchdogTimer = null;
1755
- let lastValueAt = 0;
1756
- let staleAfterMs = Number.POSITIVE_INFINITY;
1757
- let watchdogRestart = false;
1758
- const stopFiber = () => {
1759
- if (!fiber) return;
1760
- const current = fiber;
1761
- fiber = null;
1762
- fibers.delete(current);
1763
- void (session?.runtime.runPromise(Fiber.interrupt(current)) ?? Effect.runPromise(Fiber.interrupt(current))).catch(() => {
1764
- });
1765
- };
1766
- const scheduleRetry = () => {
1767
- if (closed || unsubscribed || retryTimer) return;
1768
- retryTimer = setTimeout(() => {
1769
- retryTimer = null;
1770
- start();
1771
- }, options.reconnectBaseDelayMs ?? 250);
1772
- retryTimer.unref?.();
1773
- };
1774
- if (staleAfterFor) {
1775
- const checkEveryMs = Math.min(5e3, Math.max(10, (options.snapshotStaleFloorMs ?? 9e4) / 2));
1776
- watchdogTimer = setInterval(() => {
1777
- if (!fiber || lastValueAt === 0 || Date.now() - lastValueAt <= staleAfterMs) return;
1778
- watchdogRestart = true;
1779
- lastValueAt = Date.now();
1780
- setConn("reconnecting");
1781
- stopFiber();
1782
- }, checkEveryMs);
1783
- watchdogTimer.unref?.();
1784
- }
1785
- const start = () => {
1786
- void (async () => {
1787
- try {
1788
- const active = await ensureSession();
1789
- if (closed || unsubscribed) return;
1790
- lastValueAt = Date.now();
1791
- staleAfterMs = options.snapshotStaleFloorMs ?? 9e4;
1792
- const currentFiber = active.runtime.runFork(
1793
- TokmonRpcClient.use(
1794
- (client) => streamFor(client).pipe(
1795
- Stream.runForEach(
1796
- (value) => Effect.sync(() => {
1797
- lastValueAt = Date.now();
1798
- if (staleAfterFor) staleAfterMs = staleAfterFor(value);
1799
- try {
1800
- onValue(value);
1801
- } catch {
1802
- }
1803
- })
1804
- )
1805
- )
1806
- )
1807
- );
1808
- fiber = currentFiber;
1809
- fibers.add(currentFiber);
1810
- currentFiber.addObserver((exit) => {
1811
- fibers.delete(currentFiber);
1812
- if (fiber === currentFiber) fiber = null;
1813
- if (closed || unsubscribed) return;
1814
- const stale = watchdogRestart;
1815
- watchdogRestart = false;
1816
- resetSession(active);
1817
- if (stale || Exit.isSuccess(exit)) setConn("reconnecting");
1818
- else setConn("error", Cause.squash(exit.cause));
1819
- scheduleRetry();
1820
- });
1821
- } catch (error) {
1822
- if (!closed && !unsubscribed) {
1823
- resetSession();
1824
- setConn("error", error);
1825
- scheduleRetry();
1826
- }
1827
- }
1828
- })();
1829
- };
1830
- start();
1831
- return () => {
1832
- unsubscribed = true;
1833
- if (retryTimer) {
1834
- clearTimeout(retryTimer);
1835
- retryTimer = null;
1836
- }
1837
- if (watchdogTimer) {
1838
- clearInterval(watchdogTimer);
1839
- watchdogTimer = null;
1840
- }
1841
- stopFiber();
1842
- };
1843
- };
1844
- return {
1845
- getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})).then((state) => state),
1846
- setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)).then((state) => state),
1847
- refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
1848
- browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
1849
- subscribeSnapshot: (onSnapshot) => subscribe(
1850
- (client) => client[TOKMON_WS_METHODS.snapshot]({}).pipe(Stream.map((value) => value)),
1851
- onSnapshot,
1852
- (snapshot) => Math.max(options.snapshotStaleFloorMs ?? 9e4, snapshot.intervalMs * 3)
1853
- ),
1854
- subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}).pipe(Stream.map((value) => value)), onConfig),
1855
- async close() {
1856
- if (closed) return;
1857
- closed = true;
1858
- setConn("closed");
1859
- const active = [...fibers];
1860
- fibers.clear();
1861
- const activeSession = session ?? await sessionPromise?.catch(() => null) ?? null;
1862
- await Promise.all(active.map(
1863
- (fiber) => (activeSession?.runtime.runPromise(Fiber.interrupt(fiber)) ?? Effect.runPromise(Fiber.interrupt(fiber))).catch(() => {
1864
- })
1865
- ));
1866
- session = null;
1867
- sessionPromise = null;
1868
- if (activeSession) {
1869
- await activeSession.runtime.dispose().catch(() => {
1870
- });
1871
- }
1872
- }
1873
- };
1874
- }
1875
-
1876
- // src/client/use-daemon.ts
1877
1621
  function useDaemon(baseUrl) {
1878
1622
  const [snapshot, setSnapshot] = useState3(null);
1879
1623
  const [conn, setConn] = useState3("connecting");
@@ -2388,6 +2132,10 @@ function handleSettings(input, key, ctx) {
2388
2132
  }
2389
2133
  if (index === 8 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2390
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" }));
2391
2139
  }
2392
2140
  }
2393
2141
  }
@@ -2543,10 +2291,9 @@ function terminalFocusEvent(input) {
2543
2291
  if (input === "[O") return "out";
2544
2292
  return null;
2545
2293
  }
2546
- function handleTerminalFocusInput(input, onFocusIn) {
2294
+ function handleTerminalFocusInput(input) {
2547
2295
  const event = terminalFocusEvent(input);
2548
2296
  if (!event) return false;
2549
- if (event === "in") onFocusIn();
2550
2297
  return true;
2551
2298
  }
2552
2299
  function isRefreshAllShortcut(input, mode) {
@@ -3654,7 +3401,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3654
3401
  if (webStartingRef.current) return;
3655
3402
  webStartingRef.current = true;
3656
3403
  try {
3657
- const { startWebServer } = await import("./server-XUCQWX6N.js");
3404
+ const { startWebServer } = await import("./server-23ERUOJ7.js");
3658
3405
  const ctrl = await startWebServer({ config: cfg, log: false });
3659
3406
  webRef.current = ctrl;
3660
3407
  openUrl(ctrl.url);
@@ -3701,17 +3448,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3701
3448
  () => tab === 1 ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
3702
3449
  [tab, rawTokenRows, activeTableModel, search, sort]
3703
3450
  );
3704
- const lastFocusRefreshRef = useRef8(Date.now());
3705
- const refreshOnFocus = useCallback7(() => {
3706
- const now = Date.now();
3707
- if (now - lastFocusRefreshRef.current < 3e4) return;
3708
- lastFocusRefreshRef.current = now;
3709
- const task = connected ? daemonRefreshRef.current("billing") : degradedRefreshRef.current();
3710
- void task.catch(() => {
3711
- });
3712
- }, [connected]);
3713
3451
  useInput((input, key) => {
3714
- if (handleTerminalFocusInput(input, refreshOnFocus)) return;
3452
+ if (handleTerminalFocusInput(input)) return;
3715
3453
  handleKey(input, key, {
3716
3454
  onboarding: {
3717
3455
  show: showPicker,
@@ -3833,7 +3571,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3833
3571
  ] }),
3834
3572
  /* @__PURE__ */ jsxs12(Box11, { children: [
3835
3573
  peak && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3836
- /* @__PURE__ */ jsx12(PeakBadge, { peak }),
3574
+ /* @__PURE__ */ jsx12(PeakBadge, { peak, resetDisplay: cfg.resetDisplay, tz }),
3837
3575
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3838
3576
  " ",
3839
3577
  glyphs().middot,
@@ -3892,7 +3630,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3892
3630
  ] })
3893
3631
  ] }),
3894
3632
  tab === 0 && /* @__PURE__ */ jsxs12(Fragment4, { children: [
3895
- /* @__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 }),
3896
3634
  slots.length > 1 && /* @__PURE__ */ jsxs12(Box11, { marginTop: 1, children: [
3897
3635
  /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "focus " }),
3898
3636
  /* @__PURE__ */ jsx12(