ccdeck 3.22.14 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -10
- package/THIRD_PARTY_NOTICES.md +123 -15
- package/bin/deck.js +264 -21
- package/dist/web/assets/BrowserWatchModal-DD6srgsk.js +1 -0
- package/dist/web/assets/UsageHistoryModal-zExZpRWU.js +1 -0
- package/dist/web/assets/index-Bf-qcZ6x.css +1 -0
- package/dist/web/assets/index-Dsca2TwS.js +296 -0
- package/dist/web/index.html +9 -5
- package/hook/hook.js +631 -99
- package/package.json +1 -1
- package/release-notes.json +62 -1
- package/src/server/activity.mjs +42 -4
- package/src/server/args.mjs +30 -4
- package/src/server/browser-history.mjs +164 -38
- package/src/server/browser-presence.mjs +60 -0
- package/src/server/browser-react.mjs +160 -7
- package/src/server/browser-watch-store.mjs +319 -17
- package/src/server/browser-watch.mjs +304 -60
- package/src/server/ccusage.mjs +20 -4
- package/src/server/claude-accounts.mjs +79 -34
- package/src/server/claude-identity.mjs +121 -0
- package/src/server/cswap-admin.mjs +160 -133
- package/src/server/cswap-auto.mjs +86 -7
- package/src/server/cswap-install.mjs +280 -43
- package/src/server/deck-home.mjs +1 -1
- package/src/server/deck-prefs.mjs +252 -36
- package/src/server/deck-probe.mjs +1 -1
- package/src/server/exec.mjs +84 -3
- package/src/server/global-install.mjs +13 -0
- package/src/server/index.mjs +1454 -182
- package/src/server/installer.mjs +136 -8
- package/src/server/lan-about.mjs +8 -3
- package/src/server/lan-engine.mjs +385 -51
- package/src/server/lan-socket.mjs +384 -57
- package/src/server/lan-sync.mjs +495 -22
- package/src/server/log-writer.mjs +575 -2
- package/src/server/login-service.mjs +31 -4
- package/src/server/open-url.mjs +22 -2
- package/src/server/output-watch.mjs +59 -8
- package/src/server/purge-key.mjs +135 -0
- package/src/server/quota.mjs +112 -7
- package/src/server/running-deck.mjs +15 -1
- package/src/server/self-update.mjs +98 -4
- package/src/server/session-recap.mjs +89 -0
- package/src/server/static-cache.mjs +67 -0
- package/src/server/stop-deck.mjs +2 -2
- package/src/server/store-lock.mjs +51 -0
- package/src/server/supervisor.mjs +1 -1
- package/src/server/system-metrics.mjs +143 -17
- package/dist/web/assets/index-BdYsAtN8.js +0 -274
- package/dist/web/assets/index-CgwmcBHv.css +0 -1
package/bin/deck.js
CHANGED
|
@@ -71,7 +71,10 @@ if (flags.version) {
|
|
|
71
71
|
process.exit(0);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// `--purge` enters here too, and on its own. It is `--uninstall` plus the deck's
|
|
75
|
+
// own key, and a `--purge` that started a server instead would be a flag whose
|
|
76
|
+
// entire effect was silence.
|
|
77
|
+
if (flags.uninstall || flags.purge) {
|
|
75
78
|
const { uninstallHooks, hasCodexInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
76
79
|
// Anything that could not be taken out. An uninstall that removed nothing
|
|
77
80
|
// because it could not read the file has not uninstalled anything, and both
|
|
@@ -159,6 +162,55 @@ if (flags.uninstall) {
|
|
|
159
162
|
if (hasCodexInstalled()) {
|
|
160
163
|
report(await uninstallHooks({ provider: "codex" }), "Codex");
|
|
161
164
|
}
|
|
165
|
+
// ── THE PRIVATE KEY ────────────────────────────────────────────────────────
|
|
166
|
+
//
|
|
167
|
+
// The one thing left on the disk that is a CREDENTIAL rather than data. Every
|
|
168
|
+
// deck paired with this one has pinned the key in prefs.json, and until #959
|
|
169
|
+
// nothing in this command — or in the `--help` text, or in the README's
|
|
170
|
+
// uninstall paragraph — named the file, so somebody who followed the
|
|
171
|
+
// instructions to the letter believed the machine was clean and it was not.
|
|
172
|
+
//
|
|
173
|
+
// NAMED, NOT REMOVED, unless somebody asked. prefs.json also holds the
|
|
174
|
+
// pairings, the aliases and which accounts this deck offers, and deleting a
|
|
175
|
+
// user's settings out from under a command documented as "hook entries only"
|
|
176
|
+
// is a different complaint of the same size. So the default is to say exactly
|
|
177
|
+
// where the key is — resolved for THIS machine, both copies, because
|
|
178
|
+
// `migrateDeckFiles` copies rather than moves and the old one is in a
|
|
179
|
+
// directory nothing calls the deck's state — and `--purge` is the sentence
|
|
180
|
+
// somebody types when they mean it.
|
|
181
|
+
{
|
|
182
|
+
const { keyDirs, findKeyFiles, purgeKeyFiles } =
|
|
183
|
+
await import(pathToFileURL(join(PKG_ROOT, "src/server/purge-key.mjs")).href);
|
|
184
|
+
const found = await findKeyFiles(keyDirs());
|
|
185
|
+
if (flags.purge) {
|
|
186
|
+
const { removed, failed } = await purgeKeyFiles(found);
|
|
187
|
+
for (const path of removed) console.log(`${PRODUCT}: removed ${path}`);
|
|
188
|
+
for (const f of failed) {
|
|
189
|
+
refused = true;
|
|
190
|
+
// `gDash`, not an em dash: top-level block, possibly the legacy Windows
|
|
191
|
+
// console (#797).
|
|
192
|
+
console.error(`${PRODUCT}: could NOT remove ${f.path} ${gDash} ${f.why}`);
|
|
193
|
+
console.error(`${PRODUCT}: this deck's LAN private key is still in that file.`);
|
|
194
|
+
}
|
|
195
|
+
if (removed.length === 0 && failed.length === 0) {
|
|
196
|
+
console.log(`${PRODUCT}: no ${PRODUCT} state to purge`);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// "no-key" files are not mentioned. Telling somebody their private key is
|
|
200
|
+
// on the machine when the file has none is the same defect as the silence,
|
|
201
|
+
// pointing the other way. "unknown" IS mentioned: an unparseable file is
|
|
202
|
+
// the one nobody can rule a key out of.
|
|
203
|
+
const holding = found.filter(f => f.holds !== "no-key");
|
|
204
|
+
if (holding.length > 0) {
|
|
205
|
+
console.log(`${PRODUCT}: this deck's LAN private key is still on this machine:`);
|
|
206
|
+
for (const f of holding) {
|
|
207
|
+
console.log(`${PRODUCT}: ${f.path}${f.holds === "unknown" ? ` (unreadable ${gDash} ${f.why})` : ""}`);
|
|
208
|
+
}
|
|
209
|
+
console.log(`${PRODUCT}: every deck you paired with has pinned that key. \`${INVOKED_AS ?? PRODUCT} --uninstall --purge\` removes the file(s) above.`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
162
214
|
// The remedy last and once, after every symptom above it, rather than once
|
|
163
215
|
// per refusal in the middle of the list.
|
|
164
216
|
if (named.size > 0) {
|
|
@@ -191,6 +243,47 @@ if (flags.stop || flags.status || flags.logs || flags.install || flags.installSe
|
|
|
191
243
|
const tone = palette(colorProfile({ isTTY: Boolean(process.stdout.isTTY) }));
|
|
192
244
|
const say = (line) => process.stdout.write(`${line}\n`);
|
|
193
245
|
|
|
246
|
+
// WHAT THE ONE-SHOTS USED TO SWALLOW.
|
|
247
|
+
//
|
|
248
|
+
// reportUnknownFlags and reportIncompleteFlags live below this block and
|
|
249
|
+
// cannot be called from it — they reach for `G`, `P` and `write`, all
|
|
250
|
+
// declared a thousand lines down, so a call here is a temporal dead zone
|
|
251
|
+
// (the same reason the `--port` guard builds its own glyphs, #797). So the
|
|
252
|
+
// rows are written here, with this block's own tone.
|
|
253
|
+
//
|
|
254
|
+
// Without them `--help`'s closing promise — "Anything else on the command
|
|
255
|
+
// line is reported as an unknown option and then ignored" — held for a boot
|
|
256
|
+
// and for none of the nine one-shots: `ccdeck --status --prot 4317` printed
|
|
257
|
+
// the status and never mentioned the flag.
|
|
258
|
+
for (const token of flags.unknown ?? []) {
|
|
259
|
+
process.stderr.write(` ${tone.warn}${gWarn} unknown option${tone.reset} ${token} ${dash} see \`${INVOKED_AS ?? PRODUCT} --help\`\n`);
|
|
260
|
+
}
|
|
261
|
+
for (const { flag, expects } of flags.incomplete ?? []) {
|
|
262
|
+
process.stderr.write(` ${tone.warn}${gWarn} missing value${tone.reset} ${flag} ${dash} expected ${expects}\n`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// AND `--stop` FAILS CLOSED ON A PORT IT CANNOT USE.
|
|
266
|
+
//
|
|
267
|
+
// The selector below reads "no usable port" and "no port asked for" as the
|
|
268
|
+
// same thing, and the second means every deck — so `ccdeck --stop --port
|
|
269
|
+
// 431x`, or `--port $UNSET`, ended every deck on the machine and exited 0.
|
|
270
|
+
// The guard that refuses a bad port sits ~270 lines below this block, which
|
|
271
|
+
// always exits before reaching it.
|
|
272
|
+
//
|
|
273
|
+
// A narrowing flag that fails open to "everything" is the wrong default for
|
|
274
|
+
// an off switch: failing closed costs a retype, failing open costs a deck
|
|
275
|
+
// somebody else was watching.
|
|
276
|
+
if (flags.stop) {
|
|
277
|
+
const askedPort = (flags.incomplete ?? []).some(x => x.flag === "--port" || x.flag === "-p");
|
|
278
|
+
const badPort = flags.port != null && !isPortValue(flags.port);
|
|
279
|
+
if (badPort || askedPort) {
|
|
280
|
+
const shown = badPort ? ` ${flags.port}` : "";
|
|
281
|
+
console.error(`${INVOKED_AS ?? PRODUCT}: --port${shown}: not a port number ${dash} expected 0-65535.`);
|
|
282
|
+
console.error(`${INVOKED_AS ?? PRODUCT}: refusing to stop every deck when you asked for one.`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
194
287
|
const { deckDataDir, deckLogDir } = await import(pathToFileURL(join(PKG_ROOT, "src/server/deck-home.mjs")).href);
|
|
195
288
|
|
|
196
289
|
// ── --install ─────────────────────────────────────────────────────────────
|
|
@@ -309,7 +402,7 @@ if (flags.stop || flags.status || flags.logs || flags.install || flags.installSe
|
|
|
309
402
|
process.exit(0);
|
|
310
403
|
}
|
|
311
404
|
const { canonicalLogPath } = await import(pathToFileURL(join(PKG_ROOT, "src/server/log-writer.mjs")).href);
|
|
312
|
-
const { hasCodexInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
405
|
+
const { hasCodexInstalled, codexHomeField } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
313
406
|
const { hasClaudeInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-dir.mjs")).href);
|
|
314
407
|
const { liveDecks, sameShape } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
315
408
|
|
|
@@ -324,6 +417,12 @@ if (flags.stop || flags.status || flags.logs || flags.install || flags.installSe
|
|
|
324
417
|
codex: hasCodexInstalled(),
|
|
325
418
|
claude: hasClaudeInstalled(),
|
|
326
419
|
};
|
|
420
|
+
// And the Codex tree, the one field of a start's shape that is not a flag. A
|
|
421
|
+
// bare start has passed it since #1123 and replaces a deck on another tree, so
|
|
422
|
+
// without it here every tree matched, and `--status` could mark as "opens this
|
|
423
|
+
// one" exactly the deck the next `ccdeck` stops (#1134). Worked out by the
|
|
424
|
+
// function the start and the discovery record use, so all three agree.
|
|
425
|
+
mine.codexHome = codexHomeField(mine.codex);
|
|
327
426
|
|
|
328
427
|
// ── --logs ────────────────────────────────────────────────────────────────
|
|
329
428
|
// What the deck wrote where a terminal would have shown it. Printed RAW,
|
|
@@ -437,7 +536,11 @@ if (flags.stop || flags.status || flags.logs || flags.install || flags.installSe
|
|
|
437
536
|
say("");
|
|
438
537
|
say(` ${tone.muted}${dash} ${left.length} other deck${left.length === 1 ? "" : "s"} still running:${tone.reset}`);
|
|
439
538
|
for (const d of left) say(` ${tone.muted}pid ${d.pid} ${bullet} ${d.port} ${bullet} ${where(d)}${tone.reset}`);
|
|
440
|
-
|
|
539
|
+
// `--stop --all` used to be named here as the way to reach every deck. It
|
|
540
|
+
// is not: `--all` is a parsed no-op, and a bare `--stop` already ends every
|
|
541
|
+
// deck — which is why this line is only reachable after `--stop --port <n>`
|
|
542
|
+
// narrowed one. Naming the narrowing form is the half that is true.
|
|
543
|
+
say(` ${tone.muted}\`${INVOKED_AS ?? PRODUCT} --stop\` ends every deck ${bullet} \`--stop --port <n>\` ends one${tone.reset}`);
|
|
441
544
|
}
|
|
442
545
|
say("");
|
|
443
546
|
process.exit(refused ? 1 : 0);
|
|
@@ -509,7 +612,18 @@ const { deckDataDir, deckLogDir, legacyDeckDir, migrateDeckFiles, sweepTempFiles
|
|
|
509
612
|
// The litter an atomic write leaves when its process is killed between the
|
|
510
613
|
// write and the rename. Nothing has ever swept it, because the code that
|
|
511
614
|
// makes it is not running any more when it is made.
|
|
512
|
-
|
|
615
|
+
//
|
|
616
|
+
// THE WATCH'S OWN DIRECTORY IS NAMED SEPARATELY because the sweep does not
|
|
617
|
+
// recurse: it walks the directories it is handed and nothing under them, and
|
|
618
|
+
// `browser-watch/` is a subdirectory of `legacy`. Its temp files are the
|
|
619
|
+
// biggest ones the deck makes — about 2.5 MB each with a full 500-episode
|
|
620
|
+
// archive, by that module's own measurement — and until now they were outside
|
|
621
|
+
// the reach of the sweep that exists because ninety-seven of them were found
|
|
622
|
+
// in one directory, the oldest six days old.
|
|
623
|
+
const { storeDir: watchStoreDir } = await import(
|
|
624
|
+
pathToFileURL(join(PKG_ROOT, "src/server/browser-watch-store.mjs")).href
|
|
625
|
+
);
|
|
626
|
+
await sweepTempFiles({ dirs: [legacy, data, log, watchStoreDir()], fs: fsp }).catch(() => 0);
|
|
513
627
|
}
|
|
514
628
|
// CANONICALISED here rather than left as typed: the discovery file publishes
|
|
515
629
|
// this path so the hook can tell which decks share one log and elect a single
|
|
@@ -529,7 +643,7 @@ const persist = flags.noPersist
|
|
|
529
643
|
// share a path still elect one writer for it.
|
|
530
644
|
: canonicalLogPath(flags.history ?? join(deckLogDir(), "events.jsonl"));
|
|
531
645
|
|
|
532
|
-
const { installHooks, keepDiscovery, removeDiscovery, hasCodexInstalled } =
|
|
646
|
+
const { installHooks, keepDiscovery, removeDiscovery, hasCodexInstalled, leftoverCodexHooks, codexHomeField } =
|
|
533
647
|
await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
534
648
|
// CODEX_SESSIONS_DIR comes along because the banner below names the directory
|
|
535
649
|
// the watcher tails, and the watcher lives in that module. Recomputing the path
|
|
@@ -573,6 +687,11 @@ const wantCodex = flags.noCodex
|
|
|
573
687
|
const wantClaude = flags.noClaude
|
|
574
688
|
? false
|
|
575
689
|
: (flags.claude === true || hasClaudeInstalled());
|
|
690
|
+
// The Codex tree this deck would tail, spelled the way its discovery record will
|
|
691
|
+
// spell it — the one field of a start's shape that is not a flag. Resolved here,
|
|
692
|
+
// with the other three, because the second-start question below is asked of
|
|
693
|
+
// nothing but inputs already settled. A --no-codex start reads no tree: null.
|
|
694
|
+
const codexHome = codexHomeField(wantCodex);
|
|
576
695
|
|
|
577
696
|
const WEB_DIST = join(PKG_ROOT, "dist", "web", "index.html");
|
|
578
697
|
if (!existsSync(WEB_DIST)) {
|
|
@@ -642,7 +761,7 @@ process.on("SIGHUP", () => { showCursor(); dieOfSignal("SIGHUP"); });
|
|
|
642
761
|
// into each string as trailing spaces, so any new row, or any label a character
|
|
643
762
|
// longer, silently broke the alignment of every other one.
|
|
644
763
|
const LABELS = [
|
|
645
|
-
"workspace", "Claude hooks", "Codex sessions", "claude-swap", "accounts",
|
|
764
|
+
"workspace", "Claude hooks", "Codex sessions", "Codex hooks", "claude-swap", "accounts",
|
|
646
765
|
"ccusage", "update", "name", "server ready", "log", "unknown option",
|
|
647
766
|
"missing value",
|
|
648
767
|
];
|
|
@@ -791,6 +910,27 @@ function startupWork() {
|
|
|
791
910
|
return { cs, seed: await seedFirstAccount().catch(() => ({ state: "failed" })) };
|
|
792
911
|
})().catch(() => null);
|
|
793
912
|
|
|
913
|
+
// WHEN THE TOOL IS QUIET, which is not the same as when the job above settles
|
|
914
|
+
// (#1043). The auto-switch drives this same claude-swap — every tick is
|
|
915
|
+
// `cswap auto --once`, which moves the user's live Claude credentials — and
|
|
916
|
+
// the server arms it the moment the port binds, while this job is still in
|
|
917
|
+
// ensureCswap. So the server holds every tick until this resolves, and it has
|
|
918
|
+
// two halves because the job covers only the first: ensureCswap answers
|
|
919
|
+
// "upgrading" as soon as it has FIRED an upgrade it deliberately does not
|
|
920
|
+
// await, and that upgrade is a uv or pipx environment being rewritten for
|
|
921
|
+
// tens of seconds after this job has returned. upgradeSettled() is the handle
|
|
922
|
+
// on it, and null when nothing was started.
|
|
923
|
+
//
|
|
924
|
+
// Not markDeckReady, the other place "the boot is over" gets said:
|
|
925
|
+
// reportStartup stops waiting for claude-swap the moment the install
|
|
926
|
+
// announces itself, so the deck is marked ready with the install still
|
|
927
|
+
// running. Never rejects — a job that failed has nothing left running.
|
|
928
|
+
const cswapQuiet = cswap.then(async () => {
|
|
929
|
+
if (!wantClaude) return;
|
|
930
|
+
const { upgradeSettled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/cswap-install.mjs")).href);
|
|
931
|
+
await upgradeSettled();
|
|
932
|
+
}).catch(() => {});
|
|
933
|
+
|
|
794
934
|
// ccusage backs the usage-history modal. Primed at boot rather than on first
|
|
795
935
|
// open so a cold machine pays the install while the deck is still starting.
|
|
796
936
|
// Nothing is lost by skipping the prime: runCcusage falls back to npx, so the
|
|
@@ -814,7 +954,13 @@ function startupWork() {
|
|
|
814
954
|
new Promise(r => setTimeout(() => r(null), 1200)),
|
|
815
955
|
]);
|
|
816
956
|
|
|
817
|
-
|
|
957
|
+
// An older deck's Codex forwarders, looked for and never touched — see
|
|
958
|
+
// leftoverCodexHooks for why a boot names them rather than removing them.
|
|
959
|
+
// Asked with --no-codex too: the entries fire whether or not this deck is
|
|
960
|
+
// watching the rollouts, and one file read is the whole cost.
|
|
961
|
+
const codexHooks = leftoverCodexHooks().catch(() => null);
|
|
962
|
+
|
|
963
|
+
return { hooks, cswap, cswapInstalling, cswapQuiet, ccusage, update, codexHooks };
|
|
818
964
|
}
|
|
819
965
|
|
|
820
966
|
/** The same work, said out loud, in a fixed order — a boot whose rows arrive in
|
|
@@ -872,6 +1018,27 @@ async function reportStartup(jobs) {
|
|
|
872
1018
|
write(row({ label: "Codex sessions", detail: `skipped ${G.dash} no ~/.codex/, or --no-codex` }));
|
|
873
1019
|
}
|
|
874
1020
|
|
|
1021
|
+
// #983. A machine that ran a deck from before #253 still has that deck's
|
|
1022
|
+
// forwarders in Codex's hooks.json, and nothing but `--uninstall` ever looked.
|
|
1023
|
+
// Where Codex honours the file, each one posts the session to /api/event while
|
|
1024
|
+
// the watcher above reads the same session off disk, so it arrives twice. Both
|
|
1025
|
+
// copies go into the ring and events.jsonl; the reducer's two-second
|
|
1026
|
+
// redelivery windows fold some of them on the card and not others, and a
|
|
1027
|
+
// duplicate that shows only sometimes reads as a reducer bug, which is where
|
|
1028
|
+
// it would otherwise be chased. Saying nothing was the one answer the issue
|
|
1029
|
+
// ruled out. So: one row, in the place the deck already talks about Codex,
|
|
1030
|
+
// naming the file and the command. `keep`, because a remedy cut off by an
|
|
1031
|
+
// ellipsis is no remedy. The clause about arriving twice is said only while
|
|
1032
|
+
// the watcher runs, since without it the forwarders are the only copy.
|
|
1033
|
+
const leftover = await jobs.codexHooks;
|
|
1034
|
+
if (leftover) {
|
|
1035
|
+
const twice = wantCodex ? ", so Codex sessions can arrive twice" : "";
|
|
1036
|
+
write(row({
|
|
1037
|
+
mark: G.warn, tone: P.warn, label: "Codex hooks", keep: true,
|
|
1038
|
+
detail: `left by an older deck in ${fileLink(leftover.settingsPath)}${twice} ${G.dash} \`${INVOKED_AS ?? PRODUCT} --uninstall\` takes them out`,
|
|
1039
|
+
}));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
875
1042
|
// Bounded, because this is the job that made a first boot look hung: on a
|
|
876
1043
|
// machine with neither claude-swap nor a Python toolchain it fetches a uv
|
|
877
1044
|
// binary and then builds an environment with it, and the report used to wait
|
|
@@ -969,7 +1136,13 @@ function writeSwapRows(swap, { late = false } = {}) {
|
|
|
969
1136
|
? `installed via ${cs.via} but not on PATH ${G.dash} add ${
|
|
970
1137
|
process.platform === "win32" ? "%USERPROFILE%\\.local\\bin" : "~/.local/bin"
|
|
971
1138
|
}`
|
|
972
|
-
|
|
1139
|
+
// The deck asked for one version and a different one answered, so it
|
|
1140
|
+
// declines to drive it. Naming both is the whole content of the row:
|
|
1141
|
+
// "install failed" would be a lie about an install that succeeded, and
|
|
1142
|
+
// the number that arrived is the thing a person needs to see.
|
|
1143
|
+
: cs?.reason === "unexpected_version"
|
|
1144
|
+
? `refused v${cs.version} ${G.dash} asked for ${cs.want}, not driving it`
|
|
1145
|
+
: `install failed${cs?.via ? ` via ${cs.via}` : ""}`;
|
|
973
1146
|
out += row({ mark: G.fail, tone: P.warn, label: "claude-swap", detail: how });
|
|
974
1147
|
// A URL is not an answer when someone just wants the panel to work. Print
|
|
975
1148
|
// the command for THIS machine, picked from what is already on it.
|
|
@@ -1183,6 +1356,28 @@ let discoveryFile = null;
|
|
|
1183
1356
|
// this deck is registered, and given back by the exit handler on every way out
|
|
1184
1357
|
// before that. See src/server/boot-lock.mjs.
|
|
1185
1358
|
let bootLock = null;
|
|
1359
|
+
// HERE, not at the bottom with SIGINT and SIGTERM, and this line is the whole
|
|
1360
|
+
// of the fix for #980.
|
|
1361
|
+
//
|
|
1362
|
+
// This file is an ES module with top-level `await`, so its statements run in
|
|
1363
|
+
// source order and a handler registered at the bottom does not exist until the
|
|
1364
|
+
// boot has got there. Three `process.exit()` calls sit between the gate below
|
|
1365
|
+
// and that point — the yield when another deck came up first, the ATTACH that
|
|
1366
|
+
// every `ccdeck` typed beside a running deck takes, and the exit of a boot
|
|
1367
|
+
// whose server could not bind — and the explicit release further down is on the
|
|
1368
|
+
// one path none of them take. So the handler that was written to cover "every
|
|
1369
|
+
// way out" covered only the ways out it was already past.
|
|
1370
|
+
//
|
|
1371
|
+
// What the leftover file costs is a start that has to wait the lock out: the
|
|
1372
|
+
// next `ccdeck` finds it, and recovers through `!alive(holder.pid)` — unless
|
|
1373
|
+
// the OS has recycled that pid onto a live process, which Windows does
|
|
1374
|
+
// routinely. Then the only way past is BOOT_LOCK_STALE_MS measured from when
|
|
1375
|
+
// the lock was WRITTEN, so a start a few seconds after an attach polls for the
|
|
1376
|
+
// remainder of thirty before it can so much as ask whether a deck is up.
|
|
1377
|
+
//
|
|
1378
|
+
// Synchronous, because `exit` allows nothing else, and a no-op once the lock
|
|
1379
|
+
// has been given back at the registration below.
|
|
1380
|
+
process.on("exit", () => { bootLock?.release(); });
|
|
1186
1381
|
|
|
1187
1382
|
// This worker does not outlive the supervisor that started it (#702).
|
|
1188
1383
|
//
|
|
@@ -1267,7 +1462,7 @@ bootLock = await takeBootLock({ dir: deckRegistryDir() }).catch(() => null);
|
|
|
1267
1462
|
const askedPort = flags.port != null && isPortValue(flags.port) ? Number(flags.port) : null;
|
|
1268
1463
|
const plan = secondStart({
|
|
1269
1464
|
live: await liveDecks().catch(() => []),
|
|
1270
|
-
want: { workspace, persist, codex: wantCodex, claude: wantClaude },
|
|
1465
|
+
want: { workspace, persist, codex: wantCodex, claude: wantClaude, codexHome },
|
|
1271
1466
|
port: askedPort,
|
|
1272
1467
|
ours: PKG_VERSION,
|
|
1273
1468
|
fresh: flags.new === true,
|
|
@@ -1346,6 +1541,15 @@ bootLock = await takeBootLock({ dir: deckRegistryDir() }).catch(() => null);
|
|
|
1346
1541
|
}
|
|
1347
1542
|
}
|
|
1348
1543
|
|
|
1544
|
+
// The one fact the auto-switch has to wait for and the server cannot see for
|
|
1545
|
+
// itself: that the claude-swap it drives is not being installed or upgraded
|
|
1546
|
+
// underneath it (#1043). startupWork is what knows, and it has not run yet — it
|
|
1547
|
+
// runs beside the server rather than before it (#483) — so the server is handed
|
|
1548
|
+
// a promise here and startupWork settles it below. A respawn runs no startup
|
|
1549
|
+
// work and installs nothing, so there it settles at once. See cswapQuiet.
|
|
1550
|
+
let settleCswap;
|
|
1551
|
+
const cswapQuiet = new Promise(r => { settleCswap = r; });
|
|
1552
|
+
|
|
1349
1553
|
const starting = startServer({
|
|
1350
1554
|
port, persist, workspace, codex: wantCodex, claude: wantClaude,
|
|
1351
1555
|
// Withheld when nothing is supervising us: without a parent, exiting is just
|
|
@@ -1357,6 +1561,8 @@ const starting = startServer({
|
|
|
1357
1561
|
// shutdown() is a hoisted declaration precisely so it is callable from the
|
|
1358
1562
|
// first instruction of this module — see the long note beside it (#448).
|
|
1359
1563
|
onStop: () => shutdown(0),
|
|
1564
|
+
// Every auto-switch tick waits on this until claude-swap is quiet — see above.
|
|
1565
|
+
cswapQuiet,
|
|
1360
1566
|
}).then(s => ({ ok: true, s }), err => ({ ok: false, err }));
|
|
1361
1567
|
|
|
1362
1568
|
// Once-per-session setup — hook install, tool probes, registry lookups, and the
|
|
@@ -1365,8 +1571,11 @@ const starting = startServer({
|
|
|
1365
1571
|
// restart that feels instant and one that makes you wonder whether it worked.
|
|
1366
1572
|
if (!RESPAWN) {
|
|
1367
1573
|
const jobs = startupWork();
|
|
1574
|
+
jobs.cswapQuiet.then(settleCswap);
|
|
1368
1575
|
await printBanner();
|
|
1369
1576
|
await reportStartup(jobs);
|
|
1577
|
+
} else {
|
|
1578
|
+
settleCswap();
|
|
1370
1579
|
}
|
|
1371
1580
|
|
|
1372
1581
|
// Usually settled long ago by the time we get here, which is the point: `step`
|
|
@@ -1507,9 +1716,6 @@ if (!RESPAWN) {
|
|
|
1507
1716
|
script: join(PKG_ROOT, "bin", "agent-dag.js"),
|
|
1508
1717
|
logPath: join(deckLogDir(), "deck.log"),
|
|
1509
1718
|
product: PRODUCT,
|
|
1510
|
-
// The directories that decide WHICH deck this is, so the one started at
|
|
1511
|
-
// login is the one started from this shell — see scopeEnv.
|
|
1512
|
-
serviceEnv: svc.scopeEnv(process.env),
|
|
1513
1719
|
});
|
|
1514
1720
|
svc.writeServiceRecord(deckDataDir(), out.ok
|
|
1515
1721
|
? { installed: PKG_VERSION, at: new Date().toISOString(), path: out.path }
|
|
@@ -1661,6 +1867,25 @@ async function shutdown(code = 0) {
|
|
|
1661
1867
|
if (tty && code !== RESTART_CODE && code !== UPGRADE_CODE) {
|
|
1662
1868
|
write(`\n\n ${P.warn}${G.stop} shutting down${G.ellipsis}${P.reset}\n`);
|
|
1663
1869
|
}
|
|
1870
|
+
// WHAT THIS DECK STARTED, ENDED WITH IT. Every deadline in exec.mjs lives
|
|
1871
|
+
// in a timer in THIS process — `run` states the outcome and only then kills
|
|
1872
|
+
// — so an exit that reaps nothing leaves the hung tool a deadline exists
|
|
1873
|
+
// for with nothing left anywhere that will ever stop it. Measured on a
|
|
1874
|
+
// sandboxed deck (#1012): SIGINT at 05:26:22.706, deck gone within 200ms,
|
|
1875
|
+
// and its `claude --print /usage` child — spawned at 05:26:18.078 under a
|
|
1876
|
+
// 15-second deadline — still running at 05:26:35.204, reparented to init.
|
|
1877
|
+
// A whole Claude Code process, orphaned by a Ctrl+C.
|
|
1878
|
+
//
|
|
1879
|
+
// FIRST, so it is ahead of both exits below, and cheap enough to be: a
|
|
1880
|
+
// POSIX signal lands synchronously, and on Windows killTree's taskkill is
|
|
1881
|
+
// already a running process by the time spawn() returns and outlives this
|
|
1882
|
+
// one. No corpse is waited for, and the only await is a module already in
|
|
1883
|
+
// the cache — anything that spawned a child loaded exec.mjs to do it — so
|
|
1884
|
+
// the ~200ms exit that is the rest of shutdown's good behaviour stays that.
|
|
1885
|
+
try {
|
|
1886
|
+
const { killLiveChildren } = await import(pathToFileURL(join(PKG_ROOT, "src/server/exec.mjs")).href);
|
|
1887
|
+
killLiveChildren();
|
|
1888
|
+
} catch { /* exec.mjs never loaded, so nothing was ever spawned */ }
|
|
1664
1889
|
// Stopped first, always: a tick landing after the unlink would re-register a
|
|
1665
1890
|
// deck that is on its way out, and leave the file behind for the hooks to
|
|
1666
1891
|
// find once nothing is listening.
|
|
@@ -1679,6 +1904,18 @@ async function shutdown(code = 0) {
|
|
|
1679
1904
|
await discovery?.stop();
|
|
1680
1905
|
if (discoveryFile) await removeDiscovery(discoveryFile);
|
|
1681
1906
|
} catch { /* the sweep at the next boot gets it */ }
|
|
1907
|
+
// THE LINES WE ALREADY SAID WE HAD. /api/event answers {ok:true, seq}
|
|
1908
|
+
// before the append lands — right for the hook, which holds a 1.9s cap and
|
|
1909
|
+
// must not wait on a filesystem — so an exit that waits only for the
|
|
1910
|
+
// listener abandons whatever is still queued. Measured: 40 posts
|
|
1911
|
+
// acknowledged, 12 written. The restart path is the one that costs most,
|
|
1912
|
+
// since the replacement deck rebuilds its canvas from the log before it
|
|
1913
|
+
// binds. Bounded inside drainAppends, so a wedged filesystem cannot hold
|
|
1914
|
+
// the exit; a deadline reached is the old behaviour.
|
|
1915
|
+
try {
|
|
1916
|
+
const { drainAppends } = await import(pathToFileURL(join(PKG_ROOT, "src/server/log-writer.mjs")).href);
|
|
1917
|
+
await drainAppends();
|
|
1918
|
+
} catch { /* nothing queued, or the module never loaded */ }
|
|
1682
1919
|
// No server yet means nothing to drain and nothing to hand the port over to,
|
|
1683
1920
|
// so the exit is the whole of the shutdown.
|
|
1684
1921
|
if (!server) return process.exit(code);
|
|
@@ -1696,11 +1933,10 @@ async function shutdown(code = 0) {
|
|
|
1696
1933
|
process.on("SIGINT", () => shutdown(0));
|
|
1697
1934
|
process.on("SIGTERM", () => shutdown(0));
|
|
1698
1935
|
process.on("beforeExit", () => { discovery?.stop(); if (discoveryFile) removeDiscovery(discoveryFile); });
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
// before
|
|
1702
|
-
//
|
|
1703
|
-
process.on("exit", () => { bootLock?.release(); });
|
|
1936
|
+
// The boot lock's `exit` handler is NOT here with its siblings. It is armed up
|
|
1937
|
+
// beside `let bootLock = null;`, above the start gate, because the three exits
|
|
1938
|
+
// this file takes before reaching this line are the ones it exists for — see
|
|
1939
|
+
// the note there (#980).
|
|
1704
1940
|
|
|
1705
1941
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
1706
1942
|
|
|
@@ -1807,8 +2043,9 @@ Options:
|
|
|
1807
2043
|
--uninstall-service Stop starting at login. \`--uninstall\` does this too
|
|
1808
2044
|
--workspace <path> Only capture sessions whose cwd is inside <path>
|
|
1809
2045
|
--scope Restrict to current working directory
|
|
1810
|
-
--all Capture every session (default).
|
|
1811
|
-
|
|
2046
|
+
--all Capture every session (default). Accepted and
|
|
2047
|
+
ignored — it is what a bare run already does, and
|
|
2048
|
+
\`--stop --port <n>\` is the only way to narrow a stop
|
|
1812
2049
|
--history <path> Override events log file (default: this platform's log directory)
|
|
1813
2050
|
--no-persist Don't write or replay events log (RAM-only)
|
|
1814
2051
|
--codex Force-enable Codex capture even if ~/.codex/ missing
|
|
@@ -1817,8 +2054,14 @@ Options:
|
|
|
1817
2054
|
--no-claude Skip Claude entirely: no hooks, no claude-swap, no accounts panel
|
|
1818
2055
|
--uninstall Remove ${PRODUCT}'s hooks from ~/.claude/settings.json and
|
|
1819
2056
|
~/.codex/hooks.json, and restore any sound hooks of yours it parked.
|
|
1820
|
-
Hook entries only: the forwarder script
|
|
1821
|
-
the
|
|
2057
|
+
Hook entries only: the forwarder script under
|
|
2058
|
+
~/.claude/agent-dag/, the deck's own state and log
|
|
2059
|
+
directories (\`--status\` names them), ~/.agents-deck/
|
|
2060
|
+
and claude-swap all stay. It NAMES the prefs.json
|
|
2061
|
+
files that still hold this deck's LAN private key
|
|
2062
|
+
--purge With --uninstall: delete those prefs.json files too, so the
|
|
2063
|
+
private key every deck you paired with has pinned does not
|
|
2064
|
+
outlive the uninstall. Your ${PRODUCT} settings go with them
|
|
1822
2065
|
-h, --help Show this help
|
|
1823
2066
|
-v, --version Print the version and exit
|
|
1824
2067
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as l,j as e,u as se,h as te,s as ne,i as ae}from"./index-Dsca2TwS.js";import{S as ve,k as ye}from"./index-Dsca2TwS.js";function ie(n){const i=/^#?([0-9a-f]{6})$/i.exec((n??"").trim());if(!i)return!1;const o=parseInt(i[1],16),r=[o>>16&255,o>>8&255,o&255].map(b=>{const t=b/255;return t<=.03928?t/12.92:Math.pow((t+.055)/1.055,2.4)});return .2126*r[0]+.7152*r[1]+.0722*r[2]>.5}const V=1e4,re=12e3;function oe({browsers:n,findings:i,watching:o,palette:r}){const b=l.useRef(null),t=l.useRef(null);return t.current={browsers:n,findings:i,watching:o},l.useEffect(()=>{var L;const w=b.current;if(!w)return;const a=w.getContext("2d");if(!a)return;const v=((L=window.matchMedia)==null?void 0:L.call(window,"(prefers-reduced-motion: reduce)").matches)??!1;let y=0,d=!1;const N=()=>{var H;if(d)return;const W=t.current;if(!W)return;const{browsers:D,findings:U,watching:O}=W,B=r["--accent"]||"#7dd3fc",G=r["--line"]||"#1f2229",_=r["--text-dim"]||"#6e727c",P=ie(r["--bg"]),F=P?42:26,J=P?.85:.65,A=Date.now(),k=Math.min(window.devicePixelRatio||1,2),M=w.clientWidth,R=w.clientHeight;(w.width!==M*k||w.height!==R*k)&&(w.width=M*k,w.height=R*k),a.setTransform(k,0,0,k,0,0),a.clearRect(0,0,M,R);const m=M/2,g=R/2,p=Math.min(M,R)/2-6;a.strokeStyle=G,a.lineWidth=1,a.globalAlpha=P?1:.9;for(const h of[.4,.7,1])a.beginPath(),a.arc(m,g,p*h,0,Math.PI*2),a.stroke();a.beginPath(),a.moveTo(m-p,g),a.lineTo(m+p,g),a.moveTo(m,g-p),a.lineTo(m,g+p),a.stroke(),a.globalAlpha=1;const j=(O&&!v?A%V/V:0)*Math.PI*2-Math.PI/2;if(O){const h=(H=a.createConicGradient)==null?void 0:H.call(a,j-.85,m,g);h&&(h.addColorStop(0,"transparent"),h.addColorStop(.85/(Math.PI*2),`color-mix(in srgb, ${B} ${F}%, transparent)`),h.addColorStop(1,"transparent"),a.fillStyle=h,a.beginPath(),a.moveTo(m,g),a.arc(m,g,p,j-.85,j),a.closePath(),a.fill()),a.strokeStyle=B,a.globalAlpha=J,a.beginPath(),a.moveTo(m,g),a.lineTo(m+Math.cos(j)*p,g+Math.sin(j)*p),a.stroke(),a.globalAlpha=1}D.forEach((h,S)=>{const $=S/Math.max(D.length,1)*Math.PI*2-Math.PI/2,T=h.lastReadMs===null?1:Math.min(1,(A-h.lastReadMs)/12e4),E=p*(.42+T*.5),C=m+Math.cos($)*E,I=g+Math.sin($)*E,q=Math.abs((j-$+Math.PI*3)%(Math.PI*2)-Math.PI),K=O?Math.max(0,1-q/.55):0;a.beginPath(),a.arc(C,I,3+K*1.6,0,Math.PI*2),a.fillStyle=h.running?B:_,a.globalAlpha=h.running?.55+K*.45:.35,a.fill(),a.globalAlpha=1});for(const h of U){const S=(A-h.atMs)/re;if(S<0||S>1)continue;const T=Math.max(0,D.findIndex(I=>I.key===h.browser))/Math.max(D.length,1)*Math.PI*2-Math.PI/2,E=m+Math.cos(T)*p*.6,C=g+Math.sin(T)*p*.6;a.beginPath(),a.arc(E,C,4+S*22,0,Math.PI*2),a.strokeStyle=B,a.globalAlpha=(1-S)*.8,a.lineWidth=1.5,a.stroke(),a.globalAlpha=1}y=requestAnimationFrame(N)};return N(),()=>{d=!0,cancelAnimationFrame(y)}},[r]),e.jsx("canvas",{ref:b,className:"bw-radar",role:"img","aria-label":o?`Watching ${n.length} browser${n.length===1?"":"s"}`:"Not watching"})}function le(n,i,o){if(n===null)return null;const r=n+i-o;return r>0?r:null}function ce(n){const i=Math.ceil(n/1e3),o=Math.floor(i/60),r=i%60;return o>0?`${o}m ${r}s`:`${r}s`}function Z(n){return(n??[]).filter(i=>i.installed&&i.profiles>0)}function z(n,i){return i?{kind:"saving",word:"Saving",detail:""}:n.settings.enabled?{kind:"on",word:"Watching",detail:""}:{kind:"off",word:"Paused",detail:"nothing new is recorded"}}function de(n){return!Number.isFinite(n)||n<=0?"empty":n<1024?`${Math.round(n)} B`:n<1024*1024?`${Math.max(1,Math.round(n/1024))} KB`:`${(n/1024/1024).toFixed(1)} MB`}function he(n,i){return n.map(o=>({key:o.key,name:o.name,running:o.running,visits:i.filter(r=>r.browser===o.key).reduce((r,b)=>r+b.programVisits,0)}))}function ue(n,i){if(n===null)return null;const o=Math.max(0,Math.round((i-n)/1e3));if(o<5)return"just now";if(o<60)return`${o} sec ago`;const r=Math.round(o/60);return r<60?`${r} min ago`:`${Math.round(r/60)} hr ago`}function we(n){const i=n.profiles.filter(r=>r.degraded);if(i.length>0){const r=i[0],b=i.length-1;return{kind:"unreadable",text:`${r.name}/${r.profile} could not be read — ${r.reason??"no reason given"}`+(b>0?`, and ${b} more`:"")+". The watch continues on the profiles it can read."}}const o=Z(n.browsers);return o.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."}:o.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(n){const i=o=>new Date(o).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1});return n.count===1||n.endMs-n.startMs<6e4?i(n.startMs):`${i(n.startMs)} → ${i(n.endMs)}`}function pe(n){return new Date(n).toLocaleDateString(void 0,{weekday:"short",day:"numeric",month:"short"})}function X(n){const i=Math.round((n.endMs-n.startMs)/6e4);return i<1?"":`${i} min`}function fe({onClose:n,onSeen:i,onWatching:o,palette:r}){const b=se(n),[t,w]=l.useState(null),[a,v]=l.useState(null),[y,d]=l.useState(null),[N,L]=l.useState(!1),W=l.useRef(!1),[D,U]=l.useState(null),[O,B]=l.useState(null),[G,_]=l.useState(!1),P=l.useRef(!1),[F,J]=l.useState(!1),[A,k]=l.useState(!1),[M,R]=l.useState(!1),[m,g]=l.useState(!1),[p,Y]=l.useState(!1),[j,H]=l.useState(()=>Date.now());l.useEffect(()=>{const s=setInterval(()=>H(Date.now()),1e3);return()=>clearInterval(s)},[]);const h=l.useCallback(async s=>{var c,x;if(!(s&&!te(W.current))){W.current=s,L(!0);try{const u=await fetch(`/api/browser-watch${s?"?refresh=1":""}`);if(!u.ok)throw new Error(`the deck answered ${u.status}`);const f=await u.json();w(f),P.current||U(((c=f==null?void 0:f.settings)==null?void 0:c.quietMinutes)??15),o(((x=f==null?void 0:f.settings)==null?void 0:x.enabled)===!0),v(null)}catch(u){v(u instanceof Error?u.message:String(u))}finally{W.current=!1,L(!1)}}},[o]);l.useEffect(()=>{h(!1)},[h]),l.useEffect(()=>{const s=setInterval(()=>{h(!1)},1e4);return()=>clearInterval(s)},[h]);const S=l.useCallback(async s=>{try{await fetch("/api/browser-watch/dismiss",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({host:s.host,startMs:s.startMs})})}finally{await h(!1)}},[h]),$=l.useCallback(async s=>{P.current=!0,_(!0);try{const c=await fetch("/api/browser-watch",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(s)});if(!c.ok)throw new Error(`the deck answered ${c.status}`);await h(!1),d(null)}catch(c){d(c instanceof Error?c.message:String(c))}finally{P.current=!1,_(!1)}},[h]),T=l.useRef(i);T.current=i,l.useEffect(()=>()=>T.current(Date.now()),[]);const E=l.useMemo(()=>{const s=[];for(const c of(t==null?void 0:t.episodes)??[]){const x=pe(c.startMs),u=s[s.length-1];u&&u.label===x?u.episodes.push(c):s.push({label:x,episodes:[c]})}return s},[t]),C=Z(t==null?void 0:t.browsers),I=((t==null?void 0:t.browsers)??[]).filter(s=>!(s.installed&&s.profiles>0)),q=t?we(t):null,K=C.reduce((s,c)=>s+c.profiles,0),Q=t?le(t.coverage.lastHumanMs,t.coverage.quietMs,j):null;return e.jsx("div",{className:"modal-backdrop",onClick:n,role:"presentation",children:e.jsxs("div",{ref:b,className:"modal bw-modal",onClick:s=>s.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:t?`${K} ${K===1?"profile":"profiles"} · local only`:"local only"})]}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{className:"glyph-btn",onClick:()=>void h(!0),...ne(N),"aria-label":"Re-read every profile now",title:"Re-read every profile now",children:N?"…":"↻"}),e.jsx("button",{className:"glyph-btn",onClick:n,"aria-label":"Close (Esc)",title:"Close (Esc)",children:"×"})]})]}),e.jsxs("div",{className:"modal-body bw-body",children:[!t&&!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})]})}),t&&q&&e.jsx("p",{className:`bw-trouble ${q.kind}`,role:"status",children:q.text}),t&&e.jsxs("div",{className:"bw-work",children:[e.jsxs("aside",{className:"bw-side",children:[e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Activity overview",e.jsx("button",{className:"bw-help",onClick:()=>R(s=>!s),"aria-expanded":M,"aria-label":"What the sweep and the dots mean",children:"?"})]}),e.jsxs("div",{className:"bw-glance",children:[e.jsx(oe,{browsers:C.map(s=>{var c;return{key:s.key,name:s.name,running:s.running,lastReadMs:((c=t.profiles.find(x=>x.browser===s.key))==null?void 0:c.lastWrittenMs)??null}}),findings:t.episodes.slice(0,6).map(s=>({browser:s.browser??null,atMs:s.endMs})),watching:t.settings.enabled,palette:r}),e.jsx("ul",{className:"bw-counts",children:he(C,t.profiles).map(s=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-count-n",children:s.visits.toLocaleString("en-US")}),e.jsx("span",{className:"bw-count-of",children:s.name})]},s.key))})]}),e.jsx("p",{className:"bw-since",children:"Pages a program opened, since this deck started"}),t.coverage.checkedMs>0&&e.jsxs("p",{className:"bw-since",children:["Last checked ",ue(t.coverage.checkedMs,j),t.coverage.checks>0&&e.jsxs(e.Fragment,{children:[" · ",t.coverage.checks.toLocaleString("en-US")," ",t.coverage.checks===1?"check":"checks"]})]}),Q!==null&&e.jsxs("p",{className:"bw-gate",children:["You are browsing — a program page would count in ",ce(Q)]}),M&&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"})]})]}),e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Watched profiles",e.jsx("button",{className:"bw-help",onClick:()=>g(s=>!s),"aria-expanded":m,"aria-label":"What running, idle and connected to Anthropic mean",children:"?"})]}),m&&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:C.map(s=>e.jsxs("li",{children:[e.jsx("span",{className:`bw-prof-dot${s.running?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-prof-name",children:s.name}),e.jsx("span",{className:"bw-prof-state",children:s.running?"running":"idle"}),s.running&&s.relay.state==="live"&&e.jsx("span",{className:"bw-relay",children:"connected to Anthropic"})]},s.key))}),I.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"bw-why bw-rest-head",onClick:()=>Y(s=>!s),"aria-expanded":p,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:p?"▾":"▸"})," ",I.length," not watched"]}),p&&e.jsxs("p",{className:"bw-rest",children:[I.map(s=>s.name).join(", ")," — not installed, or installed and never opened, so there is no history to read."]})]})]}),t.relay&&e.jsx(be,{relay:t.relay})]}),e.jsxs("div",{className:"bw-main",children:[e.jsxs("section",{className:"bw-findings",children:[e.jsxs("h4",{className:"bw-sec-head bw-head-row",children:["Findings",t.episodes.length>0&&e.jsxs("span",{className:"bw-feed-count",children:[t.episodes.length," ",t.episodes.length===1?"episode":"episodes"]})]}),e.jsx("div",{className:"bw-eps",children:E.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"," ",t.settings.quietMinutes," ",t.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."]}),!t.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."})]}):E.map(s=>e.jsxs("div",{className:"bw-day",children:[e.jsx("h4",{className:"bw-sec-head",children:s.label}),s.episodes.map(c=>{const x=`${c.host}-${c.startMs}`,u=O===x;return e.jsxs("div",{className:`bw-ep${u?" open":""}`,children:[e.jsxs("div",{className:"bw-ep-row",children:[e.jsxs("button",{className:"bw-ep-head",onClick:()=>B(u?null:x),"aria-expanded":u,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:u?"▾":"▸"}),e.jsx("span",{className:"bw-ep-host",children:c.host}),e.jsxs("span",{className:"bw-ep-meta",children:[me(c),X(c)&&e.jsxs(e.Fragment,{children:[" · ",X(c)]})]}),e.jsxs("span",{className:"bw-ep-count",children:[c.count," ",c.count===1?"page":"pages"]})]}),e.jsx("button",{className:"glyph-btn bw-ep-x",onClick:()=>void S(c),"aria-label":`Dismiss ${c.host}`,title:"Dismiss — it leaves this list for good, and stays in the log file",children:"×"})]}),u&&e.jsx("ul",{className:"bw-urls",children:c.urls.map((f,ee)=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-url-time",children:new Date(f.timeMs).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1})}),e.jsx("span",{className:"bw-url",children:f.url})]},`${f.url}-${f.timeMs}-${ee}`))})]},x)})]},s.label))})]}),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:[t.log.length," ",t.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:t.log.length===0?e.jsx("p",{className:"bw-note",children:"The deck writes a line here each time it looks at a profile."}):t.log.map(s=>e.jsxs("div",{className:`bw-log-line ${s.level}${s.parts?"":" sys"}`,children:[e.jsx("span",{className:"bw-log-time",children:new Date(s.atMs).toLocaleTimeString("en-GB",{hour12:!1})}),s.parts?e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"bw-log-browser",children:s.parts.browser}),e.jsx("span",{className:"bw-log-profile",title:s.parts.profile,children:s.parts.profile}),e.jsxs("span",{className:"bw-log-value",children:[s.parts.value,s.parts.flagged>0&&e.jsxs("span",{className:"bw-log-flag",children:[" · ",s.parts.flagged," flagged"]})]})]}):e.jsx("span",{className:"bw-log-sys",children:s.text})]},`${s.atMs}-${s.level}-${s.text}`))})]})]})]}),t&&F&&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:D??t.settings.quietMinutes,onChange:s=>{U(Number(s.target.value)),$({quietMinutes:Number(s.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:t.settings.reaction,onChange:s=>void $({reaction:s.target.value}),disabled:!t.settings.enabled,title:t.settings.enabled?"What to do besides writing it down.":"Turn watching on to arm a reaction.",children:(t.reactions??["notify"]).map(s=>e.jsx("option",{value:s,children:s==="notify"?"notify me":s==="close-tab"?"close the tab":"quit the browser"},s))})]}),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:()=>k(s=>!s),"aria-expanded":A,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:A?"▾":"▸"})," What Browser Watch can access"]}),A&&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(t.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:t.coverage.logPath}),typeof t.coverage.logBytes=="number"?` (${de(t.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."})]})]})]}),y&&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:[y," — the switches below show what is stored, not what you pressed."]}),e.jsx("button",{className:"glyph-btn bw-write-err-x",onClick:()=>d(null),"aria-label":"Dismiss",title:"Dismiss",children:"×"})]})}),t&&e.jsxs("footer",{className:"bw-status",children:[e.jsx("span",{className:`bw-mode-dot${t.settings.enabled?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-mode-word",children:z(t,G).word},z(t,G).kind),e.jsx("span",{className:"bw-status-text",children:t.coverage.archived===0?t.settings.enabled?"No episodes recorded yet":"No episodes on disk":`${t.coverage.archived} ${t.coverage.archived===1?"episode":"episodes"} on disk`}),e.jsx("button",{className:`glyph-btn bw-gear${F?" on":""}`,onClick:()=>J(s=>!s),"aria-expanded":F,"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":t.settings.enabled,"aria-labelledby":"bw-enabled-label",className:"switch",onClick:()=>void $({enabled:!t.settings.enabled}),title:t.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 be({relay:n}){const[i,o]=l.useState(!1),[r,b]=l.useState(!1),t=n.killswitch.blocked?"unblock":"block",w=n.command[t],a=l.useCallback(async()=>{await ae(w.command)&&(b(!0),window.setTimeout(()=>b(!1),1600))},[w.command]),v=n.profiles.filter(d=>{var N;return(N=d.report)==null?void 0:N.present}),y=n.profiles.filter(d=>d.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 ${n.verdict}`,role:"status",children:n.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.":n.verdict==="protected"?`The relay is black-holed in ${n.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."}),n.killswitch.foreign.length>0&&e.jsxs("p",{className:"bw-rc-foreign",children:["Another entry for this host is already in ",n.hostsPath,", and ccdeck will not touch it:",n.killswitch.foreign.map(d=>e.jsx("code",{className:"bw-path",children:d},d))]}),e.jsxs("button",{className:"bw-why",onClick:()=>o(d=>!d),"aria-expanded":i,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:i?"▾":"▸"})," ",n.verdict==="protected"?"How to undo the block":"What it can reach, and how to stop it"]}),i&&e.jsxs("div",{className:"bw-rc-body",children:[v.length>0&&e.jsx("dl",{className:"bw-key",children:v.map(d=>e.jsxs(l.Fragment,{children:[e.jsxs("dt",{children:[d.name,d.profile==="Default"?"":` · ${d.profile}`]}),e.jsxs("dd",{children:[d.report.enabled?"enabled":"installed but switched off",d.report.allUrls?", can act on every site":", limited to the sites it was granted",d.report.sensitiveApis.length>0&&e.jsxs(e.Fragment,{children:[" — ",d.report.sensitiveApis.join(", ")]})]})]},`${d.browser}/${d.profile}`))}),y.length>0&&e.jsxs("p",{className:"bw-rc-note",children:[y.map(d=>d.name).join(", ")," — its settings file could not be read, so nothing is claimed about it either way."]}),!n.hostsRead&&e.jsxs("p",{className:"bw-rc-note",children:[n.hostsPath," could not be read, so whether the relay is already blocked is unknown."]}),v.some(d=>d.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:w.command}),e.jsx("button",{className:"btn bw-rc-copy",onClick:a,children:r?"Copied":"Copy"})]}),e.jsx("p",{className:"bw-rc-note",children:w.note})]})]})}export{ve as SEEN_KEY,ue as agoLabel,le as armsIn,fe as default,de as logBytesLabel,ye as unseenEpisodes,ce as untilLabel,he as visitTotals,we as watchTrouble,Z as watchedBrowsers};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as z,f as j,r as c,u as J,j as e,b as K,s as D,e as Q,c as X,d as x,g as F,h as Y,p as Z}from"./index-Dsca2TwS.js";function _(){let t=0;return{begin(){const a=++t;return()=>a===t},cancel(){t++}}}function ee(t){return!t.answer||t.answer.range!==t.want?{phase:"busy",stale:!1}:t.answer.ok?{phase:t.answer.days>0?"chart":"empty",stale:t.loading}:{phase:"error",stale:t.loading}}function I(t){const a=new Map;for(const n of t)for(const o of n.agents??[]){if(!o||typeof o.agent!="string"||o.agent==="")continue;const r=a.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,a.set(o.agent,r)}return[...a.values()].sort((n,o)=>o.cost-n.cost||n.id.localeCompare(o.id))}function se(t){const a=I([{agents:t}]);return a.length<2?null:a.map(n=>`${z(n.id)} ${j(n.cost)}`).join(" · ")}function U(t){switch(t.toLowerCase()){case"claude":return"var(--usage-purple)";case"codex":return"var(--usage-orange)";case"gemini":return"var(--usage-indigo)";case"copilot":return"var(--usage-green)";default:return"var(--usage-zinc)"}}function ae(t,a){if(!(a>0)||!(t>0))return"0%";const n=t/a*100;return n<.1?"<0.1%":`${n.toFixed(1)}%`}const d=t=>typeof t=="number"&&Number.isFinite(t)?t:0,L=t=>typeof t=="string"?t:"",O=t=>Array.isArray(t)?t.filter(a=>typeof a=="string"):[];function te(t){const a=t??{};return{modelName:L(a.modelName),cost:d(a.cost),inputTokens:d(a.inputTokens),outputTokens:d(a.outputTokens),cacheCreationTokens:d(a.cacheCreationTokens),cacheReadTokens:d(a.cacheReadTokens)}}function ne(t){const a=t??{},n=Array.isArray(a.agents)?a.agents.map(o=>{const r=o??{};return{agent:L(r.agent),totalCost:d(r.totalCost),totalTokens:d(r.totalTokens)}}):void 0;return{period:L(a.period),totalCost:d(a.totalCost),totalTokens:d(a.totalTokens),inputTokens:d(a.inputTokens),outputTokens:d(a.outputTokens),cacheCreationTokens:d(a.cacheCreationTokens),cacheReadTokens:d(a.cacheReadTokens),modelsUsed:O(a.modelsUsed),modelBreakdowns:Array.isArray(a.modelBreakdowns)?a.modelBreakdowns.map(te):[],...n?{agents:n}:{},...a.metadata&&typeof a.metadata=="object"?{metadata:{agents:O(a.metadata.agents)}}:{}}}function oe(t){const a=t??{};return{...a,ok:a.ok===!0,days:Array.isArray(a.days)?a.days.map(ne):void 0}}function M(t){const a=t.toLowerCase();return a.includes("opus")?"var(--usage-purple)":a.includes("sonnet")?"var(--usage-blue)":a.includes("haiku")?"var(--usage-green)":a.includes("gpt-5")||a.includes("gpt5")?"var(--usage-amber)":a.includes("gpt")?"var(--usage-red)":a.includes("gemini")?"var(--usage-indigo)":a.includes("codex")?"var(--usage-orange)":"var(--usage-zinc)"}const le=[7,14,30,90];function re(t){const[a,n]=c.useState(null),[o,r]=c.useState(!1),C=c.useState(_)[0],h=c.useRef(!1),f=(y=!1)=>{if(y&&!Y(h.current))return;const b=C.begin();h.current=y;const T=t;r(!0);const i=`/api/ccusage?since=${Z(T)}${y?"&refresh=1":""}`;fetch(i).then(v=>v.json()).then(v=>{b()&&n({range:T,resp:oe(v)})}).catch(()=>{b()&&n({range:T,resp:{ok:!1,reason:"unreachable"}})}).finally(()=>{b()&&(h.current=!1,r(!1))})};return c.useEffect(()=>(f(!1),()=>C.cancel()),[t]),{landed:a,loading:o,reload:()=>f(!0)}}function ie({onClose:t,providers:a}){var P;const[n,o]=c.useState(30),[r,C]=c.useState(null),{landed:h,loading:f,reload:y}=re(n),b=c.useRef(null),T=J(t,{focusRef:b}),g=ee({loading:f,want:n,answer:h&&{range:h.range,ok:h.resp.ok===!0,days:((P=h.resp.days)==null?void 0:P.length)??0}}),i=g.phase==="chart"?h.resp.days??[]:[],v=c.useMemo(()=>i.reduce((s,l)=>Math.max(s,l.totalCost),0),[i]),B=c.useRef(null);c.useLayoutEffect(()=>{const s=B.current;s&&(s.scrollLeft=s.scrollWidth)},[i]);const{totalCost:R,totalTok:q,inOut:G,cacheRead:H,modelCosts:E}=c.useMemo(()=>{let s=0,l=0,k=0,p=0;const w=new Map;for(const m of i){s+=m.totalCost,l+=m.totalTokens,k+=m.inputTokens+m.outputTokens,p+=m.cacheReadTokens;for(const N of m.modelBreakdowns)w.set(N.modelName,(w.get(N.modelName)??0)+N.cost)}return{totalCost:s,totalTok:l,inOut:k,cacheRead:p,modelCosts:w}},[i]),V=c.useMemo(()=>Array.from(E.entries()).sort((s,l)=>l[1]-s[1]),[E]),$=c.useMemo(()=>I(i),[i]),W=$.length>1,u=r?i.find(s=>s.period===r)??null:null;return e.jsx("div",{className:"modal-backdrop",onClick:t,role:"presentation",children:e.jsxs("div",{ref:T,className:"modal uh-modal",onClick:s=>s.stopPropagation(),role:"dialog","aria-modal":"true","aria-label":"Usage history",children:[e.jsxs("header",{className:"uh-head",children:[e.jsxs("div",{className:"uh-titlewrap",children:[e.jsx("div",{className:"uh-title",children:"Usage history"}),e.jsx("div",{className:"uh-sub",children:K(a,$.map(s=>s.id))})]}),e.jsx("div",{className:"uh-range",role:"group","aria-label":"Range",children:le.map(s=>e.jsxs("button",{type:"button","aria-pressed":n===s,className:"uh-range-btn",onClick:()=>{o(s),C(null)},children:[s,"d"]},s))}),e.jsx("button",{className:"glyph-btn uh-reload",onClick:y,...D(f),title:"Re-run ccusage","aria-label":"Reload",children:f?"…":"↻"}),e.jsx("button",{ref:b,className:"glyph-btn uh-close",onClick:t,"aria-label":"Close",children:"×"})]}),g.phase==="busy"?e.jsxs("div",{className:"uh-status uh-status-wait","aria-busy":"true",children:[e.jsx("span",{className:"uh-wait-word",children:h?"running ccusage…":"running ccusage… (first run downloads the package)"}),e.jsx("span",{className:"uh-wait-axis","aria-hidden":!0})]}):g.phase==="error"?e.jsxs("div",{className:"uh-status uh-err",title:X(h.resp)||void 0,children:[Q(h.resp,"ccusage did not report usage"),e.jsx("div",{children:e.jsx("button",{className:"btn uh-retry",onClick:y,...D(f),children:f?"trying…":"Try again"})})]}):g.phase==="empty"?e.jsx("div",{className:`uh-status${g.stale?" uh-stale":""}`,"aria-busy":g.stale||void 0,children:"no usage in this range"}):e.jsxs("div",{className:g.stale?"uh-stale":void 0,"aria-busy":g.stale||void 0,children:[e.jsxs("div",{className:"uh-totals",children:[e.jsx(S,{label:"total cost",val:j(R),accent:!0}),e.jsx(S,{label:"tokens",val:x(q)}),e.jsx(S,{label:"input+output",val:x(G)}),e.jsx(S,{label:"cache reads",val:x(H)})]}),W&&e.jsxs("div",{className:"uh-agents",role:"group","aria-label":"Cost by CLI",children:[e.jsx("div",{className:"uh-agent-bar","aria-hidden":"true",children:$.map(s=>e.jsx("span",{className:"uh-agent-seg",style:{width:`${R>0?s.cost/R*100:0}%`,background:U(s.id)}},s.id))}),e.jsx("div",{className:"uh-agent-keys",children:$.map(s=>e.jsxs("span",{className:"uh-agent-key",title:`${s.id} · ${x(s.tokens)} tokens`,children:[e.jsx("span",{className:"uh-legend-dot",style:{background:U(s.id)}}),z(s.id),e.jsx("span",{className:"uh-agent-cost",children:j(s.cost)}),e.jsx("span",{className:"uh-agent-share",children:ae(s.cost,R)})]},s.id))})]}),e.jsx("div",{ref:B,className:"uh-chart",role:"group","aria-label":"Daily cost by model",children:i.map((s,l)=>{const k=v>0?s.totalCost/v*100:0,p=s.period===r,w=`${s.period}, ${j(s.totalCost)}`;return e.jsxs("button",{className:`uh-bar-col${p?" sel":""}`,onClick:()=>C(p?null:s.period),"aria-pressed":p,"aria-label":w,title:`${s.period} · ${j(s.totalCost)}`,style:{flexBasis:`${100/i.length}%`,"--uh-i":l,"--uh-step":`${Math.min(10,200/Math.max(i.length,1))}ms`},children:[e.jsx("div",{className:"uh-bar",style:{height:`${Math.max(k,s.totalCost>0?2:0)}%`},children:s.modelBreakdowns.slice().sort((m,N)=>N.cost-m.cost).map(m=>{const N=s.totalCost>0?m.cost/s.totalCost*100:0;return e.jsx("div",{className:"uh-bar-seg",style:{height:`${N}%`,background:M(m.modelName)}},m.modelName)})}),e.jsx("span",{className:"uh-bar-label",children:s.period.slice(5)})]},s.period)})}),e.jsx("div",{className:"uh-legend",children:V.map(([s,l])=>e.jsxs("span",{className:"uh-legend-item",title:s,children:[e.jsx("span",{className:"uh-legend-dot",style:{background:M(s)}}),F(s)," ",e.jsx("span",{className:"uh-legend-cost",children:j(l)})]},s))}),u&&e.jsxs("div",{className:"uh-detail",children:[e.jsxs("div",{className:"uh-detail-head",children:[e.jsx("span",{className:"uh-detail-date",children:u.period}),e.jsx("span",{className:"uh-detail-cost",children:j(u.totalCost)}),(()=>{var k,p;const l=se(u.agents)??((p=(k=u.metadata)==null?void 0:k.agents)!=null&&p.length?u.metadata.agents.join(" · "):null);return l?e.jsx("span",{className:"uh-detail-agents",title:l,children:l}):null})()]}),e.jsxs("div",{className:"uh-detail-mini",children:[e.jsx(A,{label:"input",val:x(u.inputTokens)}),e.jsx(A,{label:"output",val:x(u.outputTokens)}),e.jsx(A,{label:"cache write",val:x(u.cacheCreationTokens)}),e.jsx(A,{label:"cache read",val:x(u.cacheReadTokens)})]}),e.jsx("div",{className:"uh-detail-models",children:u.modelBreakdowns.slice().sort((s,l)=>l.cost-s.cost).map(s=>{const l=u.totalCost>0?s.cost/u.totalCost*100:0;return e.jsxs("div",{className:"uh-model-row",title:s.modelName,children:[e.jsxs("span",{className:"uh-model-name",children:[e.jsx("span",{className:"uh-legend-dot",style:{background:M(s.modelName)}}),e.jsx("span",{className:"uh-model-label",children:F(s.modelName)})]}),e.jsx("span",{className:"uh-model-bar",children:e.jsx("span",{className:"uh-model-bar-fill",style:{width:`${l}%`,background:M(s.modelName)}})}),e.jsx("span",{className:"uh-model-cost",children:j(s.cost)})]},s.modelName)})})]})]})]})})}function S({label:t,val:a,accent:n}){return e.jsxs("div",{className:"uh-stat",children:[e.jsx("span",{className:`uh-stat-val${n?" accent":""}`,children:a}),e.jsx("span",{className:"uh-stat-label",children:t})]})}function A({label:t,val:a}){return e.jsxs("div",{className:"uh-ministat",children:[e.jsx("span",{className:"uh-ministat-val",children:a}),e.jsx("span",{className:"uh-ministat-label",children:t})]})}export{ie as default};
|