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
|
@@ -57,11 +57,12 @@
|
|
|
57
57
|
// handshake and the liveness probe therefore live in deck-probe.mjs, a leaf
|
|
58
58
|
// that imports two node builtins; index.mjs takes them from the same place and
|
|
59
59
|
// re-exports them, so there is still exactly one spelling in the package.
|
|
60
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
60
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
61
61
|
import { join } from "node:path";
|
|
62
62
|
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
63
63
|
import { challengeDeck, isProcessAlive } from "./deck-probe.mjs";
|
|
64
64
|
import { sameCodexTree } from "./log-election.mjs";
|
|
65
|
+
import { CRASH_WINDOW_MS } from "./supervisor.mjs";
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* Where every deck on this machine registers itself.
|
|
@@ -156,12 +157,19 @@ export function serves(record, { want = {}, port = null, ours = "" } = {}) {
|
|
|
156
157
|
* deck returning, so anything already running got there in the gap, was asked
|
|
157
158
|
* for more recently, and keeps its place.
|
|
158
159
|
*
|
|
160
|
+
* `atLogin` is the login item's job (`--at-login`). The newest start wins
|
|
161
|
+
* because it is the one somebody just asked for, and nobody asked for this
|
|
162
|
+
* one: registering the item starts it, beside whatever deck the user launched
|
|
163
|
+
* a second ago (#1778). So it attaches to what is running — the deck that
|
|
164
|
+
* serves if there is one, else the first — and stops nothing.
|
|
165
|
+
*
|
|
159
166
|
* The deck kept on an attach is the first that serves, in the registry's port
|
|
160
167
|
* order, so the answer is the same every time it is asked.
|
|
161
168
|
*/
|
|
162
|
-
export function secondStart({ live = [], want = {}, port = null, ours = "", fresh = false, respawn = false } = {}) {
|
|
169
|
+
export function secondStart({ live = [], want = {}, port = null, ours = "", fresh = false, respawn = false, atLogin = false } = {}) {
|
|
163
170
|
if (!live.length) return { act: "start", stop: [] };
|
|
164
171
|
if (respawn) return { act: "yield", deck: live[0], stop: [] };
|
|
172
|
+
if (atLogin) return { act: "attach", deck: live.find(d => serves(d, { want, port, ours })) ?? live[0], stop: [] };
|
|
165
173
|
const keep = fresh ? null : live.find(d => serves(d, { want, port, ours })) ?? null;
|
|
166
174
|
if (keep) return { act: "attach", deck: keep, stop: live.filter(d => d !== keep) };
|
|
167
175
|
return { act: "replace", stop: [...live] };
|
|
@@ -237,6 +245,68 @@ export async function liveDecks({
|
|
|
237
245
|
return proved.filter(Boolean);
|
|
238
246
|
}
|
|
239
247
|
|
|
248
|
+
/**
|
|
249
|
+
* The decks that are down only for now: the worker gone, and the supervisor
|
|
250
|
+
* that is about to bring it back still there (#1779).
|
|
251
|
+
*
|
|
252
|
+
* A crashed worker leaves its record behind — shutdown() is what unlinks it,
|
|
253
|
+
* and a crash never runs it — with `parent` naming the supervisor, which waits
|
|
254
|
+
* out a backoff and starts the deck again. registeredDecks drops the record,
|
|
255
|
+
* because its pid is dead, so `ccdeck --stop` inside that wait said no deck was
|
|
256
|
+
* running and the timer brought it back a few seconds later. This is the list
|
|
257
|
+
* that lets `--stop` end the supervisor instead, and `--status` say so.
|
|
258
|
+
*
|
|
259
|
+
* NOTHING HERE IS PROVED, and nothing can be: there is no port to challenge.
|
|
260
|
+
* So a record qualifies only while it is fresh — a running deck stamps its
|
|
261
|
+
* record every five seconds (ensureDiscovery), so the mtime is when its worker
|
|
262
|
+
* was last alive, and a supervisor still inside its crash window was alive
|
|
263
|
+
* then. An older record whose parent pid answers is far likelier to name a
|
|
264
|
+
* recycled pid than a supervisor, and a signal sent there ends a stranger.
|
|
265
|
+
* The window is the supervisor's own, CRASH_WINDOW_MS.
|
|
266
|
+
*
|
|
267
|
+
* One entry per supervisor, and none for a supervisor that already has a live
|
|
268
|
+
* worker registered: that deck is on the ordinary list, and stopping it ends
|
|
269
|
+
* the supervisor too. Never this process or its own supervisor. Marked
|
|
270
|
+
* `restarting`, which is how stopDeck knows there is no worker to ask.
|
|
271
|
+
*/
|
|
272
|
+
export async function restartingDecks({
|
|
273
|
+
dir = deckRegistryDir(),
|
|
274
|
+
fs = { readdir, readFile, stat },
|
|
275
|
+
self = process.pid,
|
|
276
|
+
selfParent = process.ppid,
|
|
277
|
+
alive = isProcessAlive,
|
|
278
|
+
now = Date.now(),
|
|
279
|
+
windowMs = CRASH_WINDOW_MS,
|
|
280
|
+
} = {}) {
|
|
281
|
+
let names;
|
|
282
|
+
try { names = await fs.readdir(dir); } catch { return []; }
|
|
283
|
+
const records = [];
|
|
284
|
+
for (const name of names) {
|
|
285
|
+
if (!name.endsWith(".json")) continue;
|
|
286
|
+
const file = join(dir, name);
|
|
287
|
+
let d;
|
|
288
|
+
let mtime;
|
|
289
|
+
try {
|
|
290
|
+
d = JSON.parse(await fs.readFile(file, "utf8"));
|
|
291
|
+
mtime = (await fs.stat(file)).mtimeMs;
|
|
292
|
+
} catch { continue; }
|
|
293
|
+
if (!usable(d) || d.pid === self) continue;
|
|
294
|
+
records.push({ d, mtime, up: alive(d.pid) });
|
|
295
|
+
}
|
|
296
|
+
const serving = new Set(records.filter(r => r.up).map(r => r.d.parent));
|
|
297
|
+
const out = [];
|
|
298
|
+
for (const { d, mtime, up } of records) {
|
|
299
|
+
const parent = d.parent;
|
|
300
|
+
if (up || !Number.isInteger(parent) || parent <= 0) continue;
|
|
301
|
+
if (parent === self || parent === selfParent || serving.has(parent)) continue;
|
|
302
|
+
if (!(Number.isFinite(mtime) && now - mtime >= 0 && now - mtime < windowMs)) continue;
|
|
303
|
+
if (!alive(parent)) continue;
|
|
304
|
+
serving.add(parent);
|
|
305
|
+
out.push({ ...d, restarting: true });
|
|
306
|
+
}
|
|
307
|
+
return out.sort((a, b) => a.port - b.port || a.pid - b.pid);
|
|
308
|
+
}
|
|
309
|
+
|
|
240
310
|
/**
|
|
241
311
|
* What to say about the version of the deck we attached to.
|
|
242
312
|
*
|
|
@@ -129,23 +129,27 @@ export async function readModelFromTranscript(path) {
|
|
|
129
129
|
* total without their model would price a Haiku subagent's spend at the root's
|
|
130
130
|
* Opus rate. It stays constant-cost the way the flat total does: the key count
|
|
131
131
|
* is the number of MODELS a session touched, two or three, not the number of
|
|
132
|
-
* files it delegated to.
|
|
132
|
+
* files it delegated to.
|
|
133
|
+
*
|
|
134
|
+
* EVERY `agent-*.jsonl`, AT EVERY DEPTH (#1749). This took hex ids at the top
|
|
135
|
+
* of the directory only, and CC writes two more kinds: a labelled agent, whose
|
|
136
|
+
* id is `a<label>-<16 hex>` (the memory extractor is `aextract_memories-…`),
|
|
137
|
+
* and every agent a workflow starts, which lands in
|
|
138
|
+
* `subagents/workflows/<run>/agent-<id>.jsonl`. CC's own lister accepts
|
|
139
|
+
* `agent-(.+).jsonl` at any depth below `subagents/`, so this does too, and the
|
|
140
|
+
* id is what the name carries — the same `agent_id` its hooks report. */
|
|
133
141
|
async function readSubagentsFromDir(transcriptPath) {
|
|
134
142
|
// Subagent dir sits next to the main jsonl: <dir>/<sessionId>/subagents/
|
|
135
143
|
// Derive from transcript_path by stripping the .jsonl suffix.
|
|
136
144
|
if (!transcriptPath || typeof transcriptPath !== "string") return null;
|
|
137
145
|
const sessionDir = transcriptPath.replace(/\.jsonl$/i, "");
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
try { entries = await readdir(subDir); } catch { return null; }
|
|
146
|
+
const files = await listSubagentTranscripts(join(sessionDir, "subagents"));
|
|
147
|
+
if (!files) return null;
|
|
141
148
|
const models = {};
|
|
142
149
|
const usage = newUsageTotals();
|
|
143
150
|
const usageByModel = {};
|
|
144
151
|
let spent = false;
|
|
145
|
-
for (const
|
|
146
|
-
if (!/^agent-([0-9a-f]+)\.jsonl$/i.test(f)) continue;
|
|
147
|
-
const agentId = f.replace(/^agent-/, "").replace(/\.jsonl$/i, "");
|
|
148
|
-
const full = join(subDir, f);
|
|
152
|
+
for (const { agentId, full } of files) {
|
|
149
153
|
try {
|
|
150
154
|
// Same incremental cursor as the main transcript. Last-seen claude-*
|
|
151
155
|
// model wins — subagents may switch model mid-turn (Sonnet → Haiku for
|
|
@@ -173,6 +177,34 @@ async function readSubagentsFromDir(transcriptPath) {
|
|
|
173
177
|
};
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
// A subagent's transcript as CC names it, capturing the agent id.
|
|
181
|
+
const SUBAGENT_FILE_RE = /^agent-(.+)\.jsonl$/i;
|
|
182
|
+
|
|
183
|
+
/** Every subagent transcript below `subDir`, at any depth, as `{ agentId, full }`,
|
|
184
|
+
* or null when there is no such directory. Iterative rather than recursive, and
|
|
185
|
+
* a symlinked folder is not followed, so a tree that loops back on itself is
|
|
186
|
+
* listed once. A folder that goes away mid-walk is skipped, like an unreadable
|
|
187
|
+
* file is below. */
|
|
188
|
+
async function listSubagentTranscripts(subDir) {
|
|
189
|
+
let top;
|
|
190
|
+
try { top = await readdir(subDir, { withFileTypes: true }); } catch { return null; }
|
|
191
|
+
const found = [];
|
|
192
|
+
const pending = [[subDir, top]];
|
|
193
|
+
while (pending.length > 0) {
|
|
194
|
+
const [dir, entries] = pending.pop();
|
|
195
|
+
for (const e of entries) {
|
|
196
|
+
const full = join(dir, e.name);
|
|
197
|
+
if (e.isDirectory()) {
|
|
198
|
+
try { pending.push([full, await readdir(full, { withFileTypes: true })]); } catch { /* gone since */ }
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const m = SUBAGENT_FILE_RE.exec(e.name);
|
|
202
|
+
if (m) found.push({ agentId: m[1], full });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
|
|
176
208
|
// One walk of `<sessionDir>/subagents/` serves both the model pass and the
|
|
177
209
|
// usage pass. The two are throttled independently but fire from the same hook
|
|
178
210
|
// events, so without this the directory — up to a few hundred files — would be
|
package/src/server/stop-deck.mjs
CHANGED
|
@@ -148,10 +148,19 @@ export async function stopDeck(rec, {
|
|
|
148
148
|
askMs = STOP_ASK_MS,
|
|
149
149
|
goneMs = STOP_GONE_MS,
|
|
150
150
|
} = {}) {
|
|
151
|
-
const wait = () => gone(rec.pid, { alive, deadlineMs: goneMs, sleep, now });
|
|
152
151
|
const parent = Number.isInteger(rec.parent) ? rec.parent : null;
|
|
152
|
+
// A DECK BETWEEN WORKERS — restartingDecks in running-deck.mjs, #1779. The
|
|
153
|
+
// worker crashed and its supervisor is waiting to start the next one, so the
|
|
154
|
+
// supervisor is the whole of what is left to end. Nothing is asked: nothing
|
|
155
|
+
// is listening on that port that has proved it is ours, and the token is not
|
|
156
|
+
// sent to whatever is. And the dead worker's pid is never signalled, since it
|
|
157
|
+
// may belong to anything by now.
|
|
158
|
+
const restarting = rec.restarting === true && parent !== null;
|
|
159
|
+
const wait = () => gone(restarting ? parent : rec.pid, { alive, deadlineMs: goneMs, sleep, now });
|
|
153
160
|
|
|
154
|
-
const answer =
|
|
161
|
+
const answer = restarting
|
|
162
|
+
? { ok: false, status: 0, old: false, reason: "restarting" }
|
|
163
|
+
: await ask(rec, { timeoutMs: askMs });
|
|
155
164
|
if (answer.ok && await wait()) return { ok: true, how: "asked" };
|
|
156
165
|
|
|
157
166
|
if (platform !== "win32") {
|
|
@@ -159,12 +168,12 @@ export async function stopDeck(rec, {
|
|
|
159
168
|
// killed worker is a supervisor doing its job, which here means undoing
|
|
160
169
|
// ours.
|
|
161
170
|
if (parent !== null) kill(parent, "SIGTERM", { platform });
|
|
162
|
-
kill(rec.pid, "SIGTERM", { platform });
|
|
171
|
+
if (!restarting) kill(rec.pid, "SIGTERM", { platform });
|
|
163
172
|
if (await wait()) return { ok: true, how: "signalled", old: answer.old === true };
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
if (parent !== null) kill(parent, "SIGKILL", { platform });
|
|
167
|
-
kill(rec.pid, "SIGKILL", { platform });
|
|
176
|
+
if (!restarting) kill(rec.pid, "SIGKILL", { platform });
|
|
168
177
|
if (await wait()) return { ok: true, how: "killed", old: answer.old === true };
|
|
169
178
|
|
|
170
179
|
return { ok: false, how: "stuck", reason: answer.reason ?? `http ${answer.status}` };
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
networkSnapshot, notePanelPoll, probeNetwork, routeChangesSince, sampleNetwork,
|
|
26
26
|
startNetworkTimers, stopNetwork,
|
|
27
27
|
} from "./network-sampler.mjs";
|
|
28
|
-
import { readProcesses, resetProcessList } from "./process-list.mjs";
|
|
28
|
+
import { processesReply, readProcesses, resetProcessList } from "./process-list.mjs";
|
|
29
29
|
import { CRIT_C, WARN_C } from "./thermal-metrics.mjs";
|
|
30
30
|
import {
|
|
31
31
|
lastThermal, sampleThermal, startThermalTimer, stopThermal, thermalSnapshot, THROTTLE_LABEL,
|
|
@@ -135,7 +135,7 @@ function cpuPercent() {
|
|
|
135
135
|
* The process list, which lives in process-list.mjs: it is read on demand
|
|
136
136
|
* rather than on a timer here, and index.mjs asks this module for it.
|
|
137
137
|
*/
|
|
138
|
-
export { readProcesses };
|
|
138
|
+
export { processesReply, readProcesses };
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* The network probe, which lives in network-sampler.mjs with the rest of that
|
|
@@ -447,12 +447,28 @@ function foldContextLine(state, line, record) {
|
|
|
447
447
|
* `join`, so `C:/x/y.jsonl` and `C:\x\y\subagents\agent-1.jsonl` would
|
|
448
448
|
* otherwise be two sessions instead of one; `resolve` settles the separator
|
|
449
449
|
* on all three platforms, and leaves a backslash inside a POSIX filename
|
|
450
|
-
* alone rather than reading it as a directory break.
|
|
450
|
+
* alone rather than reading it as a directory break.
|
|
451
|
+
*
|
|
452
|
+
* ANY AGENT ID, AT ANY DEPTH BELOW `subagents/` (#1749). The id is not all hex:
|
|
453
|
+
* CC names a labelled agent `a<label>-<16 hex>` (`aextract_memories-…`), and it
|
|
454
|
+
* writes a workflow's agents one folder further down, in
|
|
455
|
+
* `subagents/workflows/<run>/`. Both used to fall outside the pattern and so
|
|
456
|
+
* became sessions of their own, each evicting on its own. The session is the
|
|
457
|
+
* directory above the nearest `subagents` folder, found in two linear passes
|
|
458
|
+
* rather than one backtracking pattern, because the path can be a caller's. */
|
|
451
459
|
export function transcriptSessionKey(path) {
|
|
452
460
|
const full = resolve(path);
|
|
453
|
-
|
|
454
|
-
|
|
461
|
+
if (SUBAGENT_TRANSCRIPT_RE.test(full)) {
|
|
462
|
+
let at = -1;
|
|
463
|
+
for (const m of full.matchAll(SUBAGENTS_DIR_RE)) at = m.index;
|
|
464
|
+
if (at >= 0) return full.slice(0, at);
|
|
465
|
+
}
|
|
466
|
+
return full.replace(/\.jsonl$/i, "");
|
|
455
467
|
}
|
|
468
|
+
// A subagent's transcript as CC names it — `agent-<id>.jsonl`, whatever the id —
|
|
469
|
+
// and the folder it sits somewhere below. See transcriptSessionKey.
|
|
470
|
+
const SUBAGENT_TRANSCRIPT_RE = /[\\/]agent-[^\\/]+\.jsonl$/i;
|
|
471
|
+
const SUBAGENTS_DIR_RE = /[\\/]subagents(?=[\\/])/gi;
|
|
456
472
|
|
|
457
473
|
/** Record a use of `path` and keep the cache under both caps. Re-inserting the
|
|
458
474
|
* session on every touch makes the Map's own insertion order the LRU order,
|
|
@@ -516,17 +532,22 @@ function scanTranscript(path) {
|
|
|
516
532
|
const inFlight = transcriptScanInFlight.get(path);
|
|
517
533
|
if (inFlight) return inFlight;
|
|
518
534
|
const run = (async () => {
|
|
535
|
+
// The file is looked at BEFORE it is given a slot. The cache is capped in
|
|
536
|
+
// sessions and a new slot can evict another session's cursors, which costs
|
|
537
|
+
// that session a re-read of its whole transcript from byte 0 — the cost the
|
|
538
|
+
// cursor exists to remove. A path that cannot be stat-ed, or is not a file,
|
|
539
|
+
// has nothing to fold, so it answers with what it already had and takes no
|
|
540
|
+
// slot.
|
|
541
|
+
const s = await stat(path).catch(() => null);
|
|
542
|
+
if (!s || !s.isFile()) return transcriptScans.get(path) ?? newTranscriptState();
|
|
519
543
|
let state = transcriptScans.get(path);
|
|
520
544
|
if (!state) state = newTranscriptState();
|
|
521
545
|
touchTranscriptScan(path, state);
|
|
522
546
|
try {
|
|
523
|
-
const s = await stat(path);
|
|
524
547
|
// Shorter than the cursor means the file was truncated, rotated or
|
|
525
548
|
// replaced — the offset now points at unrelated bytes, so start over.
|
|
526
549
|
if (s.size < state.offset) {
|
|
527
550
|
state = newTranscriptState();
|
|
528
|
-
// Touch again rather than set: the await above yielded, and another
|
|
529
|
-
// path's scan may have re-ordered or evicted this entry meanwhile.
|
|
530
551
|
touchTranscriptScan(path, state);
|
|
531
552
|
}
|
|
532
553
|
if (s.size <= state.offset) return state;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{j as e,r as h,i as Q,k as _,s as V,u as Y,e as z}from"./index-P2zWn3Hw.js";function X({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}`))})]})}function Z(s){const t=o=>new Date(o).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 ee(s){return new Date(s).toLocaleDateString(void 0,{weekday:"short",day:"numeric",month:"short"})}function K(s){const t=Math.round((s.endMs-s.startMs)/6e4);return t<1?"":`${t} min`}function se({snap:s,dismiss:t}){const[o,r]=h.useState(null),d=h.useMemo(()=>{const i=[];for(const n of s.episodes){const a=ee(n.startMs),l=i[i.length-1];l&&l.label===a?l.episodes.push(n):i.push({label:a,episodes:[n]})}return i},[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:d.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."})]}):d.map(i=>e.jsxs("div",{className:"bw-day",children:[e.jsx("h4",{className:"bw-sec-head",children:i.label}),i.episodes.map(n=>{const a=`${n.host}-${n.startMs}`,l=o===a;return e.jsxs("div",{className:`bw-ep${l?" open":""}`,children:[e.jsxs("div",{className:"bw-ep-row",children:[e.jsxs("button",{className:"bw-ep-head",onClick:()=>r(l?null:a),"aria-expanded":l,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:l?"▾":"▸"}),e.jsx("span",{className:"bw-ep-host",children:n.host}),e.jsxs("span",{className:"bw-ep-meta",children:[Z(n),K(n)&&e.jsxs(e.Fragment,{children:[" · ",K(n)]})]}),e.jsxs("span",{className:"bw-ep-count",children:[n.count," ",n.count===1?"page":"pages"]})]}),e.jsx("button",{className:"glyph-btn bw-ep-x",onClick:()=>void t(n),"aria-label":`Dismiss ${n.host}`,title:"Dismiss — it leaves this list for good, and stays in the log file",children:"×"})]}),l&&e.jsx("ul",{className:"bw-urls",children:n.urls.map((u,c)=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-url-time",children:new Date(u.timeMs).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1})}),e.jsx("span",{className:"bw-url",children:u.url})]},`${u.url}-${u.timeMs}-${c}`))})]},a)})]},i.label))})]})}function te(s){const t=/^#?([0-9a-f]{6})$/i.exec((s??"").trim());if(!t)return!1;const o=parseInt(t[1],16),r=[o>>16&255,o>>8&255,o&255].map(d=>{const i=d/255;return i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4)});return .2126*r[0]+.7152*r[1]+.0722*r[2]>.5}const U=1e4,ne=12e3;function ae({browsers:s,findings:t,watching:o,palette:r}){const d=h.useRef(null),i=h.useRef(null);return i.current={browsers:s,findings:t,watching:o},h.useEffect(()=>{var R;const n=d.current;if(!n)return;const a=n.getContext("2d");if(!a)return;const l=((R=window.matchMedia)==null?void 0:R.call(window,"(prefers-reduced-motion: reduce)").matches)??!1;let u=0,c=!1;const N=()=>{var F;if(c)return;const P=i.current;if(!P)return;const{browsers:k,findings:m,watching:$}=P,C=r["--accent"]||"#7dd3fc",b=r["--line"]||"#1f2229",g=r["--text-dim"]||"#6e727c",M=te(r["--bg"]),f=M?42:26,y=M?.85:.65,W=Date.now(),S=Math.min(window.devicePixelRatio||1,2),p=n.clientWidth,A=n.clientHeight;(n.width!==p*S||n.height!==A*S)&&(n.width=p*S,n.height=A*S),a.setTransform(S,0,0,S,0,0),a.clearRect(0,0,p,A);const x=p/2,j=A/2,v=Math.min(p,A)/2-6;a.strokeStyle=b,a.lineWidth=1,a.globalAlpha=M?1:.9;for(const w of[.4,.7,1])a.beginPath(),a.arc(x,j,v*w,0,Math.PI*2),a.stroke();a.beginPath(),a.moveTo(x-v,j),a.lineTo(x+v,j),a.moveTo(x,j-v),a.lineTo(x,j+v),a.stroke(),a.globalAlpha=1;const T=($&&!l?W%U/U:0)*Math.PI*2-Math.PI/2;if($){const w=(F=a.createConicGradient)==null?void 0:F.call(a,T-.85,x,j);w&&(w.addColorStop(0,"transparent"),w.addColorStop(.85/(Math.PI*2),`color-mix(in srgb, ${C} ${f}%, transparent)`),w.addColorStop(1,"transparent"),a.fillStyle=w,a.beginPath(),a.moveTo(x,j),a.arc(x,j,v,T-.85,T),a.closePath(),a.fill()),a.strokeStyle=C,a.globalAlpha=y,a.beginPath(),a.moveTo(x,j),a.lineTo(x+Math.cos(T)*v,j+Math.sin(T)*v),a.stroke(),a.globalAlpha=1}k.forEach((w,I)=>{const B=I/Math.max(k.length,1)*Math.PI*2-Math.PI/2,E=w.lastReadMs===null?1:Math.min(1,(W-w.lastReadMs)/12e4),D=v*(.42+E*.5),O=x+Math.cos(B)*D,L=j+Math.sin(B)*D,J=Math.abs((T-B+Math.PI*3)%(Math.PI*2)-Math.PI),q=$?Math.max(0,1-J/.55):0;a.beginPath(),a.arc(O,L,3+q*1.6,0,Math.PI*2),a.fillStyle=w.running?C:g,a.globalAlpha=w.running?.55+q*.45:.35,a.fill(),a.globalAlpha=1});for(const w of m){const I=(W-w.atMs)/ne;if(I<0||I>1)continue;const E=Math.max(0,k.findIndex(L=>L.key===w.browser))/Math.max(k.length,1)*Math.PI*2-Math.PI/2,D=x+Math.cos(E)*v*.6,O=j+Math.sin(E)*v*.6;a.beginPath(),a.arc(D,O,4+I*22,0,Math.PI*2),a.strokeStyle=C,a.globalAlpha=(1-I)*.8,a.lineWidth=1.5,a.stroke(),a.globalAlpha=1}u=requestAnimationFrame(N)};return N(),()=>{c=!0,cancelAnimationFrame(u)}},[r]),e.jsx("canvas",{ref:d,className:"bw-radar",role:"img","aria-label":o?`Watching ${s.length} browser${s.length===1?"":"s"}`:"Not watching"})}function re(s,t,o){if(s===null)return null;const r=s+t-o;return r>0?r:null}function ie(s){const t=Math.ceil(s/1e3),o=Math.floor(t/60),r=t%60;return o>0?`${o}m ${r}s`:`${r}s`}function H(s){return(s??[]).filter(t=>t.installed&&t.profiles>0)}function oe(s){return!Number.isFinite(s)||s<=0?"empty":Q(s)}function le(s,t){return s.map(o=>({key:o.key,name:o.name,running:o.running,visits:t.filter(r=>r.browser===o.key).reduce((r,d)=>r+d.programVisits,0)}))}function ce(s,t){if(s===null)return null;const o=Math.max(0,Math.round((t-s)/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 de(s){const t=s.profiles.filter(r=>r.degraded);if(t.length>0){const r=t[0],d=t.length-1;return{kind:"unreadable",text:`${r.name}/${r.profile} could not be read — ${r.reason??"no reason given"}`+(d>0?`, and ${d} more`:"")+". The watch continues on the profiles it can read."}}const o=H(s.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 he({snap:s,watching:t,palette:o}){const[r,d]=h.useState(!1),[i,n]=h.useState(()=>Date.now());h.useEffect(()=>{const l=setInterval(()=>n(Date.now()),1e3);return()=>clearInterval(l)},[]);const a=re(s.coverage.lastHumanMs,s.coverage.quietMs,i);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:()=>d(l=>!l),"aria-expanded":r,"aria-label":"What the sweep and the dots mean",children:"?"})]}),e.jsxs("div",{className:"bw-glance",children:[e.jsx(ae,{browsers:t.map(l=>{var u;return{key:l.key,name:l.name,running:l.running,lastReadMs:((u=s.profiles.find(c=>c.browser===l.key))==null?void 0:u.lastWrittenMs)??null}}),findings:s.episodes.slice(0,6).map(l=>({browser:l.browser??null,atMs:l.endMs})),watching:s.settings.enabled,palette:o}),e.jsx("ul",{className:"bw-counts",children:le(t,s.profiles).map(l=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-count-n",children:l.visits.toLocaleString("en-US")}),e.jsx("span",{className:"bw-count-of",children:l.name})]},l.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 ",ce(s.coverage.checkedMs,i),s.coverage.checks>0&&e.jsxs(e.Fragment,{children:[" · ",s.coverage.checks.toLocaleString("en-US")," ",s.coverage.checks===1?"check":"checks"]})]}),a!==null&&e.jsxs("p",{className:"bw-gate",children:["You are browsing — a program page would count in ",ie(a)]}),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 ue({watching:s,rest:t}){const[o,r]=h.useState(!1),[d,i]=h.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(n=>!n),"aria-expanded":o,"aria-label":"What running, idle and connected to Anthropic mean",children:"?"})]}),o&&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(n=>e.jsxs("li",{children:[e.jsx("span",{className:`bw-prof-dot${n.running?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-prof-name",children:n.name}),e.jsx("span",{className:"bw-prof-state",children:n.running?"running":"idle"}),n.running&&n.relay.state==="live"&&e.jsx("span",{className:"bw-relay",children:"connected to Anthropic"})]},n.key))}),t.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"bw-why bw-rest-head",onClick:()=>i(n=>!n),"aria-expanded":d,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:d?"▾":"▸"})," ",t.length," not watched"]}),d&&e.jsxs("p",{className:"bw-rest",children:[t.map(n=>n.name).join(", ")," — not installed, or installed and never opened, so there is no history to read."]})]})]})}function we({snap:s,quiet:t,setQuiet:o,save:r,access:d,setAccess:i}){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:n=>{o(Number(n.target.value)),r({quietMinutes:Number(n.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:n=>void r({reaction:n.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(n=>e.jsx("option",{value:n,children:n==="notify"?"notify me":n==="close-tab"?"close the tab":"quit the browser"},n))})]}),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:()=>i(n=>!n),"aria-expanded":d,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:d?"▾":"▸"})," What Browser Watch can access"]}),d&&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"?` (${oe(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 G(s,t){return t?{kind:"saving",word:"Saving"}:s.settings.enabled?{kind:"on",word:"Watching"}:{kind:"off",word:"Paused"}}function me({snap:s,saving:t,save:o,why:r,setWhy:d}){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:G(s,t).word},G(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:()=>d(i=>!i),"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 o({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 be({relay:s}){const[t,o]=h.useState(!1),[r,d]=h.useState(!1),i=s.killswitch.blocked?"unblock":"block",n=s.command[i],a=h.useCallback(async()=>{await _(n.command)&&(d(!0),window.setTimeout(()=>d(!1),1600))},[n.command]),l=s.profiles.filter(c=>{var N;return(N=c.report)==null?void 0:N.present}),u=s.profiles.filter(c=>c.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(c=>e.jsx("code",{className:"bw-path",children:c},c))]}),e.jsxs("button",{className:"bw-why",onClick:()=>o(c=>!c),"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:[l.length>0&&e.jsx("dl",{className:"bw-key",children:l.map(c=>e.jsxs(h.Fragment,{children:[e.jsxs("dt",{children:[c.name,c.profile==="Default"?"":` · ${c.profile}`]}),e.jsxs("dd",{children:[c.report.enabled?"enabled":"installed but switched off",c.report.allUrls?", can act on every site":", limited to the sites it was granted",c.report.sensitiveApis.length>0&&e.jsxs(e.Fragment,{children:[" — ",c.report.sensitiveApis.join(", ")]})]})]},`${c.browser}/${c.profile}`))}),u.length>0&&e.jsxs("p",{className:"bw-rc-note",children:[u.map(c=>c.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."]}),l.some(c=>c.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:n.command}),e.jsx("button",{className:"btn bw-rc-copy",onClick:a,children:r?"Copied":"Copy"})]}),e.jsx("p",{className:"bw-rc-note",children:n.note})]})]})}function ge(s){const[t,o]=h.useState(null),[r,d]=h.useState(null),[i,n]=h.useState(null),[a,l]=h.useState(!1),u=h.useRef(!1),[c,N]=h.useState(null),[R,P]=h.useState(!1),k=h.useRef(!1),m=h.useCallback(async b=>{var g,M;if(!(b&&!V(u.current))){u.current=b,l(!0);try{const f=await fetch(`/api/browser-watch${b?"?refresh=1":""}`);if(!f.ok)throw new Error(`the deck answered ${f.status}`);const y=await f.json();o(y),k.current||N(((g=y==null?void 0:y.settings)==null?void 0:g.quietMinutes)??15),s(((M=y==null?void 0:y.settings)==null?void 0:M.enabled)===!0),d(null)}catch(f){d(f instanceof Error?f.message:String(f))}finally{u.current=!1,l(!1)}}},[s]);h.useEffect(()=>{m(!1)},[m]),h.useEffect(()=>{const b=setInterval(()=>{m(!1)},1e4);return()=>clearInterval(b)},[m]);const $=h.useCallback(async b=>{try{await fetch("/api/browser-watch/dismiss",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({host:b.host,startMs:b.startMs})})}finally{await m(!1)}},[m]),C=h.useCallback(async b=>{k.current=!0,P(!0);try{const g=await fetch("/api/browser-watch",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(b)});if(!g.ok)throw new Error(`the deck answered ${g.status}`);await m(!1),n(null)}catch(g){n(g instanceof Error?g.message:String(g))}finally{k.current=!1,P(!1)}},[m]);return{snap:t,error:r,writeError:i,setWriteError:n,busy:a,quiet:c,setQuiet:N,saving:R,load:m,dismiss:$,save:C}}function xe({onClose:s,onSeen:t,onWatching:o,palette:r}){const d=Y(s),{snap:i,error:n,writeError:a,setWriteError:l,busy:u,quiet:c,setQuiet:N,saving:R,load:P,dismiss:k,save:m}=ge(o),[$,C]=h.useState(!1),[b,g]=h.useState(!1),M=h.useRef(t);M.current=t,h.useEffect(()=>()=>M.current(Date.now()),[]);const f=H(i==null?void 0:i.browsers),y=((i==null?void 0:i.browsers)??[]).filter(p=>!(p.installed&&p.profiles>0)),W=i?de(i):null,S=f.reduce((p,A)=>p+A.profiles,0);return e.jsx("div",{className:"modal-backdrop",onClick:s,role:"presentation",children:e.jsxs("div",{ref:d,className:"modal bw-modal",onClick:p=>p.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:i?`${S} ${S===1?"profile":"profiles"} · local only`:"local only"})]}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{className:"glyph-btn",onClick:()=>void P(!0),...z(u),"aria-label":"Re-read every profile now",title:"Re-read every profile now",children:u?"…":"↻"}),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:[!i&&!n&&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."})]}),n&&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:n})]})}),i&&W&&e.jsx("p",{className:`bw-trouble ${W.kind}`,role:"status",children:W.text}),i&&e.jsxs("div",{className:"bw-work",children:[e.jsxs("aside",{className:"bw-side",children:[e.jsx(he,{snap:i,watching:f,palette:r}),e.jsx(ue,{watching:f,rest:y}),i.relay&&e.jsx(be,{relay:i.relay})]}),e.jsxs("div",{className:"bw-main",children:[e.jsx(se,{snap:i,dismiss:k}),e.jsx(X,{snap:i})]})]}),i&&$&&e.jsx(we,{snap:i,quiet:c,setQuiet:N,save:m,access:b,setAccess:g}),a&&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:[a," — the switches below show what is stored, not what you pressed."]}),e.jsx("button",{className:"glyph-btn bw-write-err-x",onClick:()=>l(null),"aria-label":"Dismiss",title:"Dismiss",children:"×"})]})}),i&&e.jsx(me,{snap:i,saving:R,save:m,why:$,setWhy:C})]})]})})}export{xe as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as F,f as p,r as c,s as X,p as Y,j as a,b as g,c as O,u as Z,d as _,e as E,g as D,h as ee}from"./index-P2zWn3Hw.js";function se(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 ae(s){const e=I([{agents:s}]);return e.length<2?null:e.map(n=>`${F(n.id)} ${p(n.cost)}`).join(" · ")}function P(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 te(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:"",U=s=>Array.isArray(s)?s.filter(e=>typeof e=="string"):[];function ne(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 oe(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:U(e.modelsUsed),modelBreakdowns:Array.isArray(e.modelBreakdowns)?e.modelBreakdowns.map(ne):[],...n?{agents:n}:{},...e.metadata&&typeof e.metadata=="object"?{metadata:{agents:U(e.metadata.agents)}}:{}}}function le(s){const e=s??{};return{...e,ok:e.ok===!0,days:Array.isArray(e.days)?e.days.map(oe):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 re(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 ce(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 ie(s){var n,o;return ae(s.agents)??((o=(n=s.metadata)==null?void 0:n.agents)!=null&&o.length?s.metadata.agents.join(" · "):null)}function ue(){let s=0;return{begin(){const e=++s;return()=>e===s},cancel(){s++}}}function de(s){const[e,n]=c.useState(null),[o,r]=c.useState(!1),m=c.useState(ue)[0],l=c.useRef(!1),d=(f=!1)=>{if(f&&!X(l.current))return;const x=m.begin();l.current=f;const j=s;r(!0);const i=`/api/ccusage?since=${Y(j)}${f?"&refresh=1":""}`;fetch(i).then(k=>k.json()).then(k=>{x()&&n({range:j,resp:le(k)})}).catch(()=>{x()&&n({range:j,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 he({selectedDay:s}){const e=ie(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 me=[7,14,30,90];function ge({onClose:s,providers:e}){var B;const[n,o]=c.useState(30),[r,m]=c.useState(null),{landed:l,loading:d,reload:f}=de(n),x=c.useRef(null),j=Z(s,{focusRef:x}),h=se({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??[]:[],k=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:G,inOut:H,cacheRead:V,modelCosts:A}=c.useMemo(()=>re(i),[i]),W=c.useMemo(()=>ce(A),[A]),b=c.useMemo(()=>I(i),[i]),z=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:j,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…":_(e,b.map(t=>t.id))})]}),a.jsx("div",{className:"uh-range",role:"group","aria-label":"Range",children:me.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",{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:ee(l.resp)||void 0,children:[D(l.resp,"ccusage did not report usage"),a.jsx("div",{children:a.jsx("button",{className:"btn uh-retry",onClick: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(G)}),a.jsx(v,{label:"input+output",val:g(H)}),a.jsx(v,{label:"cache reads",val:g(V)})]}),z&&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:P(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:P(t.id)}}),F(t.id),a.jsx("span",{className:"uh-agent-cost",children:p(t.cost)}),a.jsx("span",{className:"uh-agent-share",children:te(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 J=C(t.totalCost,k),R=t.period===r,K=`${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":K,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(J,t.totalCost>0?2:0)}%`},children:q(t.modelBreakdowns).map($=>{const Q=C($.cost,t.totalCost);return a.jsx("div",{className:"uh-bar-seg",style:{height:`${Q}%`,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:W.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(he,{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{ge as default};
|