ccdeck 3.32.1 → 3.33.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 +14 -12
- package/bin/agent-dag.js +6 -0
- package/bin/cli/help.js +2 -0
- package/bin/cli/one-shot.js +32 -13
- package/bin/cli/second-start.js +1 -0
- package/bin/cli/startup.js +34 -0
- package/bin/cli/uninstall.js +47 -2
- package/bin/deck.js +12 -1
- package/dist/web/assets/BrowserWatchModal-B3UjvY82.js +1 -0
- package/dist/web/assets/UsageHistoryModal-DxNI8gR6.js +1 -0
- package/dist/web/assets/index-BIANlQ9A.css +1 -0
- package/dist/web/assets/index-OLDoLpOW.js +299 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +40 -0
- package/src/server/account-projects.mjs +11 -2
- package/src/server/args.mjs +12 -2
- package/src/server/auto-update.mjs +14 -1
- package/src/server/best-of-nostalgia.mjs +2 -1
- package/src/server/block-notify.mjs +114 -11
- package/src/server/browser-history.mjs +81 -4
- package/src/server/browser-presence.mjs +98 -2
- package/src/server/browser-react.mjs +39 -4
- package/src/server/browser-watch-seen.mjs +35 -1
- package/src/server/browser-watch.mjs +105 -18
- package/src/server/cafe-music-bgm.mjs +2 -1
- package/src/server/ccusage-install.mjs +8 -4
- package/src/server/ccusage.mjs +3 -1
- package/src/server/claude-accounts.mjs +38 -16
- package/src/server/codex-dir.mjs +8 -6
- package/src/server/codex-quota.mjs +4 -2
- package/src/server/codex-usage.mjs +93 -38
- package/src/server/codex-watch.mjs +150 -18
- package/src/server/cswap-auto-loop.mjs +15 -2
- package/src/server/cswap-auto-readers.mjs +10 -4
- package/src/server/cswap-auto.mjs +6 -6
- package/src/server/cswap-install.mjs +118 -3
- package/src/server/deck-prefs.mjs +4 -0
- package/src/server/event-pipeline.mjs +3 -0
- package/src/server/event-ring.mjs +2 -2
- package/src/server/good-life-radio.mjs +2 -1
- package/src/server/index.mjs +3 -2
- package/src/server/installer.mjs +62 -12
- package/src/server/lan-beacon.mjs +6 -4
- package/src/server/lan-call.mjs +4 -0
- package/src/server/lan-dials.mjs +10 -1
- package/src/server/lan-engine.mjs +221 -44
- package/src/server/lan-requests.mjs +88 -5
- package/src/server/lan-round-record.mjs +21 -4
- package/src/server/lan-socket.mjs +105 -15
- package/src/server/lan-sync.mjs +18 -4
- package/src/server/lifecycle.mjs +21 -2
- package/src/server/live-radio-mix.mjs +2 -1
- package/src/server/log-replay.mjs +55 -24
- package/src/server/login-service.mjs +38 -3
- package/src/server/metrics-run.mjs +5 -3
- package/src/server/network-sampler.mjs +25 -2
- package/src/server/npm-upgrade.mjs +8 -1
- package/src/server/npx.mjs +32 -5
- package/src/server/process-list.mjs +103 -13
- package/src/server/purge-key.mjs +52 -24
- package/src/server/request-gates.mjs +47 -14
- package/src/server/retire-sound-hook.mjs +56 -9
- package/src/server/ring-bounds.mjs +10 -0
- package/src/server/running-deck.mjs +72 -2
- package/src/server/session-enrichment.mjs +40 -8
- package/src/server/stop-deck.mjs +13 -4
- package/src/server/system-metrics.mjs +2 -2
- package/src/server/transcript-scan.mjs +27 -6
- package/dist/web/assets/BrowserWatchModal-ohP7oQCN.js +0 -1
- package/dist/web/assets/UsageHistoryModal-CNjMaM59.js +0 -1
- package/dist/web/assets/index-DKn7nc15.css +0 -1
- package/dist/web/assets/index-P2zWn3Hw.js +0 -299
package/README.md
CHANGED
|
@@ -85,10 +85,10 @@ The deck opens on these eight pictures the first time it runs — they are the w
|
|
|
85
85
|
| | |
|
|
86
86
|
|---|---|
|
|
87
87
|
| **Blocked on you** | A permission prompt, or a finished turn waiting for your next instruction, sorts that session to the top of the sidebar with how long it has been stuck — longest wait first, so the oldest block is the first row. A permission prompt also puts a count in the topbar that jumps straight to it. Claude Code only — the deck reads Codex from its rollout log, and a rollout carries no such signal. |
|
|
88
|
-
| **Live DAG** | Nodes are agents
|
|
88
|
+
| **Live DAG** | Nodes are agents and edges are spawns; each agent's latest tool calls sit beside its node and light up while they run. In-flight edges animate, settled ones dim. |
|
|
89
89
|
| **Both providers, one canvas** | Claude Code through hooks, Codex through its rollout log. The model chip (`Opus 5`, `GPT-5.5`) tells them apart. |
|
|
90
90
|
| **Cost and quota, live** | Spend per model and per session, plus Claude and Codex quota windows as they refill. |
|
|
91
|
-
| **
|
|
91
|
+
| **Double-click to inspect** | Any node opens its prompt, tool calls, token usage and timing in the side panel. A single click selects it and frames its session. |
|
|
92
92
|
| **Survives restarts** | Events are appended to this platform's log directory (see `--history` below) and replayed on open. |
|
|
93
93
|
| **Accounts without a terminal** | Sign a new Claude account in, move one or your whole set to another machine, rename, reorder, remove — from the panel. |
|
|
94
94
|
| **Logins that repair each other** | A Claude login that expires on one of your machines is copied back from another machine on the same network that still has it — see [Local network](#local-network). |
|
|
@@ -116,7 +116,7 @@ What the deck does write, and the short list of what does leave the machine, is
|
|
|
116
116
|
|
|
117
117
|
### Desktop app
|
|
118
118
|
|
|
119
|
-
The same deck as an app: it starts the deck itself
|
|
119
|
+
The same deck as an app: it starts the deck itself and puts an icon in the menu bar (the tray on Windows and Linux) that counts the Claude Code sessions stopped on a permission prompt or a question — the number sits beside the icon on macOS, and in the icon's tooltip and menu on Windows and Linux. With **Notifications while closed** ticked in that menu, which starts off, it also sends a notification while the window is closed wherever an open page would have played a sound, with the deck's own tone on macOS. It needs no Node.js. If a deck from `npx ccdeck` is already running, the app uses that one rather than starting a second, and replaces it only when it is older than the one the app carries.
|
|
120
120
|
|
|
121
121
|
| System | Download |
|
|
122
122
|
| --- | --- |
|
|
@@ -129,7 +129,7 @@ The same deck as an app: it starts the deck itself, puts an icon in the menu bar
|
|
|
129
129
|
- **Windows** — installs for your user, with no admin prompt. It is not signed yet, so SmartScreen asks once: More info, then Run anyway.
|
|
130
130
|
- **Linux** — on Debian and Ubuntu take the **.deb**: it installs with a double click and puts ccdeck in the applications menu. The AppImage is one file for every other distribution, and a browser saves it without the permission to run, so it does nothing at all until you give it one back — `chmod +x ccdeck-linux-x86_64.AppImage`, then open it. The icon also needs a tray to sit in: KDE and waybar have one, and GNOME needs the AppIndicator extension.
|
|
131
131
|
|
|
132
|
-
The app keeps itself current from this repository's releases, and installs an update only if it carries ccdeck's own signature. It installs on Quit,
|
|
132
|
+
The app keeps itself current from this repository's releases, and installs an update only if it carries ccdeck's own signature. It installs on Quit, or by itself once the app has been left alone for a minute — its window closed or not in focus, and no deck starting — and restarts into the new version. That does not wait for your agents to go idle: the deck is gone for a second or two, which can leave a gap in the drawing of a turn, and the agents carry on. On the .deb, where installing asks for your password, it installs only when you choose Restart to update.
|
|
133
133
|
|
|
134
134
|
## Requirements
|
|
135
135
|
|
|
@@ -209,20 +209,20 @@ The Accounts panel reads the store [claude-swap](https://pypi.org/project/claude
|
|
|
209
209
|
|
|
210
210
|
The deck installs that package itself, so it installs it **bounded**: `claude-swap~=0.26`, which is `>= 0.26, == 0.*`, and when PyPI can be reached the exact version it resolved rather than the range. What `cswap --version` reports afterwards has to be that version, or the deck says so and leaves the panel dark rather than driving a copy it cannot account for — this is the tool that holds your Claude logins. The daily upgrade is bounded by the same specifier, and what it left behind is written to `~/.agents-deck/cswap-upgrade.json`.
|
|
211
211
|
|
|
212
|
-
**`+` → Sign in** runs `claude auth login`, shows you the link,
|
|
212
|
+
**`+` → Sign in** runs `claude auth login`, shows you the link, and hands the result to `cswap add`. The sign-in usually completes by itself once you approve it in the browser; only when the page shows a code — as it does on a deck opened from another machine — is there one to paste back into the dialog. The account you were using **stays active** — signing in replaces the live credentials, so the previous one is switched back the moment the new one is recorded. A pasted code goes straight into the CLI's stdin on this machine; it is never stored, logged, or sent anywhere else.
|
|
213
213
|
|
|
214
214
|
**`share`** on an account produces a `ccdeck2:…` blob to paste into another deck's **`+` → Paste a share**.
|
|
215
215
|
|
|
216
216
|
**`↗`** in the panel header does the same for a set of them, which is what moving your accounts from home to work actually is. Tick the ones to send — all of them to start — and one blob carries the set. The dialog counts sign-in tokens rather than rows, and an account that cannot be exported is named rather than quietly dropped, so the number on the copy button is always the number in the blob.
|
|
217
217
|
|
|
218
|
-
**On macOS, the login itself lives in the Keychain.** If ccdeck is running from SSH, a LaunchAgent, or another session that cannot open that Keychain, the account may still be valid while this process cannot read or share it. ccdeck
|
|
218
|
+
**On macOS, the login itself lives in the Keychain.** If ccdeck is running from SSH, a LaunchAgent, or another session that cannot open that Keychain, the account may still be valid while this process cannot read or share it. ccdeck does not call that login expired: the account's row says **Keychain unreadable**, and Local network says **cannot share here**. It refuses clipboard and LAN exports from it, and tells you to start ccdeck from a Terminal window on the Mac itself. Once claude-swap can read the Keychain again, the account becomes shareable normally.
|
|
219
219
|
|
|
220
|
-
An import adds what is missing and leaves a working account exactly as it is. The one it does rewrite unasked is a slot claude-swap has itself quarantined as refresh-token-dead, which is what heals a machine whose login stopped working. The result names every account in the paste — imported, already here,
|
|
220
|
+
An import adds what is missing and leaves a working account exactly as it is. The one it does rewrite unasked is a slot claude-swap has itself quarantined as refresh-token-dead, which is what heals a machine whose login stopped working. The result names every account in the paste — imported, already here, dead token replaced, or not imported — and an account it skipped can be overwritten one at a time with **update anyway**.
|
|
221
221
|
|
|
222
222
|
> [!WARNING]
|
|
223
223
|
> A share carries the **live login of every account in it, in the clear** — claude-swap's export format has no encryption, and five ticked boxes is five passwords on your clipboard. It expires ten minutes after it is made and imports refuse it after that. While it lives, treat it as those passwords: anything that can read your clipboard can read the accounts.
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
**Rename**, **Move to slot…** and **Remove** are on the same row menu. Removal takes two clicks and cannot be undone.
|
|
226
226
|
|
|
227
227
|
## Local network
|
|
228
228
|
|
|
@@ -286,6 +286,8 @@ ccdeck [options]
|
|
|
286
286
|
--install-service Start the deck when you log in. Set up on first run;
|
|
287
287
|
this is only for putting it back
|
|
288
288
|
--uninstall-service Stop starting at login (`--uninstall` does this too)
|
|
289
|
+
--at-login What the login item starts the deck with: beside a
|
|
290
|
+
deck that is already running, it leaves that one be
|
|
289
291
|
--codex Force Codex capture even if ~/.codex/ is missing
|
|
290
292
|
--no-codex Skip Codex capture (Claude only)
|
|
291
293
|
--claude Force Claude capture even if Claude Code wasn't found
|
|
@@ -366,7 +368,7 @@ them, the other one's hooks, installs and panels are skipped rather than shown
|
|
|
366
368
|
empty — the boot banner says which way it went, and `--claude` / `--codex`
|
|
367
369
|
override it if the guess is wrong.
|
|
368
370
|
|
|
369
|
-
`--workspace` is a filter this deck applies to itself, not a claim on the sessions it matches: **every** running deck whose workspace contains a session's directory draws that session
|
|
371
|
+
`--workspace` is a filter this deck applies to itself, not a claim on the sessions it matches: **every** running deck whose workspace contains a session's directory draws that session. That is not a way to keep a machine-wide deck and one scoped to `~/proj` side by side: in one Claude config directory the second start stops the first and takes its place, as above. The filter reads the same way on all three paths a session can reach the canvas by — Claude Code's hook, Codex's rollout files, and the boot replay of the events log — and the events log still gets exactly one copy of each event, whichever decks are up. A relative path is resolved against the directory you start the deck in, and once, so every path scopes to the same tree. The log is machine-wide by default and shared by every deck on the box, so a scoped deck replays only the part of it that is inside its own workspace: it comes up showing what it will go on to capture, and nothing else.
|
|
370
372
|
|
|
371
373
|
That one events log is also the reason Clear is not quite the per-deck button it looks like. The decks elect a single writer for each log file, and only that deck may empty it: Clear on any other deck wipes its own canvas and leaves the file to the deck that writes it. The confirmation says which of the two you are about to do, and how many decks share the log when it is yours to empty — so `--history` or `--no-persist` gives a deck a log of its own if you want Clear to answer to nobody else.
|
|
372
374
|
|
|
@@ -401,7 +403,7 @@ Being told to restart after an upgrade is local only — no network involved —
|
|
|
401
403
|
npx ccdeck --uninstall
|
|
402
404
|
```
|
|
403
405
|
|
|
404
|
-
Removes every hook entry ccdeck injected from `~/.claude/settings.json`, and `~/.codex/hooks.json` if present — and the login item, if this machine had one.
|
|
406
|
+
Removes every hook entry ccdeck injected from `~/.claude/settings.json`, and `~/.codex/hooks.json` if present — and the login item, if this machine had one — and then stops every deck still running, with a line for each. Those two exceptions to the narrowness below are deliberate: a login item left behind would keep starting a deck whose hooks had just been removed, and a deck left running would put them back itself on its next update. A deck that cannot be stopped makes the command exit 1.
|
|
405
407
|
|
|
406
408
|
It removes the hook entries and nothing else. The forwarder script
|
|
407
409
|
(`~/.claude/agent-dag/hook.js`), the discovery directory around it, the events
|
|
@@ -469,9 +471,9 @@ It restarts on its own only after 30 seconds with nothing running, because hook
|
|
|
469
471
|
## Design
|
|
470
472
|
|
|
471
473
|
- Node = agent (root session or subagent)
|
|
472
|
-
- Edge = parent → child (spawn)
|
|
474
|
+
- Edge = parent → child (spawn); tool calls sit beside their agent's node
|
|
473
475
|
- In-flight animates; settled dims
|
|
474
|
-
-
|
|
476
|
+
- Double-click a node for the full story
|
|
475
477
|
|
|
476
478
|
## Names
|
|
477
479
|
|
package/bin/agent-dag.js
CHANGED
|
@@ -206,6 +206,12 @@ function launch(respawn) {
|
|
|
206
206
|
// claude-swap probe with its 8s timeout, the ccusage prime). Repeating it
|
|
207
207
|
// is what would make a restart feel like a restart.
|
|
208
208
|
AGENTS_DECK_RESPAWN: respawn ? "1" : "",
|
|
209
|
+
// Except when the package under it has changed since (#1735). VERSION is
|
|
210
|
+
// read once, when this process started, so it is the version the first
|
|
211
|
+
// worker booted on and installed the hooks with; a respawn running a
|
|
212
|
+
// different one installs them again. Empty for "we could not tell",
|
|
213
|
+
// which the worker reads as different.
|
|
214
|
+
AGENTS_DECK_BOOT_VERSION: VERSION === "?" ? "" : VERSION,
|
|
209
215
|
AGENTS_DECK_RESTARTS: String(restarts),
|
|
210
216
|
// Empty for "we could not tell", which knownCommand reads as unknown just
|
|
211
217
|
// like an absent one. Set on the worker's environment only: the npx
|
package/bin/cli/help.js
CHANGED
|
@@ -32,6 +32,8 @@ Options:
|
|
|
32
32
|
--install-service Start the deck when you log in. Set up on first run;
|
|
33
33
|
this is only for putting it back
|
|
34
34
|
--uninstall-service Stop starting at login. \`--uninstall\` does this too
|
|
35
|
+
--at-login What the login item starts the deck with. Beside a
|
|
36
|
+
deck that is already running, it leaves that one be
|
|
35
37
|
--workspace <path> Only capture sessions whose cwd is inside <path>
|
|
36
38
|
--scope Restrict to current working directory
|
|
37
39
|
--all Capture every session (default). Accepted and
|
package/bin/cli/one-shot.js
CHANGED
|
@@ -37,10 +37,14 @@ export async function oneShot(flags) {
|
|
|
37
37
|
if (flags.installService || flags.uninstallService) return loginItemCommand(flags, { ...voice, deckDataDir, deckLogDir });
|
|
38
38
|
if (flags.logs) return printLogs(deckLogDir, voice);
|
|
39
39
|
|
|
40
|
-
const { liveDecks } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
40
|
+
const { liveDecks, restartingDecks } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
41
41
|
const decks = await liveDecks().catch(() => []);
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// And the deck a crash took down, whose supervisor is about to bring it back
|
|
43
|
+
// (#1779). Left off, `--stop` inside that wait said nothing was running and
|
|
44
|
+
// the deck came back by itself a few seconds later.
|
|
45
|
+
const restarting = await restartingDecks().catch(() => []);
|
|
46
|
+
if (flags.status) return printStatus([...decks, ...restarting], await defaultShape(deckLogDir), voice);
|
|
47
|
+
return stopDecks(flags, [...decks, ...restarting], voice);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/** The screen's glyphs and palette, by the names the one-shots use, and `say`. */
|
|
@@ -54,6 +58,8 @@ function oneShotVoice() {
|
|
|
54
58
|
const age = (d) => sinceLabel(Date.now() - Date.parse(d.startedAt ?? ""));
|
|
55
59
|
const where = (d) => (d.workspace ? d.workspace : "(all)");
|
|
56
60
|
const url = (d) => `http://127.0.0.1:${d.port}`;
|
|
61
|
+
// A deck between workers is its supervisor: the worker's pid is a dead one.
|
|
62
|
+
const pidOf = (d) => (d.restarting ? d.parent : d.pid);
|
|
57
63
|
|
|
58
64
|
// WHAT THE ONE-SHOTS USED TO SWALLOW.
|
|
59
65
|
//
|
|
@@ -87,8 +93,13 @@ function reportFlagRows(flags, { tone, gWarn, dash }) {
|
|
|
87
93
|
// an off switch: failing closed costs a retype, failing open costs a deck
|
|
88
94
|
// somebody else was watching. So this answers true, having said why, when
|
|
89
95
|
// `--stop` was pointed at a port it cannot use.
|
|
96
|
+
//
|
|
97
|
+
// And a port-shaped token the parser could not read at all — `--port4317`,
|
|
98
|
+
// `--ports 4317` — which lands in `unknown` with `flags.port` undefined, the
|
|
99
|
+
// same "every deck" by a third road (#1780). Its row above has already named it.
|
|
90
100
|
function refusesPort(flags, { dash }) {
|
|
91
|
-
const askedPort = (flags.incomplete ?? []).some(x => x.flag === "--port" || x.flag === "-p")
|
|
101
|
+
const askedPort = (flags.incomplete ?? []).some(x => x.flag === "--port" || x.flag === "-p")
|
|
102
|
+
|| (flags.unknown ?? []).some(t => /^(-p|--port)/.test(String(t)));
|
|
92
103
|
const badPort = flags.port != null && !isPortValue(flags.port);
|
|
93
104
|
if (badPort || askedPort) {
|
|
94
105
|
const shown = badPort ? ` ${flags.port}` : "";
|
|
@@ -165,14 +176,19 @@ async function printStatus(decks, mine, { say, tone, dash, gOk, bullet, arrow })
|
|
|
165
176
|
return 0;
|
|
166
177
|
}
|
|
167
178
|
// The one a bare `ccdeck` would open is marked, because with two decks up
|
|
168
|
-
// that is the only question this command is really being asked.
|
|
169
|
-
|
|
179
|
+
// that is the only question this command is really being asked. Never a
|
|
180
|
+
// deck between workers: a start typed now finds nothing to open there.
|
|
181
|
+
const opens = decks.find(d => !d.restarting && sameShape(d, mine)) ?? null;
|
|
170
182
|
say("");
|
|
171
183
|
for (const d of decks) {
|
|
172
184
|
// The version chunk is dropped rather than printed as "v?" for a deck too
|
|
173
185
|
// old to publish one — same rule as the attach line, and for the same
|
|
174
186
|
// reason: a question mark beside two real facts reads as a fault.
|
|
175
|
-
|
|
187
|
+
//
|
|
188
|
+
// A deck between workers is named by its supervisor, the one process of
|
|
189
|
+
// it still running, and says what it is doing instead of how long it has
|
|
190
|
+
// been up.
|
|
191
|
+
const head = [d.version ? `v${d.version}` : "", `pid ${pidOf(d)}`, d.restarting ? "restarting after a crash" : `up ${age(d)}`]
|
|
176
192
|
.filter(Boolean).join(` ${bullet} `);
|
|
177
193
|
const mark = d === opens ? `${tone.ok}${gOk}${tone.reset}` : `${tone.muted}${bullet}${tone.reset}`;
|
|
178
194
|
const tail = d === opens ? `${tone.muted} ${arrow} \`${COMMAND}\` opens this one${tone.reset}` : "";
|
|
@@ -222,18 +238,21 @@ async function stopDecks(flags, decks, { say, tone, dash, gOk, gWarn, bullet })
|
|
|
222
238
|
const out = await stopDeck(d);
|
|
223
239
|
if (!out.ok) {
|
|
224
240
|
refused = true;
|
|
225
|
-
say(` ${tone.err}${gWarn} could not stop pid ${d
|
|
241
|
+
say(` ${tone.err}${gWarn} could not stop pid ${pidOf(d)} on ${d.port} ${dash} ${out.reason}${tone.reset}`);
|
|
226
242
|
continue;
|
|
227
243
|
}
|
|
228
244
|
// HOW it went out, not just that it did. "asked" means the deck closed its
|
|
229
245
|
// listener, unlinked its registration and left the LAN cleanly; anything
|
|
230
246
|
// else means none of that happened and the next boot has litter to sweep.
|
|
247
|
+
// A deck between workers had nothing to ask, and is said to be that.
|
|
231
248
|
const how = out.how === "asked"
|
|
232
249
|
? ""
|
|
233
|
-
:
|
|
234
|
-
? ` ${tone.muted}(${out.how} ${dash}
|
|
235
|
-
:
|
|
236
|
-
|
|
250
|
+
: d.restarting
|
|
251
|
+
? ` ${tone.muted}(${out.how} ${dash} it was restarting after a crash)${tone.reset}`
|
|
252
|
+
: out.old
|
|
253
|
+
? ` ${tone.muted}(${out.how} ${dash} that deck predates \`--stop\`)${tone.reset}`
|
|
254
|
+
: ` ${tone.muted}(${out.how} ${dash} it did not answer)${tone.reset}`;
|
|
255
|
+
say(` ${tone.ok}${gOk}${tone.reset} stopped${tone.muted} ${bullet} pid ${pidOf(d)} ${bullet} port ${d.port} ${bullet} was up ${was}${tone.reset}${how}`);
|
|
237
256
|
}
|
|
238
257
|
|
|
239
258
|
// What is still up, named. A command that ends one of three decks and says
|
|
@@ -242,7 +261,7 @@ async function stopDecks(flags, decks, { say, tone, dash, gOk, gWarn, bullet })
|
|
|
242
261
|
if (left.length) {
|
|
243
262
|
say("");
|
|
244
263
|
say(` ${tone.muted}${dash} ${left.length} other deck${left.length === 1 ? "" : "s"} still running:${tone.reset}`);
|
|
245
|
-
for (const d of left) say(` ${tone.muted}pid ${d
|
|
264
|
+
for (const d of left) say(` ${tone.muted}pid ${pidOf(d)} ${bullet} ${d.port} ${bullet} ${where(d)}${tone.reset}`);
|
|
246
265
|
// `--stop --all` used to be named here as the way to reach every deck. It
|
|
247
266
|
// is not: `--all` is a parsed no-op, and a bare `--stop` already ends every
|
|
248
267
|
// deck — which is why this line is only reachable after `--stop --port <n>`
|
package/bin/cli/second-start.js
CHANGED
|
@@ -44,6 +44,7 @@ export async function settleSecondStart({ flags, workspace, persist, wantCodex,
|
|
|
44
44
|
ours: PKG_VERSION,
|
|
45
45
|
fresh: flags.new === true,
|
|
46
46
|
respawn: RESPAWN,
|
|
47
|
+
atLogin: flags.atLogin === true,
|
|
47
48
|
});
|
|
48
49
|
if (plan.act === "yield") {
|
|
49
50
|
console.error(`${PRODUCT}: a deck started on ${plan.deck.port} while this one was coming back ${G.dash} leaving it to that one.`);
|
package/bin/cli/startup.js
CHANGED
|
@@ -140,6 +140,40 @@ export function startupWork({ wantClaude, installHooks, leftoverCodexHooks }) {
|
|
|
140
140
|
return { hooks, cswap, cswapInstalling, cswapQuiet, ccusage, update, codexHooks };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The one piece of startupWork a respawn still owes: the hooks, when the
|
|
145
|
+
* package under the session has changed since they were installed (#1735).
|
|
146
|
+
*
|
|
147
|
+
* A global-install update relaunches the worker from the new files with
|
|
148
|
+
* AGENTS_DECK_RESPAWN=1, and a respawn skips startupWork — which is the only
|
|
149
|
+
* caller of installHooks, and so the only code that puts this version's hook.js
|
|
150
|
+
* and its package.json beside Claude Code and rewrites the entries. The deck
|
|
151
|
+
* came back on the new version while every tool call still ran the previous
|
|
152
|
+
* version's forwarder, until the supervisor itself restarted: for a deck in the
|
|
153
|
+
* background, the next login.
|
|
154
|
+
*
|
|
155
|
+
* `bootVersion` is the version the supervisor started on, which is the one the
|
|
156
|
+
* first worker installed with. The same version is the same session continuing
|
|
157
|
+
* and installs nothing, which keeps a restart the second it is. Anything else —
|
|
158
|
+
* including no answer, which is a supervisor from before this variable and so
|
|
159
|
+
* precisely the one running on the first update after it — installs again.
|
|
160
|
+
* installHooks writes nothing that is already identical, so a second respawn on
|
|
161
|
+
* the new version costs a few reads.
|
|
162
|
+
*
|
|
163
|
+
* A failure is one line and the restart goes on: the deck is serving, and the
|
|
164
|
+
* previous forwarder, which is still in place, still reaches it.
|
|
165
|
+
*/
|
|
166
|
+
export async function respawnHooks({ wantClaude, installHooks, bootVersion }) {
|
|
167
|
+
if (!wantClaude || bootVersion === PKG_VERSION) return null;
|
|
168
|
+
try {
|
|
169
|
+
return await installHooks({ provider: "claude" });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const why = String(err?.message ?? err).split("\n")[0];
|
|
172
|
+
console.error(`${PRODUCT}: Claude hooks not updated to v${PKG_VERSION} ${G.dash} ${why}`);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
143
177
|
/** The same work, said out loud, in a fixed order — a boot whose rows arrive in
|
|
144
178
|
* whatever order the network settled is a boot nobody can scan twice. */
|
|
145
179
|
export async function reportStartup(jobs, { workspace, wantClaude, wantCodex, CODEX_SESSIONS_DIR }) {
|
package/bin/cli/uninstall.js
CHANGED
|
@@ -14,8 +14,9 @@ import { G } from "./screen.js";
|
|
|
14
14
|
import { removedRecord } from "./login-item.js";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Remove this deck's hooks
|
|
18
|
-
* holding its LAN private key — and
|
|
17
|
+
* Remove this deck's hooks and its login item, stop every deck still running
|
|
18
|
+
* and, with `flags.purge`, remove the files holding its LAN private key — and
|
|
19
|
+
* say what happened to each.
|
|
19
20
|
*
|
|
20
21
|
* Resolves to the exit code rather than exiting, so the caller owns the
|
|
21
22
|
* process: 1 when any half of it refused, 0 otherwise.
|
|
@@ -109,6 +110,19 @@ export async function uninstall(flags) {
|
|
|
109
110
|
if (hasCodexInstalled()) {
|
|
110
111
|
report(await uninstallHooks({ provider: "codex" }), "Codex");
|
|
111
112
|
}
|
|
113
|
+
// ── THE DECKS STILL RUNNING ────────────────────────────────────────────────
|
|
114
|
+
//
|
|
115
|
+
// Since 3.20 the deck runs in the background, and an uninstall that left it
|
|
116
|
+
// running was undone by it (#1736). On an npx install the idle auto-update
|
|
117
|
+
// relaunches through `npx -y ccdeck@latest`, which is a new supervisor and a
|
|
118
|
+
// full first boot, and a first boot installs every hook again — the argument
|
|
119
|
+
// the login item above is removed on, made by the deck itself. With --purge it
|
|
120
|
+
// also went on pairing and answering peers under a key the user had just been
|
|
121
|
+
// told was deleted.
|
|
122
|
+
//
|
|
123
|
+
// HERE, and the position is the point: after every hook is out, and before
|
|
124
|
+
// --purge takes the key files, so no deck is left running to write them back.
|
|
125
|
+
if (!(await stopLiveDecks())) refused = true;
|
|
112
126
|
// ── THE PRIVATE KEY ────────────────────────────────────────────────────────
|
|
113
127
|
//
|
|
114
128
|
// The one thing left on the disk that is a CREDENTIAL rather than data. Every
|
|
@@ -167,3 +181,34 @@ export async function uninstall(flags) {
|
|
|
167
181
|
// CI step that runs this stops on the failure instead of continuing past it.
|
|
168
182
|
return refused ? 1 : 0;
|
|
169
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Stop every live deck on this machine, one line per deck, and answer whether
|
|
187
|
+
* all of them went.
|
|
188
|
+
*
|
|
189
|
+
* `--stop`'s own list and its own ladder — liveDecks, which challenges every
|
|
190
|
+
* record, and stopDeck, which asks, then signals, then kills — so the two
|
|
191
|
+
* commands cannot disagree about which decks exist or how one is ended. Silent
|
|
192
|
+
* when none is running. A stop that throws is one that failed, never a reason
|
|
193
|
+
* for the rest of the uninstall not to happen.
|
|
194
|
+
*/
|
|
195
|
+
export async function stopLiveDecks({
|
|
196
|
+
list = async () => (await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href)).liveDecks(),
|
|
197
|
+
stop = async (d) => (await import(pathToFileURL(join(PKG_ROOT, "src/server/stop-deck.mjs")).href)).stopDeck(d),
|
|
198
|
+
out = (line) => console.log(line),
|
|
199
|
+
err = (line) => console.error(line),
|
|
200
|
+
} = {}) {
|
|
201
|
+
const decks = await list().catch(() => []);
|
|
202
|
+
let all = true;
|
|
203
|
+
for (const d of decks) {
|
|
204
|
+
const res = await stop(d).catch((e) => ({ ok: false, reason: e?.message ?? String(e) }));
|
|
205
|
+
if (res?.ok) {
|
|
206
|
+
out(`${PRODUCT}: stopped the deck on port ${d.port} (pid ${d.pid})`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
all = false;
|
|
210
|
+
// `G.dash`, like every other line here (#797).
|
|
211
|
+
err(`${PRODUCT}: could NOT stop the deck on port ${d.port} (pid ${d.pid}) ${G.dash} ${res?.reason ?? "unknown"}; while it runs it can put the hooks back.`);
|
|
212
|
+
}
|
|
213
|
+
return all;
|
|
214
|
+
}
|
package/bin/deck.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
G, P, fileLink, printBanner, showCursor, step, takeCursor, tty, write,
|
|
37
37
|
} from "./cli/screen.js";
|
|
38
38
|
// The once-per-session work and the rows that report it.
|
|
39
|
-
import { reportReady, reportRestarted, reportStartup, startupWork } from "./cli/startup.js";
|
|
39
|
+
import { reportReady, reportRestarted, reportStartup, respawnHooks, startupWork } from "./cli/startup.js";
|
|
40
40
|
import { restartLatch } from "./cli/restart.js";
|
|
41
41
|
import { startPulse } from "./cli/pulse.js";
|
|
42
42
|
import { settleSecondStart } from "./cli/second-start.js";
|
|
@@ -163,6 +163,14 @@ const { deckDataDir, deckLogDir, legacyDeckDir, migrateDeckFiles, sweepTempFiles
|
|
|
163
163
|
pathToFileURL(join(PKG_ROOT, "src/server/browser-watch-store.mjs")).href
|
|
164
164
|
);
|
|
165
165
|
await sweepTempFiles({ dirs: [legacy, data, log, watchStoreDir()], fs: fsp }).catch(() => 0);
|
|
166
|
+
// AND THE COPIES OF THE BROWSER'S HISTORY a Browser Watch read left in its
|
|
167
|
+
// staging folder (#1753): a deck that exits mid-read never deletes its copy,
|
|
168
|
+
// and the sweep above neither recurses nor matches the names. Each is a full
|
|
169
|
+
// browsing history. Only folders a minute old — a sibling deck may be reading.
|
|
170
|
+
const { stagingRoot, sweepStaging } = await import(
|
|
171
|
+
pathToFileURL(join(PKG_ROOT, "src/server/browser-history.mjs")).href
|
|
172
|
+
);
|
|
173
|
+
await sweepStaging(stagingRoot()).catch(() => 0);
|
|
166
174
|
}
|
|
167
175
|
// CANONICALISED here rather than left as typed: the discovery file publishes
|
|
168
176
|
// this path so the hook can tell which decks share one log and elect a single
|
|
@@ -384,6 +392,8 @@ const starting = startServer({
|
|
|
384
392
|
// banner it runs underneath. A respawn is the same session continuing, so it
|
|
385
393
|
// skips the lot and prints one line instead. This is the difference between a
|
|
386
394
|
// restart that feels instant and one that makes you wonder whether it worked.
|
|
395
|
+
// The hooks are the exception, when the respawn runs a different package from
|
|
396
|
+
// the one the session started on — see respawnHooks.
|
|
387
397
|
if (!RESPAWN) {
|
|
388
398
|
const jobs = startupWork({ wantClaude, installHooks, leftoverCodexHooks });
|
|
389
399
|
jobs.cswapQuiet.then(settleCswap);
|
|
@@ -391,6 +401,7 @@ if (!RESPAWN) {
|
|
|
391
401
|
await reportStartup(jobs, { workspace, wantClaude, wantCodex, CODEX_SESSIONS_DIR });
|
|
392
402
|
} else {
|
|
393
403
|
settleCswap();
|
|
404
|
+
await respawnHooks({ wantClaude, installHooks, bootVersion: process.env.AGENTS_DECK_BOOT_VERSION });
|
|
394
405
|
}
|
|
395
406
|
|
|
396
407
|
// Usually settled long ago by the time we get here, which is the point: `step`
|
|
@@ -0,0 +1 @@
|
|
|
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-OLDoLpOW.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};
|
|
@@ -0,0 +1 @@
|
|
|
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-OLDoLpOW.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};
|