ccdeck 3.36.9 → 3.37.1

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 (75) hide show
  1. package/README.md +15 -7
  2. package/bin/agent-dag.js +19 -6
  3. package/bin/cli/help.js +1 -0
  4. package/bin/cli/login-item.js +44 -20
  5. package/bin/cli/one-shot.js +15 -5
  6. package/bin/cli/screen.js +10 -5
  7. package/bin/cli/startup.js +6 -2
  8. package/bin/cli/uninstall.js +41 -5
  9. package/bin/deck.js +50 -8
  10. package/dist/web/assets/BrowserWatchModal-njN0zg93.js +1 -0
  11. package/dist/web/assets/UsageHistoryModal-DYSZZZEz.js +1 -0
  12. package/dist/web/assets/index-B0SOa-XB.css +1 -0
  13. package/dist/web/assets/index-DDxnWaTa.js +306 -0
  14. package/dist/web/index.html +2 -2
  15. package/hook/hook.js +80 -9
  16. package/package.json +1 -1
  17. package/release-notes.json +8 -1
  18. package/src/server/account-origins.mjs +8 -1
  19. package/src/server/account-projects.mjs +34 -9
  20. package/src/server/activation.mjs +39 -10
  21. package/src/server/app-host.mjs +7 -2
  22. package/src/server/args.mjs +3 -0
  23. package/src/server/browser-watch-routes.mjs +23 -16
  24. package/src/server/browser-watch.mjs +9 -3
  25. package/src/server/claude-accounts.mjs +27 -7
  26. package/src/server/claude-verdicts.mjs +12 -1
  27. package/src/server/cli-versions.mjs +6 -4
  28. package/src/server/codex-auth.mjs +29 -0
  29. package/src/server/codex-quota.mjs +57 -7
  30. package/src/server/deck-home.mjs +23 -1
  31. package/src/server/deck-prefs.mjs +70 -7
  32. package/src/server/depth-facts.mjs +178 -0
  33. package/src/server/detach.mjs +61 -33
  34. package/src/server/event-log.mjs +15 -0
  35. package/src/server/event-pipeline.mjs +5 -2
  36. package/src/server/event-routes.mjs +22 -0
  37. package/src/server/feedback-form.mjs +36 -13
  38. package/src/server/index.mjs +6 -2
  39. package/src/server/install-layout.mjs +35 -0
  40. package/src/server/install-ref.mjs +39 -0
  41. package/src/server/installer.mjs +20 -2
  42. package/src/server/lan-beacon.mjs +18 -3
  43. package/src/server/lan-deck.mjs +7 -1
  44. package/src/server/lan-engine.mjs +40 -21
  45. package/src/server/lan-manifest.mjs +9 -6
  46. package/src/server/lan-requests.mjs +18 -2
  47. package/src/server/lan-serve.mjs +4 -2
  48. package/src/server/lan-sync.mjs +97 -6
  49. package/src/server/lifecycle.mjs +18 -2
  50. package/src/server/log-replay.mjs +9 -3
  51. package/src/server/login-service.mjs +95 -12
  52. package/src/server/npm-upgrade.mjs +12 -4
  53. package/src/server/pinned-build.mjs +3 -0
  54. package/src/server/prefs-routes.mjs +5 -8
  55. package/src/server/prefs-state.mjs +9 -1
  56. package/src/server/quota-oauth.mjs +88 -13
  57. package/src/server/quota-shape.mjs +28 -0
  58. package/src/server/quota.mjs +68 -17
  59. package/src/server/rating.mjs +87 -0
  60. package/src/server/reports-routes.mjs +21 -2
  61. package/src/server/reports.mjs +672 -64
  62. package/src/server/ring-bounds.mjs +11 -0
  63. package/src/server/running-deck.mjs +48 -0
  64. package/src/server/self-update.mjs +1 -1
  65. package/src/server/session-enrichment.mjs +14 -6
  66. package/src/server/stable-node.mjs +68 -0
  67. package/src/server/static-serve.mjs +5 -0
  68. package/src/server/supervisor.mjs +21 -15
  69. package/src/server/tailscale.mjs +8 -4
  70. package/src/server/transcript-scan.mjs +7 -4
  71. package/src/server/usage-day.mjs +76 -11
  72. package/dist/web/assets/BrowserWatchModal-BCrDvuug.js +0 -1
  73. package/dist/web/assets/UsageHistoryModal-BtRCuo33.js +0 -1
  74. package/dist/web/assets/index-BkXTN4dZ.css +0 -1
  75. package/dist/web/assets/index-tVBMXvDv.js +0 -306
@@ -86,6 +86,17 @@ export function isEnrichment(raw) {
86
86
  return raw != null && typeof raw === "object" && LAST_VALUE_WINS.has(raw.hook_event_name);
87
87
  }
88
88
 
89
+ /**
90
+ * Whether an event name is one of the deck's own control markers — `__clear`,
91
+ * which handleClear sends, is the one in use — which only the server may send.
92
+ * Here beside isEnrichment for the same reason: the ingest that refuses one
93
+ * from a hook and the boot replay that honours one only from the server ask
94
+ * the same question.
95
+ */
96
+ export function isReservedEventName(name) {
97
+ return typeof name === "string" && name.startsWith("__");
98
+ }
99
+
89
100
  /**
90
101
  * The ring's array-length backstop, distinct from its event budget.
91
102
  *
@@ -58,10 +58,13 @@
58
58
  // that imports two node builtins; index.mjs takes them from the same place and
59
59
  // re-exports them, so there is still exactly one spelling in the package.
60
60
  import { readdir, readFile, stat } from "node:fs/promises";
61
+ import { uptime } from "node:os";
61
62
  import { join } from "node:path";
62
63
  import { claudeConfigDir } from "./claude-dir.mjs";
63
64
  import { challengeDeck, isProcessAlive } from "./deck-probe.mjs";
64
65
  import { sameCodexTree } from "./log-election.mjs";
66
+ import { run } from "./metrics-run.mjs";
67
+ import { elapsedSeconds } from "./process-list.mjs";
65
68
  import { CRASH_WINDOW_MS } from "./supervisor.mjs";
66
69
 
67
70
  /**
@@ -264,6 +267,16 @@ export async function liveDecks({
264
267
  * recycled pid than a supervisor, and a signal sent there ends a stranger.
265
268
  * The window is the supervisor's own, CRASH_WINDOW_MS.
266
269
  *
270
+ * AND FRESH IS NOT ENOUGH ON ITS OWN. A deck that dies without its shutdown —
271
+ * a power cut, a hard reset, a console closed on Windows — leaves a record
272
+ * exactly as fresh, and after a quick reboot, or on a system that reuses pids
273
+ * quickly, its `parent` names whatever holds that number now. So the parent
274
+ * has to prove it is the supervisor by when it started: no later than the
275
+ * worker it supervises, whose record carries `startedAt`. A record stamped
276
+ * before this boot names no process that is running, and is never read. A
277
+ * parent whose start cannot be read is left alone; the cost is the behaviour
278
+ * before #1779, a `--stop` that misses a deck about to come back.
279
+ *
267
280
  * One entry per supervisor, and none for a supervisor that already has a live
268
281
  * worker registered: that deck is on the ordinary list, and stopping it ends
269
282
  * the supervisor too. Never this process or its own supervisor. Marked
@@ -277,6 +290,8 @@ export async function restartingDecks({
277
290
  alive = isProcessAlive,
278
291
  now = Date.now(),
279
292
  windowMs = CRASH_WINDOW_MS,
293
+ bootedAt = now - uptime() * 1000,
294
+ startedAt = processStartedAt,
280
295
  } = {}) {
281
296
  let names;
282
297
  try { names = await fs.readdir(dir); } catch { return []; }
@@ -300,13 +315,46 @@ export async function restartingDecks({
300
315
  if (up || !Number.isInteger(parent) || parent <= 0) continue;
301
316
  if (parent === self || parent === selfParent || serving.has(parent)) continue;
302
317
  if (!(Number.isFinite(mtime) && now - mtime >= 0 && now - mtime < windowMs)) continue;
318
+ if (mtime < bootedAt) continue;
303
319
  if (!alive(parent)) continue;
320
+ const workerSince = Date.parse(d.startedAt ?? "");
321
+ if (!Number.isFinite(workerSince)) continue;
322
+ const parentSince = await startedAt(parent);
323
+ if (!Number.isFinite(parentSince) || parentSince > workerSince + START_SLACK_MS) continue;
304
324
  serving.add(parent);
305
325
  out.push({ ...d, restarting: true });
306
326
  }
307
327
  return out.sort((a, b) => a.port - b.port || a.pid - b.pid);
308
328
  }
309
329
 
330
+ /** How much later than its worker a supervisor may seem to have started. `ps`
331
+ * reads elapsed time in whole seconds, so a start read that way can come out
332
+ * up to a second late, and the read itself takes a moment. */
333
+ const START_SLACK_MS = 2_000;
334
+
335
+ /**
336
+ * When `pid` started, in ms since the epoch, or null when that cannot be read.
337
+ *
338
+ * `ps -o etime=` on both Unixes — the POSIX field, `[[DD-]HH:]MM:SS` — and
339
+ * Get-Process on Windows. Spawned only for a crashed worker's live parent,
340
+ * which `--stop` and `--status` meet a few times in a deck's life, so the cost
341
+ * of a child process is paid there and nowhere else. A pid that is gone, or
342
+ * that this account may not read, answers null.
343
+ */
344
+ export async function processStartedAt(pid, { platform = process.platform, runFn = run, now = Date.now } = {}) {
345
+ if (!Number.isInteger(pid) || pid <= 0) return null;
346
+ if (platform === "win32") {
347
+ const out = await runFn("powershell.exe", [
348
+ "-NoProfile", "-NonInteractive", "-Command",
349
+ `([DateTimeOffset](Get-Process -Id ${pid} -ErrorAction Stop).StartTime).ToUnixTimeMilliseconds()`,
350
+ ], 6_000);
351
+ const ms = Number(String(out ?? "").trim());
352
+ return out && Number.isFinite(ms) && ms > 0 ? ms : null;
353
+ }
354
+ const secs = elapsedSeconds(await runFn("ps", ["-o", "etime=", "-p", String(pid)], 2_000));
355
+ return secs == null ? null : now() - secs * 1000;
356
+ }
357
+
310
358
  /**
311
359
  * What to say about the version of the deck we attached to.
312
360
  *
@@ -32,7 +32,7 @@ import {
32
32
  installedVersion, PUBLISHED_NAME, registryName, upgradeCommand, upgradeName,
33
33
  } from "./install-layout.mjs";
34
34
  export {
35
- installedVersion, isNpxInstall, isGitCheckout, npxRoot, bareSpecName, npxSpecFromMeta,
35
+ installedVersion, isNpxInstall, isOneOffRun, oneOffRunner, isGitCheckout, npxRoot, bareSpecName, npxSpecFromMeta,
36
36
  npxRestartSpec, ALIAS_PACKAGES, PUBLISHED_NAME, RETIRED_NAMES, currentName, installedName,
37
37
  hostRoot, hostNameFromMeta, hostPackage, successorRoot, frozenNameInstall, upgradeName,
38
38
  upgradeCommand, registryName,
@@ -58,7 +58,7 @@ export function transcriptTarget(payload) {
58
58
  // agents created before the model was resolved. The cache is the emit
59
59
  // filter, not a read filter: reading once per session meant a subagent
60
60
  // model that only appears after the root is known was never picked up.
61
- const modelBySession = new Map(); // sessionId -> { rootModel, subsSig }
61
+ const modelBySession = new Map(); // sessionId -> { rootModel, subsSig, subagentModels }
62
62
  const MODEL_READ_THROTTLE_MS = 2500;
63
63
  const modelReads = sessionReadGate(MODEL_READ_THROTTLE_MS);
64
64
 
@@ -71,10 +71,18 @@ export function cachedModelId(cached) {
71
71
  return typeof rootModel === "string" && rootModel ? rootModel : null;
72
72
  }
73
73
 
74
- /** The root model already resolved for this session, or null — what pushEvent
75
- * stamps on a payload that arrives without one. */
76
- function knownModelId(sid) {
77
- return cachedModelId(modelBySession.get(sid));
74
+ /** The model already resolved for whoever this payload is about, or null —
75
+ * what pushEvent stamps on a payload that arrives without one. A payload that
76
+ * names a subagent (`agent_id`, or the legacy `parent_tool_use_id`) is that
77
+ * subagent's own traffic and gets its model from the transcript scan's map,
78
+ * or nothing: the root's model on it overwrote the subagent's on its card,
79
+ * and nothing ever sent the right one again. */
80
+ function knownModelId(sid, subagentKey = null) {
81
+ const cached = modelBySession.get(sid);
82
+ if (!subagentKey) return cachedModelId(cached);
83
+ const subs = cached?.subagentModels;
84
+ const model = subs && Object.hasOwn(subs, subagentKey) ? subs[subagentKey] : null;
85
+ return typeof model === "string" && model ? model : null;
78
86
  }
79
87
 
80
88
  /** Read the main session JSONL. Returns the root model and any
@@ -246,7 +254,7 @@ function maybeResolveModel(payload) {
246
254
  const prev = modelBySession.get(sid);
247
255
  const subsSig = JSON.stringify(subagentModels);
248
256
  if (prev && prev.rootModel === rootModel && prev.subsSig === subsSig) return;
249
- modelBySession.set(sid, { rootModel, subsSig });
257
+ modelBySession.set(sid, { rootModel, subsSig, subagentModels });
250
258
  pushEvent({
251
259
  hook_event_name: "ModelObserved",
252
260
  session_id: sid,
@@ -0,0 +1,68 @@
1
+ // The spelling of the running node that outlives an upgrade of the same install.
2
+ //
3
+ // Two things this deck writes name a node and are read long after it wrote
4
+ // them: the Claude Code hook command in settings.json, run on every tool call
5
+ // of every session, and the login item, run at every login. Both used
6
+ // `process.execPath`, and Node resolves symbolic links in it — on Linux it is
7
+ // /proc/self/exe, on macOS the realpath of the executable — so the path written
8
+ // is the one an upgrade deletes:
9
+ //
10
+ // Homebrew /opt/homebrew/Cellar/node/24.1.0/bin/node (`brew upgrade` removes the old keg)
11
+ // tarball ~/.local/share/node/node-v24.21.0-linux-x64/bin/node (behind ~/.local/bin/node)
12
+ //
13
+ // After that `sh -c` exits 127 on all ten hook events, Claude Code shows
14
+ // `<Event> hook error` on every tool call and the deck receives nothing — and
15
+ // the login item, naming the same binary, cannot start the deck whose next boot
16
+ // would have rewritten the entry.
17
+ //
18
+ // So the path is asked for the way the user reaches node: the first PATH entry
19
+ // whose real path IS this binary (/opt/homebrew/bin/node, ~/.local/bin/node,
20
+ // nodejs\ under nvm-windows), and failing that Homebrew's own stable link for a
21
+ // keg, <prefix>/opt/<formula>/bin/node. Only a spelling that resolves to this
22
+ // very file is taken — a different node earlier on PATH is a different
23
+ // program, with its own version and its own globals. Nothing found, and the
24
+ // running binary is the answer, as it always was: nvm, fnm and Volta have no
25
+ // such link, and a machine with nothing better loses nothing. For the hook,
26
+ // the command itself falls back to the node on PATH once that binary is gone
27
+ // (hookCommand in installer.mjs).
28
+ import { realpathSync } from "node:fs";
29
+ import { posix as posixPath, win32 as winPath } from "node:path";
30
+
31
+ /** Homebrew's keg path, mapped to the link it keeps pointing at the current
32
+ * keg of the same formula. Both separators, so the rule reads the same on any
33
+ * leg of the suite; null for anything that is not a keg. */
34
+ function brewOptPath(real) {
35
+ const m = /^(.*)([\\/])Cellar\2([^\\/]+)\2[^\\/]+\2bin\2node$/.exec(String(real));
36
+ return m ? [m[1], "opt", m[3], "bin", "node"].join(m[2]) : null;
37
+ }
38
+
39
+ export function stableNodePath({
40
+ execPath = process.execPath,
41
+ env = process.env,
42
+ platform = process.platform,
43
+ realpath = realpathSync,
44
+ } = {}) {
45
+ const win = platform === "win32";
46
+ // Windows paths compare without case; everywhere else a byte is a byte.
47
+ const key = (p) => (win ? p.toLowerCase() : p);
48
+ const resolved = (p) => { try { return key(realpath(p)); } catch { return null; } };
49
+ let own;
50
+ try { own = realpath(execPath); } catch { return execPath; }
51
+ const real = key(own);
52
+ const path = win ? winPath : posixPath;
53
+ const pathVar = env?.PATH ?? env?.Path ?? "";
54
+ for (const dir of String(pathVar).split(win ? ";" : ":")) {
55
+ // A relative entry would be resolved by Claude Code against each session's
56
+ // own cwd, and found in one directory and not in the next.
57
+ if (!dir || !path.isAbsolute(dir)) continue;
58
+ const candidate = path.join(dir, win ? "node.exe" : "node");
59
+ // The versioned directory itself on PATH is no more stable than execPath;
60
+ // a link later on PATH may still be.
61
+ if (key(candidate) !== real && resolved(candidate) === real) return candidate;
62
+ }
63
+ // From the path as given when it is a keg itself, so the prefix keeps the
64
+ // spelling it came with (/var rather than /private/var on a Mac).
65
+ const opt = brewOptPath(execPath) ?? brewOptPath(own);
66
+ if (opt && resolved(opt) === real) return opt;
67
+ return execPath;
68
+ }
@@ -62,6 +62,11 @@ async function serveStatic(req, res, url) {
62
62
  });
63
63
  res.end(body);
64
64
  } catch {
65
+ // Nothing under assets/ is a client-side route: a name there that is not
66
+ // on disk is a bundle from before an upgrade asking for a chunk the new
67
+ // build no longer has. index.html in its place is a 200 the browser cannot
68
+ // run as a module, so it is a 404, which says what happened.
69
+ if (rel.startsWith("assets/")) return send(res, 404, { error: "not found" });
65
70
  // SPA fallback to index.html for client-side routes
66
71
  try {
67
72
  const idx = await readFile(join(WEB_DIST, "index.html"));
@@ -202,9 +202,8 @@ export function signalExitAction(signal, platform = process.platform, numbers =
202
202
  * The re-raise is the obvious half and on its own it does not work, which is
203
203
  * the reason this is a function rather than one line at the call site. The
204
204
  * supervisor traps SIGINT, SIGTERM and SIGHUP — and those are exactly the
205
- * signals a worker is most likely to die of, since deck.js does not handle
206
- * SIGHUP at all and registers the other two only after a boot that takes
207
- * seconds. For all three the re-raise lands back in our own handler, which
205
+ * signals a worker is most likely to die of, since deck.js answers all three
206
+ * with its shutdown only after a boot that takes seconds. For all three the re-raise lands back in our own handler, which
208
207
  * finds no child left and exits 0: `kill -HUP` on the deck was reported to the
209
208
  * shell as a clean, successful stop. Dropping the handler first restores the
210
209
  * default action, which is to die of the signal.
@@ -363,26 +362,33 @@ export function withoutPortAndOpen(args) {
363
362
  return out;
364
363
  }
365
364
 
365
+ /** `argv` with `ours` added at the end of its options: before the first bare
366
+ * `--`, which parseArgs reads as the end of options and drops everything
367
+ * after. Appended after it, the supervisor's own flags were never read. */
368
+ function withOptions(argv, ours) {
369
+ const end = argv.indexOf("--");
370
+ return end === -1 ? [...argv, ...ours] : [...argv.slice(0, end), ...ours, ...argv.slice(end)];
371
+ }
372
+
366
373
  /** The worker's command line: the script and the user's own argv, and on a
367
- * respawn the port the last worker actually bound. Appended last so it wins:
368
- * the worker's parser keeps the final --port. */
374
+ * respawn the port the last worker actually bound. Last among the options so
375
+ * it wins: the worker's parser keeps the final --port. */
369
376
  export function workerArgs(worker, argv, { respawn = false, boundPort = null } = {}) {
370
- const args = [worker, ...argv];
371
- if (respawn && boundPort != null) args.push("--port", String(boundPort));
372
- return args;
377
+ const ours = respawn && boundPort != null ? ["--port", String(boundPort)] : [];
378
+ return [worker, ...withOptions(argv, ours)];
373
379
  }
374
380
 
375
381
  /** npx's command line for an upgrade: the spec, then the user's argv with the
376
- * two flags this sets itself taken out. Ours are appended, so the originals
377
- * are dropped rather than left to be overridden — `--port 4317 --no-open
378
- * --port 4317 --no-open` works, but it is what the next person reads in `ps`. */
382
+ * two flags this sets itself taken out. Ours are added after the rest of the
383
+ * options, so the originals are dropped rather than left to be overridden —
384
+ * `--port 4317 --no-open --port 4317 --no-open` works, but it is what the next
385
+ * person reads in `ps`. */
379
386
  export function npxRelaunchArgs(spec, argv, boundPort = null) {
380
- const args = ["-y", spec, ...withoutPortAndOpen(argv)];
381
- if (boundPort != null) args.push("--port", String(boundPort));
387
+ const ours = boundPort != null ? ["--port", String(boundPort)] : [];
382
388
  // The tab that asked for this is open and reconnecting; a second one would be
383
389
  // the deck talking over itself.
384
- args.push("--no-open");
385
- return args;
390
+ ours.push("--no-open");
391
+ return ["-y", spec, ...withOptions(withoutPortAndOpen(argv), ours)];
386
392
  }
387
393
 
388
394
  /**
@@ -179,9 +179,11 @@ export function beaconTargets(tailnet) {
179
179
  return out;
180
180
  }
181
181
 
182
- /** 100.64.0.0/10, the block Tailscale draws every IPv4 node address from. */
183
- function inTailnetBlock(addr) {
184
- const m = /^100\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(addr);
182
+ /** 100.64.0.0/10, the block Tailscale draws every IPv4 node address from.
183
+ * Exported for the accept switches — see askToAccept in lan-requests.mjs,
184
+ * which asks it of an address whose route this file could not answer. */
185
+ export function inTailnetBlock(addr) {
186
+ const m = /^100\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(String(addr ?? "").replace(/^::ffff:/, ""));
185
187
  return !!m && Number(m[1]) >= 64 && Number(m[1]) <= 127;
186
188
  }
187
189
 
@@ -190,7 +192,9 @@ function inTailnetBlock(addr) {
190
192
  *
191
193
  * Null means the local network, which is every answer this deck gave before
192
194
  * Tailscale was read — and still the answer on a machine where it is not
193
- * running.
195
+ * running. It is an answer about routing, not about trust: the local accept
196
+ * switch still waits for a press on an address in Tailscale's block that this
197
+ * could not place — see askToAccept in lan-requests.mjs.
194
198
  *
195
199
  * WHAT MAKES THE ANSWER SAFE TO ACT ON is Tailscale, not the packet: WireGuard
196
200
  * binds each node address to that node's key, and tailscaled drops a packet
@@ -654,10 +654,13 @@ function scanTranscript(path) {
654
654
  });
655
655
  }
656
656
 
657
- // What session-enrichment.mjs calls, and the two rules the Projects report
658
- // (account-projects.mjs) reads its own pass of the transcripts by. Listed here
659
- // rather than marked at each declaration so that every declaration above reads
660
- // exactly as it did where it came from.
657
+ // What session-enrichment.mjs calls, and the rules the Projects report
658
+ // (account-projects.mjs) reads its own pass of the transcripts by: which lines
659
+ // billed anything, and the speed each was billed at with the cap on how many
660
+ // speeds one model keeps apart. Listed here rather than marked at each
661
+ // declaration so that every declaration above reads exactly as it did where it
662
+ // came from.
661
663
  export {
662
664
  scanTranscript, newUsageTotals, hasSpend, mergeUsageByModel, copyUsageBucket, repeatsRequestUsage,
665
+ billedSpeed, MAX_SPEEDS_PER_MODEL, UNRECOGNISED_SPEED,
663
666
  };
@@ -1,6 +1,8 @@
1
1
  // How much this deck was used on one UTC day: the distinct sessions, subagents
2
- // and projects its hooks heard from. The "active" report carries the totals of
3
- // the last finished day (reports.mjs).
2
+ // and projects its hooks heard from — and how hard the deck worked for it: how
3
+ // many live events it took in, and its peak memory. The "active" report carries
4
+ // the totals of the last finished day (reports.mjs), the last two as buckets
5
+ // (depth-facts.mjs).
4
6
  //
5
7
  // WHY A DAY AND NOT A SNAPSHOT. The counts used to be read off the live state at
6
8
  // the moment the "active" went out — and that moment is the first check-in of
@@ -12,15 +14,20 @@
12
14
  // WHAT IS KEPT. In memory, the session ids, the subagent ids and the folders the
13
15
  // transcripts sit in, so a session heard from a hundred times counts once. They
14
16
  // never leave this module: what is saved between runs (prefs.json, beside the
15
- // install id) and what is sent are three counts, the day they belong to, and
16
- // the names of the features used that day — fixed tokens off FEATURES, a yes
17
- // for each, never what was done with them.
17
+ // install id) and what is sent are counts, the day they belong to, and the
18
+ // names of the features used that day — fixed tokens off FEATURES, a yes for
19
+ // each, never what was done with them.
18
20
  //
19
21
  // A RESTART MID-DAY. The saved counts come back as a floor and the new run
20
22
  // counts on top of them, so a session that was running across the restart is
21
23
  // counted twice. That is the price of keeping no ids on disk, and a restart is
22
24
  // rare next to the day's sessions.
23
25
  //
26
+ // DAYS USED. Beside the day, a running count of the days the deck heard any
27
+ // session at all, and the last of them so a restart does not count a day twice
28
+ // — a count and one date, never the list. It is what decides when the deck has
29
+ // been in use long enough to ask how useful it is (rating.mjs).
30
+ //
24
31
  // Only live use counts: a Claude hook fires because somebody is running the
25
32
  // CLI, and the Codex watcher emits only what a rollout gains while the deck
26
33
  // watches (its first scan skips every file's history). Replayed lines and the
@@ -66,22 +73,29 @@ function whole(n) {
66
73
  return Number.isInteger(n) && n >= 0 ? n : 0;
67
74
  }
68
75
 
69
- /** Saved totals, coerced: a day, three counts and the features used, or null for anything else. */
76
+ /** Saved totals, coerced: a day, its counts and the features used, or null for anything else.
77
+ * Events a save does not say — 3.36.x counted none — stay null, not 0: that
78
+ * day was not counted in full, and says nothing about them. */
70
79
  function savedTotals(raw) {
71
80
  if (!raw || typeof raw !== "object" || typeof raw.day !== "string" || !DAY.test(raw.day)) return null;
72
81
  return {
73
82
  day: raw.day, sessions: whole(raw.sessions), subagents: whole(raw.subagents), projects: whole(raw.projects),
74
83
  features: knownFeatures(raw.features),
84
+ events: Number.isInteger(raw.events) && raw.events >= 0 ? raw.events : null,
85
+ peakMb: whole(raw.peakMb),
75
86
  };
76
87
  }
77
88
 
78
89
  /** What prefs.json keeps for this, coerced — deck-prefs.mjs normalises with it. */
79
90
  export function normaliseUsage(raw) {
80
91
  const src = raw && typeof raw === "object" ? raw : {};
92
+ const day = v => (typeof v === "string" && DAY.test(v) ? v : "");
81
93
  return {
82
94
  current: savedTotals(src.current),
83
95
  done: savedTotals(src.done),
84
- sent: typeof src.sent === "string" && DAY.test(src.sent) ? src.sent : "",
96
+ sent: day(src.sent),
97
+ daysUsed: whole(src.daysUsed),
98
+ lastUsedDay: day(src.lastUsedDay),
85
99
  };
86
100
  }
87
101
 
@@ -105,6 +119,12 @@ export function createUsageDay({ now = () => new Date() } = {}) {
105
119
  let sent = "";
106
120
  /** Moves on every change, so the reporter saves only when there is something new. */
107
121
  let changes = 0;
122
+ /** The days used before this run, as an earlier run saved them: a count and
123
+ * the last of them. */
124
+ let usedBefore = 0;
125
+ let lastUsedBefore = "";
126
+ /** The days this run heard a session on — one or two, a deck past midnight. */
127
+ const usedThisRun = new Set();
108
128
  /** The first use this run heard, `{ at, provider }`, for activation.mjs. */
109
129
  let first = null;
110
130
  const firstListeners = [];
@@ -112,11 +132,13 @@ export function createUsageDay({ now = () => new Date() } = {}) {
112
132
  function fresh(day, floor = null) {
113
133
  return {
114
134
  day,
115
- floor: floor ?? { sessions: 0, subagents: 0, projects: 0, features: [] },
135
+ floor: floor ?? { sessions: 0, subagents: 0, projects: 0, features: [], events: 0, peakMb: 0 },
116
136
  sessions: new Set(),
117
137
  subagents: new Set(),
118
138
  projects: new Set(),
119
139
  features: new Set(),
140
+ events: 0,
141
+ peakMb: 0,
120
142
  };
121
143
  }
122
144
 
@@ -127,6 +149,10 @@ export function createUsageDay({ now = () => new Date() } = {}) {
127
149
  subagents: c.floor.subagents + c.subagents.size,
128
150
  projects: c.floor.projects + c.projects.size,
129
151
  features: knownFeatures([...c.floor.features, ...c.features]),
152
+ // A floor with no events count makes the day's unknown too: this run's
153
+ // alone would undercount it.
154
+ events: c.floor.events === null ? null : c.floor.events + c.events,
155
+ peakMb: Math.max(c.floor.peakMb, c.peakMb),
130
156
  };
131
157
  }
132
158
 
@@ -151,6 +177,9 @@ export function createUsageDay({ now = () => new Date() } = {}) {
151
177
  const sid = raw?.session_id;
152
178
  if (!sid || typeof sid !== "string") return;
153
179
  const c = today();
180
+ c.events++;
181
+ changes++;
182
+ add(usedThisRun, c.day);
154
183
  add(c.sessions, sid);
155
184
  if (typeof raw.agent_id === "string" && raw.agent_id) add(c.subagents, `${sid}\u0000${raw.agent_id}`);
156
185
  add(c.features, raw.provider === "codex" ? "codex-sessions" : "claude-sessions");
@@ -162,6 +191,16 @@ export function createUsageDay({ now = () => new Date() } = {}) {
162
191
  }
163
192
  }
164
193
 
194
+ /** The deck's resident memory right now, in MB: the day keeps its highest. */
195
+ function notePeak(mb) {
196
+ if (!Number.isFinite(mb) || mb <= 0) return;
197
+ const c = today();
198
+ const rounded = Math.round(mb);
199
+ if (rounded <= c.peakMb) return;
200
+ c.peakMb = rounded;
201
+ changes++;
202
+ }
203
+
165
204
  /** A transcript path a Claude hook carried, already accepted as Claude's:
166
205
  * the project is the folder it sits in. */
167
206
  function noteProject(path) {
@@ -182,6 +221,21 @@ export function createUsageDay({ now = () => new Date() } = {}) {
182
221
  add(today().projects, dir);
183
222
  }
184
223
 
224
+ /** How many days in all this deck heard a session on: the saved count, and
225
+ * the days of this run that came after the last of them. */
226
+ function daysUsed() {
227
+ let n = usedBefore;
228
+ for (const d of usedThisRun) if (d > lastUsedBefore) n++;
229
+ return n;
230
+ }
231
+
232
+ /** The last of those days, "" before the first. */
233
+ function lastUsedDay() {
234
+ let last = lastUsedBefore;
235
+ for (const d of usedThisRun) if (d > last) last = d;
236
+ return last;
237
+ }
238
+
185
239
  /** The first use this run heard, or null. */
186
240
  function firstUse() {
187
241
  return first ? { ...first } : null;
@@ -214,7 +268,7 @@ export function createUsageDay({ now = () => new Date() } = {}) {
214
268
 
215
269
  /** What survives a restart: counts and days, never an id or a path. */
216
270
  function saved() {
217
- return { current: current ? totals(current) : null, done, sent };
271
+ return { current: current ? totals(current) : null, done, sent, daysUsed: daysUsed(), lastUsedDay: lastUsedDay() };
218
272
  }
219
273
 
220
274
  /**
@@ -226,11 +280,19 @@ export function createUsageDay({ now = () => new Date() } = {}) {
226
280
  function restore(raw) {
227
281
  const s = normaliseUsage(raw);
228
282
  if (s.sent > sent) sent = s.sent;
283
+ // The further-along of the two, so restoring the same save twice adds nothing.
284
+ if (s.daysUsed > usedBefore || (s.daysUsed === usedBefore && s.lastUsedDay > lastUsedBefore)) {
285
+ usedBefore = s.daysUsed;
286
+ lastUsedBefore = s.lastUsedDay;
287
+ }
229
288
  for (const t of [s.done, s.current]) {
230
289
  if (!t) continue;
231
290
  const c = today();
232
291
  if (t.day === c.day) {
233
- c.floor = { sessions: t.sessions, subagents: t.subagents, projects: t.projects, features: t.features };
292
+ c.floor = {
293
+ sessions: t.sessions, subagents: t.subagents, projects: t.projects, features: t.features,
294
+ events: t.events, peakMb: t.peakMb,
295
+ };
234
296
  } else if (t.day < c.day && (!done || t.day > done.day)) {
235
297
  done = t;
236
298
  }
@@ -243,7 +305,10 @@ export function createUsageDay({ now = () => new Date() } = {}) {
243
305
  return changes;
244
306
  }
245
307
 
246
- return { noteUse, noteProject, noteFolder, noteFeature, firstUse, onFirstUse, finished, markSent, saved, restore, version };
308
+ return {
309
+ noteUse, noteProject, noteFolder, noteFeature, notePeak, firstUse, onFirstUse, finished, markSent, saved, restore, version,
310
+ daysUsed,
311
+ };
247
312
  }
248
313
 
249
314
  /** The deck's own tally, which the event pipeline feeds and the reporter reads. */
@@ -1 +0,0 @@
1
- import{j as e,r as c,k as _,l as V,m as Y,n as z,s as X,u as Z,g as ee}from"./index-tVBMXvDv.js";function se({snap:s}){return e.jsxs("section",{className:"bw-feed",children:[e.jsxs("h4",{className:"bw-sec-head bw-head-row",children:["Live activity",e.jsxs("span",{className:"bw-feed-count",children:[s.log.length," ",s.log.length===1?"event":"events"]})]}),e.jsx("p",{className:"bw-since bw-feed-note",children:"Pages you opened yourself — counted to run the rule, never written down. Addresses appear under Findings, and only for pages a program opened."}),e.jsx("div",{className:"bw-log","aria-live":"polite","aria-relevant":"additions",children:s.log.length===0?e.jsx("p",{className:"bw-note",children:"The deck writes a line here each time it looks at a profile."}):s.log.map(t=>e.jsxs("div",{className:`bw-log-line ${t.level}${t.parts?"":" sys"}`,children:[e.jsx("span",{className:"bw-log-time",children:new Date(t.atMs).toLocaleTimeString("en-GB",{hour12:!1})}),t.parts?e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"bw-log-browser",children:t.parts.browser}),e.jsx("span",{className:"bw-log-profile",title:t.parts.profile,children:t.parts.profile}),e.jsxs("span",{className:"bw-log-value",children:[t.parts.value,t.parts.flagged>0&&e.jsxs("span",{className:"bw-log-flag",children:[" · ",t.parts.flagged," flagged"]})]})]}):e.jsx("span",{className:"bw-log-sys",children:t.text})]},`${t.atMs}-${t.level}-${t.text}`))})]})}const E=s=>`${s.host}-${s.startMs}`;function te(s){const t=l=>new Date(l).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1});return s.count===1||s.endMs-s.startMs<6e4?t(s.startMs):`${t(s.startMs)} → ${t(s.endMs)}`}function ne(s){return new Date(s).toLocaleDateString(void 0,{weekday:"short",day:"numeric",month:"short"})}function U(s){const t=Math.round((s.endMs-s.startMs)/6e4);return t<1?"":`${t} min`}function ae({snap:s,dismiss:t,refreshRef:l}){const[r,h]=c.useState(null),o=c.useRef(new Map),a=c.useRef(null);c.useEffect(()=>{var i,w;const d=a.current;!d||s.episodes.some(m=>E(m)===d.id)||(a.current=null,_(((i=document.activeElement)==null?void 0:i.tagName)??null)&&((w=(d.next?o.current.get(d.next):void 0)??l.current)==null||w.focus()))},[s]);const n=d=>{a.current={id:E(d),next:V(s.episodes.map(E),E(d))},t(d)},u=c.useMemo(()=>{const d=[];for(const i of s.episodes){const w=ne(i.startMs),m=d[d.length-1];m&&m.label===w?m.episodes.push(i):d.push({label:w,episodes:[i]})}return d},[s]);return e.jsxs("section",{className:"bw-findings",children:[e.jsxs("h4",{className:"bw-sec-head bw-head-row",children:["Findings",s.episodes.length>0&&e.jsxs("span",{className:"bw-feed-count",children:[s.episodes.length," ",s.episodes.length===1?"episode":"episodes"]})]}),e.jsx("div",{className:"bw-eps",children:u.length===0?e.jsxs("div",{className:"bw-empty",children:[e.jsx("p",{className:"bw-empty-head",children:"Nothing found yet"}),e.jsxs("p",{className:"bw-empty-note",children:["An episode lands here when a program opens pages in a browser nobody has touched for"," ",s.settings.quietMinutes," ",s.settings.quietMinutes===1?"minute":"minutes",". On most machines that is rare, so an empty list is the ordinary result rather than a sign something is wrong."]}),!s.settings.enabled&&e.jsx("p",{className:"bw-empty-note",children:"Watching is off, so this shows only what this deck has seen since it started. Anything an earlier run recorded comes back when you switch it on."})]}):u.map(d=>e.jsxs("div",{className:"bw-day",children:[e.jsx("h4",{className:"bw-sec-head",children:d.label}),d.episodes.map(i=>{const w=E(i),m=r===w;return e.jsxs("div",{className:`bw-ep${m?" open":""}`,children:[e.jsxs("div",{className:"bw-ep-row",children:[e.jsxs("button",{className:"bw-ep-head",ref:b=>{b?o.current.set(w,b):o.current.delete(w)},onClick:()=>h(m?null:w),"aria-expanded":m,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:m?"▾":"▸"}),e.jsx("span",{className:"bw-ep-host",children:i.host}),e.jsxs("span",{className:"bw-ep-meta",children:[te(i),U(i)&&e.jsxs(e.Fragment,{children:[" · ",U(i)]}),i.provisional&&e.jsxs(e.Fragment,{children:[" · ",e.jsx("span",{title:"Its quiet window has not closed yet. If somebody used the browser in the minutes after it, it is withdrawn; until then it is not written down and no reaction runs.",children:"provisional"})]})]}),e.jsxs("span",{className:"bw-ep-count",children:[i.count," ",i.count===1?"page":"pages"]})]}),e.jsx("button",{className:"glyph-btn bw-ep-x",onClick:()=>n(i),"aria-label":`Dismiss ${i.host}`,title:"Dismiss — it leaves this list for good, and stays in the log file",children:"×"})]}),m&&e.jsx("ul",{className:"bw-urls",children:i.urls.map((b,y)=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-url-time",children:new Date(b.timeMs).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1})}),e.jsx("span",{className:"bw-url",children:b.url})]},`${b.url}-${b.timeMs}-${y}`))})]},w)})]},d.label))})]})}function re(s){const t=/^#?([0-9a-f]{6})$/i.exec((s??"").trim());if(!t)return!1;const l=parseInt(t[1],16),r=[l>>16&255,l>>8&255,l&255].map(h=>{const o=h/255;return o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4)});return .2126*r[0]+.7152*r[1]+.0722*r[2]>.5}const G=1e4,ie=12e3;function oe({browsers:s,findings:t,watching:l,palette:r}){const h=c.useRef(null),o=c.useRef(null);return o.current={browsers:s,findings:t,watching:l},c.useEffect(()=>{var m;const a=h.current;if(!a)return;const n=a.getContext("2d");if(!n)return;const u=((m=window.matchMedia)==null?void 0:m.call(window,"(prefers-reduced-motion: reduce)").matches)??!1;let d=0,i=!1;const w=()=>{var q;if(i)return;const b=o.current;if(!b)return;const{browsers:y,findings:p,watching:P}=b,R=r["--accent"]||"#7dd3fc",f=r["--line"]||"#1f2229",j=r["--text-dim"]||"#6e727c",C=re(r["--bg"]),N=C?42:26,v=C?.85:.65,I=Date.now(),$=Math.min(window.devicePixelRatio||1,2),W=a.clientWidth,k=a.clientHeight;(a.width!==W*$||a.height!==k*$)&&(a.width=W*$,a.height=k*$),n.setTransform($,0,0,$,0,0),n.clearRect(0,0,W,k);const x=W/2,M=k/2,S=Math.min(W,k)/2-6;n.strokeStyle=f,n.lineWidth=1,n.globalAlpha=C?1:.9;for(const g of[.4,.7,1])n.beginPath(),n.arc(x,M,S*g,0,Math.PI*2),n.stroke();n.beginPath(),n.moveTo(x-S,M),n.lineTo(x+S,M),n.moveTo(x,M-S),n.lineTo(x,M+S),n.stroke(),n.globalAlpha=1;const A=(P&&!u?I%G/G:0)*Math.PI*2-Math.PI/2;if(P){const g=(q=n.createConicGradient)==null?void 0:q.call(n,A-.85,x,M);g&&(g.addColorStop(0,"transparent"),g.addColorStop(.85/(Math.PI*2),`color-mix(in srgb, ${R} ${N}%, transparent)`),g.addColorStop(1,"transparent"),n.fillStyle=g,n.beginPath(),n.moveTo(x,M),n.arc(x,M,S,A-.85,A),n.closePath(),n.fill()),n.strokeStyle=R,n.globalAlpha=v,n.beginPath(),n.moveTo(x,M),n.lineTo(x+Math.cos(A)*S,M+Math.sin(A)*S),n.stroke(),n.globalAlpha=1}y.forEach((g,T)=>{const B=T/Math.max(y.length,1)*Math.PI*2-Math.PI/2,D=g.lastReadMs===null?1:Math.min(1,(I-g.lastReadMs)/12e4),O=S*(.42+D*.5),F=x+Math.cos(B)*O,L=M+Math.sin(B)*O,Q=Math.abs((A-B+Math.PI*3)%(Math.PI*2)-Math.PI),K=P?Math.max(0,1-Q/.55):0;n.beginPath(),n.arc(F,L,3+K*1.6,0,Math.PI*2),n.fillStyle=g.running?R:j,n.globalAlpha=g.running?.55+K*.45:.35,n.fill(),n.globalAlpha=1});for(const g of p){const T=(I-g.atMs)/ie;if(T<0||T>1)continue;const D=Math.max(0,y.findIndex(L=>L.key===g.browser))/Math.max(y.length,1)*Math.PI*2-Math.PI/2,O=x+Math.cos(D)*S*.6,F=M+Math.sin(D)*S*.6;n.beginPath(),n.arc(O,F,4+T*22,0,Math.PI*2),n.strokeStyle=R,n.globalAlpha=(1-T)*.8,n.lineWidth=1.5,n.stroke(),n.globalAlpha=1}d=requestAnimationFrame(w)};return w(),()=>{i=!0,cancelAnimationFrame(d)}},[r]),e.jsx("canvas",{ref:h,className:"bw-radar",role:"img","aria-label":l?`Watching ${s.length} browser${s.length===1?"":"s"}`:"Not watching"})}function le(s,t,l){if(s===null)return null;const r=s+t-l;return r>0?r:null}function ce(s){const t=Math.ceil(s/1e3),l=Math.floor(t/60),r=t%60;return l>0?`${l}m ${r}s`:`${r}s`}function J(s){return(s??[]).filter(t=>t.installed&&t.profiles>0)}function de(s){return!Number.isFinite(s)||s<=0?"empty":Y(s)}function he(s,t){return s.map(l=>({key:l.key,name:l.name,running:l.running,visits:t.filter(r=>r.browser===l.key).reduce((r,h)=>r+h.programVisits,0)}))}function ue(s,t){if(s===null)return null;const l=Math.max(0,Math.round((t-s)/1e3));if(l<5)return"just now";if(l<60)return`${l} sec ago`;const r=Math.round(l/60);return r<60?`${r} min ago`:`${Math.round(r/60)} hr ago`}function we(s){const t=s.profiles.filter(r=>r.degraded);if(t.length>0){const r=t[0],h=t.length-1;return{kind:"unreadable",text:`${r.name}/${r.profile} could not be read — ${r.reason??"no reason given"}`+(h>0?`, and ${h} more`:"")+". The watch continues on the profiles it can read."}}const l=J(s.browsers);return l.length===0?{kind:"no-profiles",text:"No browser on this machine has a profile to read. Open one of the browsers below once and it will be watched from then on."}:l.some(r=>r.running)?null:{kind:"none-running",text:"No watched browser is running, so nothing can be navigated right now. Their history is still read, and anything found while they were open is still here."}}function me({snap:s,watching:t,palette:l}){const[r,h]=c.useState(!1),[o,a]=c.useState(()=>Date.now());c.useEffect(()=>{const u=setInterval(()=>a(Date.now()),1e3);return()=>clearInterval(u)},[]);const n=le(s.coverage.lastHumanMs,s.coverage.quietMs,o);return e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Activity overview",e.jsx("button",{className:"bw-help",onClick:()=>h(u=>!u),"aria-expanded":r,"aria-label":"What the sweep and the dots mean",children:"?"})]}),e.jsxs("div",{className:"bw-glance",children:[e.jsx(oe,{browsers:t.map(u=>{var d;return{key:u.key,name:u.name,running:u.running,lastReadMs:((d=s.profiles.find(i=>i.browser===u.key))==null?void 0:d.lastWrittenMs)??null}}),findings:s.episodes.slice(0,6).map(u=>({browser:u.browser??null,atMs:u.endMs})),watching:s.settings.enabled,palette:l}),e.jsx("ul",{className:"bw-counts",children:he(t,s.profiles).map(u=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-count-n",children:u.visits.toLocaleString("en-US")}),e.jsx("span",{className:"bw-count-of",children:u.name})]},u.key))})]}),e.jsx("p",{className:"bw-since",children:"Pages a program opened, since this deck started"}),s.coverage.checkedMs>0&&e.jsxs("p",{className:"bw-since",children:["Last checked ",ue(s.coverage.checkedMs,o),s.coverage.checks>0&&e.jsxs(e.Fragment,{children:[" · ",s.coverage.checks.toLocaleString("en-US")," ",s.coverage.checks===1?"check":"checks"]})]}),n!==null&&e.jsxs("p",{className:"bw-gate",children:["You are browsing — a program page would count in ",ce(n)]}),r&&e.jsxs("dl",{className:"bw-key",children:[e.jsx("dt",{children:"Sweep"}),e.jsx("dd",{children:"one poll of every watched profile"}),e.jsx("dt",{children:"Dot"}),e.jsx("dd",{children:"a browser, further out the longer since it last wrote history"}),e.jsx("dt",{children:"Ring"}),e.jsx("dd",{children:"a finding, leaving the browser it came from"})]})]})}function be({watching:s,rest:t}){const[l,r]=c.useState(!1),[h,o]=c.useState(!1);return e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Watched profiles",e.jsx("button",{className:"bw-help",onClick:()=>r(a=>!a),"aria-expanded":l,"aria-label":"What running, idle and connected to Anthropic mean",children:"?"})]}),l&&e.jsxs("dl",{className:"bw-key",children:[e.jsx("dt",{children:"Running"}),e.jsx("dd",{children:"the browser has a process on this machine right now"}),e.jsx("dt",{children:"Connected"}),e.jsx("dd",{children:"an open connection to an address Anthropic's relay uses — which it shares with claude.ai, so an open tab looks the same as an agent channel"})]}),e.jsx("ul",{className:"bw-profiles",children:s.map(a=>e.jsxs("li",{children:[e.jsx("span",{className:`bw-prof-dot${a.running?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-prof-name",children:a.name}),e.jsx("span",{className:"bw-prof-state",children:a.running?"running":"idle"}),a.running&&a.relay.state==="live"&&e.jsx("span",{className:"bw-relay",children:"connected to Anthropic"})]},a.key))}),t.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"bw-why bw-rest-head",onClick:()=>o(a=>!a),"aria-expanded":h,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:h?"▾":"▸"})," ",t.length," not watched"]}),h&&e.jsxs("p",{className:"bw-rest",children:[t.map(a=>a.name).join(", ")," — not installed, or installed and never opened, so there is no history to read."]})]})]})}function fe({snap:s,quiet:t,setQuiet:l,save:r,access:h,setAccess:o}){return e.jsxs("div",{className:"bw-settings",children:[e.jsxs("label",{title:"A program opening a page counts as a finding only if nobody had touched the browser for this long. Shorter catches more and reports more of your own work; longer is quieter.",children:[e.jsx("span",{children:"Nobody browsing for"}),e.jsxs("select",{value:t??s.settings.quietMinutes,onChange:a=>{l(Number(a.target.value)),r({quietMinutes:Number(a.target.value)})},children:[e.jsx("option",{value:1,children:"1 min"}),e.jsx("option",{value:5,children:"5 min"}),e.jsx("option",{value:15,children:"15 min"}),e.jsx("option",{value:30,children:"30 min"}),e.jsx("option",{value:60,children:"60 min"})]})]}),e.jsxs("label",{children:[e.jsx("span",{children:"When it finds one"}),e.jsx("select",{value:s.settings.reaction,onChange:a=>void r({reaction:a.target.value}),disabled:!s.settings.enabled,title:s.settings.enabled?"What to do besides writing it down.":"Turn watching on to arm a reaction.",children:(s.reactions??["notify"]).map(a=>e.jsx("option",{value:a,children:a==="notify"?"notify me":a==="close-tab"?"close the tab":"quit the browser"},a))})]}),e.jsxs("div",{className:"bw-settings-note",children:[e.jsxs("p",{children:["Chrome marks a navigation that came from an extension or a command rather than from a click. These are the ones that happened while nobody had touched the browser for the time above. ",e.jsx("strong",{children:"Usually that is your own agent doing what you asked."})]}),e.jsxs("button",{className:"bw-why",onClick:()=>o(a=>!a),"aria-expanded":h,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:h?"▾":"▸"})," What Browser Watch can access"]}),h&&e.jsxs("dl",{className:"bw-access",children:[e.jsx("dt",{children:"Reads"}),e.jsxs("dd",{children:["A copy of each browser's own history database — the live file is locked while the browser holds it. Only rows newer than the moment this deck started:"," ",new Date(s.coverage.startedMs).toLocaleString(),"."]}),e.jsx("dt",{children:"Keeps"}),e.jsxs("dd",{children:["Only while the switch is on, and only the episodes it flagged — never your ordinary browsing. In ",e.jsx("code",{className:"bw-path",children:s.coverage.logPath}),typeof s.coverage.logBytes=="number"?` (${de(s.coverage.logBytes)})`:"",", with every address written in full so you can check it yourself."]}),e.jsx("dt",{children:"Sends"}),e.jsx("dd",{children:"Nothing. No part of this reads or writes over the network; the deck serves on 127.0.0.1 and this panel talks only to it."}),e.jsx("dt",{children:"Never reads"}),e.jsx("dd",{children:"Anything from before this deck started, cookies, saved passwords, page contents, or any browser profile with no history file."})]})]})]})}function H(s,t){return t?{kind:"saving",word:"Saving"}:s.settings.enabled?{kind:"on",word:"Watching"}:{kind:"off",word:"Paused"}}function ge({snap:s,saving:t,save:l,why:r,setWhy:h}){return e.jsxs("footer",{className:"bw-status",children:[e.jsx("span",{className:`bw-mode-dot${s.settings.enabled?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-mode-word",children:H(s,t).word},H(s,t).kind),e.jsx("span",{className:"bw-status-text",children:s.coverage.archived===0?s.settings.enabled?"No episodes recorded yet":"No episodes on disk":`${s.coverage.archived} ${s.coverage.archived===1?"episode":"episodes"} on disk`}),e.jsx("button",{className:`glyph-btn bw-gear${r?" on":""}`,onClick:()=>h(o=>!o),"aria-expanded":r,"aria-label":"Settings",title:"Settings",children:e.jsxs("svg",{width:"13",height:"13",viewBox:"0 0 14 14",fill:"none",stroke:"currentColor",strokeWidth:"1.3","aria-hidden":!0,children:[e.jsx("circle",{cx:"7",cy:"7",r:"2.1"}),e.jsx("path",{d:"M7 1.2v1.6M7 11.2v1.6M1.2 7h1.6M11.2 7h1.6M2.9 2.9l1.1 1.1M10 10l1.1 1.1M11.1 2.9L10 4M4 10l-1.1 1.1"})]})}),e.jsxs("label",{className:"bw-switch",htmlFor:"bw-enabled",children:[e.jsx("span",{className:"bw-switch-label",id:"bw-enabled-label",children:"Watch browser activity"}),e.jsx("button",{id:"bw-enabled",type:"button",role:"switch","aria-checked":s.settings.enabled,"aria-labelledby":"bw-enabled-label",className:"switch",onClick:()=>void l({enabled:!s.settings.enabled}),title:s.settings.enabled?"On — every episode it finds is written down, so the list outlives the browsing history being cleared":"Off — showing only what this deck has seen since it started. Anything an earlier run archived is hidden until you switch back on, and nothing new is kept",children:e.jsx("span",{className:"switch-knob"})})]})]})}function pe({relay:s}){const[t,l]=c.useState(!1),[r,h]=c.useState(!1),o=s.killswitch.blocked?"unblock":"block",a=s.command[o],n=c.useCallback(async()=>{await z(a.command)&&(h(!0),window.setTimeout(()=>h(!1),1600))},[a.command]),u=s.profiles.filter(i=>{var w;return(w=i.report)==null?void 0:w.present}),d=s.profiles.filter(i=>i.report===null);return e.jsxs("section",{className:"bw-sec",children:[e.jsx("h4",{className:"bw-sec-head",children:"Remote control"}),e.jsx("p",{className:`bw-rc-verdict ${s.verdict}`,role:"status",children:s.verdict==="exposed"?"Claude in Chrome is live here, so any Claude Code session signed in to your Anthropic account can drive this browser — from any machine, with no prompt on this one.":s.verdict==="protected"?`The relay is black-holed in ${s.hostsPath}, so no new connection to it can be made from this machine.`:"Claude in Chrome is not installed and enabled in any profile here, so there is nothing for the relay to drive."}),s.killswitch.foreign.length>0&&e.jsxs("p",{className:"bw-rc-foreign",children:["Another entry for this host is already in ",s.hostsPath,", and ccdeck will not touch it:",s.killswitch.foreign.map(i=>e.jsx("code",{className:"bw-path",children:i},i))]}),e.jsxs("button",{className:"bw-why",onClick:()=>l(i=>!i),"aria-expanded":t,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:t?"▾":"▸"})," ",s.verdict==="protected"?"How to undo the block":"What it can reach, and how to stop it"]}),t&&e.jsxs("div",{className:"bw-rc-body",children:[u.length>0&&e.jsx("dl",{className:"bw-key",children:u.map(i=>e.jsxs(c.Fragment,{children:[e.jsxs("dt",{children:[i.name,i.profile==="Default"?"":` · ${i.profile}`]}),e.jsxs("dd",{children:[i.report.enabled?"enabled":"installed but switched off",i.report.allUrls?", can act on every site":", limited to the sites it was granted",i.report.sensitiveApis.length>0&&e.jsxs(e.Fragment,{children:[" — ",i.report.sensitiveApis.join(", ")]})]})]},`${i.browser}/${i.profile}`))}),d.length>0&&e.jsxs("p",{className:"bw-rc-note",children:[d.map(i=>i.name).join(", ")," — its settings file could not be read, so nothing is claimed about it either way."]}),!s.hostsRead&&e.jsxs("p",{className:"bw-rc-note",children:[s.hostsPath," could not be read, so whether the relay is already blocked is unknown."]}),u.some(i=>i.report.allUrls)&&e.jsxs("p",{className:"bw-rc-note",children:["Narrowing the sites in ",e.jsx("code",{className:"bw-path",children:"chrome://extensions"})," does not change this: the grant is held at the browser level and the per-site list is enforced inside the extension, by the extension."]}),e.jsxs("div",{className:"bw-rc-cmd",children:[e.jsx("code",{children:a.command}),e.jsx("button",{className:"btn bw-rc-copy",onClick:n,children:r?"Copied":"Copy"})]}),e.jsx("p",{className:"bw-rc-note",children:a.note})]})]})}function xe(s){const[t,l]=c.useState(null),[r,h]=c.useState(null),[o,a]=c.useState(null),[n,u]=c.useState(!1),d=c.useRef(!1),[i,w]=c.useState(null),[m,b]=c.useState(!1),y=c.useRef(!1),p=c.useCallback(async f=>{var j,C;if(!(f&&!X(d.current))){f&&(d.current=!0,u(!0));try{const N=await fetch(`/api/browser-watch${f?"?refresh=1":""}`);if(!N.ok)throw new Error(`the deck answered ${N.status}`);const v=await N.json();l(v),y.current||w(((j=v==null?void 0:v.settings)==null?void 0:j.quietMinutes)??15),s(((C=v==null?void 0:v.settings)==null?void 0:C.enabled)===!0),h(null)}catch(N){h(N instanceof Error?N.message:String(N))}finally{f&&(d.current=!1,u(!1))}}},[s]);c.useEffect(()=>{p(!1)},[p]),c.useEffect(()=>{const f=setInterval(()=>{p(!1)},1e4);return()=>clearInterval(f)},[p]);const P=c.useCallback(async f=>{try{await fetch("/api/browser-watch/dismiss",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({host:f.host,startMs:f.startMs})})}finally{await p(!1)}},[p]),R=c.useCallback(async f=>{y.current=!0,b(!0);try{const j=await fetch("/api/browser-watch",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(f)});if(!j.ok)throw new Error(`the deck answered ${j.status}`);await p(!1),a(null)}catch(j){a(j instanceof Error?j.message:String(j))}finally{y.current=!1,b(!1)}},[p]);return{snap:t,error:r,writeError:o,setWriteError:a,busy:n,quiet:i,setQuiet:w,saving:m,load:p,dismiss:P,save:R}}function ye({onClose:s,onSeen:t,onWatching:l,palette:r}){const h=Z(s),{snap:o,error:a,writeError:n,setWriteError:u,busy:d,quiet:i,setQuiet:w,saving:m,load:b,dismiss:y,save:p}=xe(l),[P,R]=c.useState(!1),[f,j]=c.useState(!1),C=c.useRef(null),N=c.useRef(t);N.current=t,c.useEffect(()=>()=>N.current(Date.now()),[]);const v=J(o==null?void 0:o.browsers),I=((o==null?void 0:o.browsers)??[]).filter(k=>!(k.installed&&k.profiles>0)),$=o?we(o):null,W=v.reduce((k,x)=>k+x.profiles,0);return e.jsx("div",{className:"modal-backdrop",onClick:s,role:"presentation",children:e.jsxs("div",{ref:h,className:"modal bw-modal",onClick:k=>k.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":"bw-title",children:[e.jsxs("header",{className:"modal-head",children:[e.jsxs("div",{className:"modal-title",children:[e.jsx("span",{className:"modal-tool-name",id:"bw-title",children:"Browser watch"}),e.jsx("span",{className:"modal-tool-id",children:o?`${W} ${W===1?"profile":"profiles"} · local only`:"local only"})]}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{ref:C,className:"glyph-btn",onClick:()=>void b(!0),...ee(d),"aria-label":"Re-read every profile now",title:"Re-read every profile now",children:d?"…":"↻"}),e.jsx("button",{className:"glyph-btn",onClick:s,"aria-label":"Close (Esc)",title:"Close (Esc)",children:"×"})]})]}),e.jsxs("div",{className:"modal-body bw-body",children:[!o&&!a&&e.jsxs("div",{className:"bw-loading",children:[e.jsx("span",{className:"bw-loading-bar","aria-hidden":!0}),e.jsx("p",{children:"Reading each browser's history. The first look copies the file, which takes a moment."})]}),a&&e.jsx("div",{className:"bw-state",children:e.jsxs("div",{className:"bw-row err",children:[e.jsx("span",{className:"bw-dot","aria-hidden":!0}),e.jsx("span",{className:"bw-row-label",children:"Could not read"}),e.jsx("span",{className:"bw-row-detail",children:a})]})}),o&&$&&e.jsx("p",{className:`bw-trouble ${$.kind}`,role:"status",children:$.text}),o&&e.jsxs("div",{className:"bw-work",children:[e.jsxs("aside",{className:"bw-side",children:[e.jsx(me,{snap:o,watching:v,palette:r}),e.jsx(be,{watching:v,rest:I}),o.relay&&e.jsx(pe,{relay:o.relay})]}),e.jsxs("div",{className:"bw-main",children:[e.jsx(ae,{snap:o,dismiss:y,refreshRef:C}),e.jsx(se,{snap:o})]})]}),o&&P&&e.jsx(fe,{snap:o,quiet:i,setQuiet:w,save:p,access:f,setAccess:j}),n&&e.jsx("div",{className:"bw-state bw-write-err",role:"alert",children:e.jsxs("div",{className:"bw-row err",children:[e.jsx("span",{className:"bw-dot","aria-hidden":!0}),e.jsx("span",{className:"bw-row-label",children:"Could not save that setting"}),e.jsxs("span",{className:"bw-row-detail",children:[n," — the switches below show what is stored, not what you pressed."]}),e.jsx("button",{className:"glyph-btn bw-write-err-x",onClick:()=>u(null),"aria-label":"Dismiss",title:"Dismiss",children:"×"})]})}),o&&e.jsx(ge,{snap:o,saving:m,save:p,why:P,setWhy:R})]})]})})}export{ye as default};
@@ -1 +0,0 @@
1
- import{a as U,f as p,r as c,s as Z,p as _,j as a,b as g,c as O,u as D,d as ee,e as se,g as E,h as ae,i as te}from"./index-tVBMXvDv.js";function ne(s){return!s.answer||s.answer.range!==s.want?{phase:"busy",stale:!1}:s.answer.ok?{phase:s.answer.days>0?"chart":"empty",stale:s.loading}:{phase:"error",stale:s.loading}}function I(s){const e=new Map;for(const n of s)for(const o of n.agents??[]){if(!o||typeof o.agent!="string"||o.agent==="")continue;const r=e.get(o.agent)??{id:o.agent,cost:0,tokens:0};r.cost+=Number.isFinite(o.totalCost)?o.totalCost:0,r.tokens+=Number.isFinite(o.totalTokens)?o.totalTokens:0,e.set(o.agent,r)}return[...e.values()].sort((n,o)=>o.cost-n.cost||n.id.localeCompare(o.id))}function oe(s){const e=I([{agents:s}]);return e.length<2?null:e.map(n=>`${U(n.id)} ${p(n.cost)}`).join(" · ")}function F(s){switch(s.toLowerCase()){case"claude":return"var(--model-opus)";case"codex":return"var(--model-codex)";case"gemini":return"var(--model-gemini)";case"copilot":return"var(--model-haiku)";default:return"var(--model-other)"}}function le(s,e){if(!(e>0)||!(s>0))return"0%";const n=s/e*100;return n<.1?"<0.1%":`${n.toFixed(1)}%`}const u=s=>typeof s=="number"&&Number.isFinite(s)?s:0,M=s=>typeof s=="string"?s:"",P=s=>Array.isArray(s)?s.filter(e=>typeof e=="string"):[];function re(s){const e=s??{};return{modelName:M(e.modelName),cost:u(e.cost),inputTokens:u(e.inputTokens),outputTokens:u(e.outputTokens),cacheCreationTokens:u(e.cacheCreationTokens),cacheReadTokens:u(e.cacheReadTokens)}}function ce(s){const e=s??{},n=Array.isArray(e.agents)?e.agents.map(o=>{const r=o??{};return{agent:M(r.agent),totalCost:u(r.totalCost),totalTokens:u(r.totalTokens)}}):void 0;return{period:M(e.period),totalCost:u(e.totalCost),totalTokens:u(e.totalTokens),inputTokens:u(e.inputTokens),outputTokens:u(e.outputTokens),cacheCreationTokens:u(e.cacheCreationTokens),cacheReadTokens:u(e.cacheReadTokens),modelsUsed:P(e.modelsUsed),modelBreakdowns:Array.isArray(e.modelBreakdowns)?e.modelBreakdowns.map(re):[],...n?{agents:n}:{},...e.metadata&&typeof e.metadata=="object"?{metadata:{agents:P(e.metadata.agents)}}:{}}}function ie(s){const e=s??{};return{...e,ok:e.ok===!0,days:Array.isArray(e.days)?e.days.map(ce):void 0}}function T(s){const e=s.toLowerCase();return e.includes("opus")?"var(--model-opus)":e.includes("sonnet")?"var(--model-sonnet)":e.includes("haiku")?"var(--model-haiku)":e.includes("gpt-5")||e.includes("gpt5")?"var(--model-gpt5)":e.includes("gpt")?"var(--model-gpt)":e.includes("gemini")?"var(--model-gemini)":e.includes("codex")?"var(--model-codex)":e.includes("fable")||e.includes("mythos")?"var(--model-fable)":"var(--model-other)"}function ue(s){let e=0,n=0,o=0,r=0;const m=new Map;for(const l of s){e+=l.totalCost,n+=l.totalTokens,o+=l.inputTokens+l.outputTokens,r+=l.cacheReadTokens;for(const d of l.modelBreakdowns)m.set(d.modelName,(m.get(d.modelName)??0)+d.cost)}return{totalCost:e,totalTok:n,inOut:o,cacheRead:r,modelCosts:m}}function de(s){return Array.from(s.entries()).sort((e,n)=>n[1]-e[1])}function q(s){return s.slice().sort((e,n)=>n.cost-e.cost)}function C(s,e){return e>0?s/e*100:0}function he(s){var n,o;return oe(s.agents)??((o=(n=s.metadata)==null?void 0:n.agents)!=null&&o.length?s.metadata.agents.join(" · "):null)}function me(){let s=0;return{begin(){const e=++s;return()=>e===s},cancel(){s++}}}function pe(s){const[e,n]=c.useState(null),[o,r]=c.useState(!1),m=c.useState(me)[0],l=c.useRef(!1),d=(f=!1)=>{if(f&&!Z(l.current))return;const x=m.begin();l.current=f;const k=s;r(!0);const i=`/api/ccusage?since=${_(k)}${f?"&refresh=1":""}`;fetch(i).then(j=>j.json()).then(j=>{x()&&n({range:k,resp:ie(j)})}).catch(()=>{x()&&n({range:k,resp:{ok:!1,reason:"unreachable"}})}).finally(()=>{x()&&(l.current=!1,r(!1))})};return c.useEffect(()=>(d(!1),()=>m.cancel()),[s]),{landed:e,loading:o,reload:()=>d(!0)}}function ge({selectedDay:s}){const e=he(s);return a.jsxs("div",{className:"uh-detail",children:[a.jsxs("div",{className:"uh-detail-head",children:[a.jsx("span",{className:"uh-detail-date",children:s.period}),a.jsx("span",{className:"uh-detail-cost",children:p(s.totalCost)}),e&&a.jsx("span",{className:"uh-detail-agents",title:e,children:e})]}),a.jsxs("div",{className:"uh-detail-mini",children:[a.jsx(y,{label:"input",val:g(s.inputTokens)}),a.jsx(y,{label:"output",val:g(s.outputTokens)}),a.jsx(y,{label:"cache write",val:g(s.cacheCreationTokens)}),a.jsx(y,{label:"cache read",val:g(s.cacheReadTokens)})]}),a.jsx("div",{className:"uh-detail-models",children:q(s.modelBreakdowns).map(n=>{const o=C(n.cost,s.totalCost);return a.jsxs("div",{className:"uh-model-row",title:n.modelName,children:[a.jsxs("span",{className:"uh-model-name",children:[a.jsx("span",{className:"uh-legend-dot",style:{background:T(n.modelName)}}),a.jsx("span",{className:"uh-model-label",children:O(n.modelName)})]}),a.jsx("span",{className:"uh-model-bar",children:a.jsx("span",{className:"uh-model-bar-fill",style:{width:`${o}%`,background:T(n.modelName)}})}),a.jsx("span",{className:"uh-model-cost",children:p(n.cost)})]},n.modelName)})})]})}function y({label:s,val:e}){return a.jsxs("div",{className:"uh-ministat",children:[a.jsx("span",{className:"uh-ministat-val",children:e}),a.jsx("span",{className:"uh-ministat-label",children:s})]})}const fe=[7,14,30,90];function je({onClose:s,providers:e}){var B;const[n,o]=c.useState(30),[r,m]=c.useState(null),{landed:l,loading:d,reload:f}=pe(n),x=c.useRef(null),k=D(s,{focusRef:x}),h=ne({loading:d,want:n,answer:l&&{range:l.range,ok:l.resp.ok===!0,days:((B=l.resp.days)==null?void 0:B.length)??0}}),i=h.phase==="chart"?l.resp.days??[]:[],j=c.useRef(null),G=ee(h.phase!=="error",j),H=c.useMemo(()=>i.reduce((t,N)=>Math.max(t,N.totalCost),0),[i]),S=c.useRef(null);c.useLayoutEffect(()=>{const t=S.current;t&&(t.scrollLeft=t.scrollWidth)},[i]);const{totalCost:w,totalTok:V,inOut:W,cacheRead:z,modelCosts:A}=c.useMemo(()=>ue(i),[i]),J=c.useMemo(()=>de(A),[A]),b=c.useMemo(()=>I(i),[i]),K=b.length>1,L=r?i.find(t=>t.period===r)??null:null;return a.jsx("div",{className:"modal-backdrop",onClick:s,role:"presentation",children:a.jsxs("div",{ref:k,className:"modal uh-modal",onClick:t=>t.stopPropagation(),role:"dialog","aria-modal":"true","aria-label":"Usage history",children:[a.jsxs("header",{className:"uh-head",children:[a.jsxs("div",{className:"uh-titlewrap",children:[a.jsx("div",{className:"uh-title",children:"Usage history"}),a.jsx("div",{className:"uh-sub",children:h.stale?"running ccusage…":se(e,b.map(t=>t.id))})]}),a.jsx("div",{className:"uh-range",role:"group","aria-label":"Range",children:fe.map(t=>a.jsxs("button",{type:"button","aria-pressed":n===t,className:"uh-range-btn",onClick:()=>{o(t),m(null)},children:[t,"d"]},t))}),a.jsx("button",{ref:j,className:"glyph-btn uh-reload",onClick:f,...E(d),title:"Re-run ccusage","aria-label":"Reload",children:d?"…":"↻"}),a.jsx("button",{ref:x,className:"glyph-btn uh-close",onClick:s,"aria-label":"Close",children:"×"})]}),h.phase==="busy"?a.jsxs("div",{className:"uh-status uh-status-wait","aria-busy":"true",children:[a.jsx("span",{className:"uh-wait-word",children:l?"running ccusage…":"running ccusage… (first run downloads the package)"}),a.jsx("span",{className:"uh-wait-axis","aria-hidden":!0})]}):h.phase==="error"?a.jsxs("div",{className:"uh-status uh-err",title:te(l.resp)||void 0,children:[ae(l.resp,"ccusage did not report usage"),a.jsx("div",{children:a.jsx("button",{className:"btn uh-retry",onClick:()=>{G(),f()},...E(d),children:d?"trying…":"Try again"})})]}):h.phase==="empty"?a.jsx("div",{className:`uh-status${h.stale?" uh-stale":""}`,"aria-busy":h.stale||void 0,children:"no usage in this range"}):a.jsxs("div",{className:h.stale?"uh-stale":void 0,"aria-busy":h.stale||void 0,children:[a.jsxs("div",{className:"uh-totals",children:[a.jsx(v,{label:"total cost",val:p(w),accent:!0}),a.jsx(v,{label:"tokens",val:g(V)}),a.jsx(v,{label:"input+output",val:g(W)}),a.jsx(v,{label:"cache reads",val:g(z)})]}),K&&a.jsxs("div",{className:"uh-agents",role:"group","aria-label":"Cost by CLI",children:[a.jsx("div",{className:"uh-agent-bar","aria-hidden":"true",children:b.map(t=>a.jsx("span",{className:"uh-agent-seg",style:{width:`${C(t.cost,w)}%`,background:F(t.id)}},t.id))}),a.jsx("div",{className:"uh-agent-keys",children:b.map(t=>a.jsxs("span",{className:"uh-agent-key",title:`${t.id} · ${g(t.tokens)} tokens`,children:[a.jsx("span",{className:"uh-legend-dot",style:{background:F(t.id)}}),U(t.id),a.jsx("span",{className:"uh-agent-cost",children:p(t.cost)}),a.jsx("span",{className:"uh-agent-share",children:le(t.cost,w)})]},t.id))})]}),a.jsx("div",{ref:S,className:"uh-chart",role:"group","aria-label":"Daily cost by model",children:i.map((t,N)=>{const Q=C(t.totalCost,H),R=t.period===r,X=`${t.period}, ${p(t.totalCost)}`;return a.jsxs("button",{className:`uh-bar-col${R?" sel":""}`,onClick:()=>m(R?null:t.period),"aria-pressed":R,"aria-label":X,title:`${t.period} · ${p(t.totalCost)}`,style:{flexBasis:`${100/i.length}%`,"--uh-i":N,"--uh-step":`${Math.min(10,200/Math.max(i.length,1))}ms`},children:[a.jsx("div",{className:"uh-bar",style:{height:`${Math.max(Q,t.totalCost>0?2:0)}%`},children:q(t.modelBreakdowns).map($=>{const Y=C($.cost,t.totalCost);return a.jsx("div",{className:"uh-bar-seg",style:{height:`${Y}%`,background:T($.modelName)}},$.modelName)})}),a.jsx("span",{className:"uh-bar-label",children:t.period.slice(5)})]},t.period)})}),a.jsx("div",{className:"uh-legend",children:J.map(([t,N])=>a.jsxs("span",{className:"uh-legend-item",title:t,children:[a.jsx("span",{className:"uh-legend-dot",style:{background:T(t)}}),O(t)," ",a.jsx("span",{className:"uh-legend-cost",children:p(N)})]},t))}),L&&a.jsx(ge,{selectedDay:L})]})]})})}function v({label:s,val:e,accent:n}){return a.jsxs("div",{className:"uh-stat",children:[a.jsx("span",{className:`uh-stat-val${n?" accent":""}`,children:e}),a.jsx("span",{className:"uh-stat-label",children:s})]})}export{je as default};