ccdeck 3.42.0 → 3.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -17
- package/dist/web/assets/BrowserWatchModal-1On-pA6T.js +1 -0
- package/dist/web/assets/RadioPanel-DkXAgxKR.js +1 -0
- package/dist/web/assets/TrafficRadar-DWilzdA3.js +1 -0
- package/dist/web/assets/UsageHistoryModal-BW9jTi6E.js +1 -0
- package/dist/web/assets/app-ByCusSHX.js +310 -0
- package/dist/web/assets/app-DsHSufBt.css +1 -0
- package/dist/web/assets/index-CzIBjYsB.js +1 -0
- package/dist/web/index.html +5 -2
- package/package.json +1 -1
- package/release-notes.json +24 -1
- package/src/server/account-choice.mjs +198 -0
- package/src/server/account-notify.mjs +15 -8
- package/src/server/account-routes.mjs +6 -0
- package/src/server/activity.mjs +11 -3
- package/src/server/claude-recovery.mjs +150 -0
- package/src/server/codex-enrichment.mjs +11 -3
- package/src/server/codex-lineage.mjs +29 -0
- package/src/server/codex-recovery.mjs +83 -0
- package/src/server/codex-translate.mjs +24 -0
- package/src/server/codex-watch.mjs +113 -9
- package/src/server/cswap-auto-choice.mjs +303 -0
- package/src/server/cswap-auto-loop.mjs +33 -2
- package/src/server/cswap-auto-readers.mjs +5 -2
- package/src/server/cswap-auto.mjs +7 -1
- package/src/server/deck-prefs.mjs +5 -1
- package/src/server/event-pipeline.mjs +4 -3
- package/src/server/evicted-enrichment.mjs +2 -0
- package/src/server/index.mjs +9 -0
- package/src/server/pinned-build.mjs +4 -0
- package/src/server/traffic-radar-config.mjs +8 -1
- package/dist/web/assets/BrowserWatchModal-TdSBv7_T.js +0 -1
- package/dist/web/assets/RadioPanel-bJc004IK.js +0 -1
- package/dist/web/assets/TrafficRadar-pRTWTOpJ.js +0 -1
- package/dist/web/assets/UsageHistoryModal-BBAEDRj9.js +0 -1
- package/dist/web/assets/index-BPLXy9SO.css +0 -1
- package/dist/web/assets/index-DzDnLqDX.js +0 -307
|
@@ -20,6 +20,9 @@ import { withStoreLock } from "./store-lock.mjs";
|
|
|
20
20
|
// moved the account is said once on the desktop (account-notify.mjs). Both
|
|
21
21
|
// doors return at once and never throw.
|
|
22
22
|
import { noteAutoSwitch, noteAutoTick } from "./account-watch.mjs";
|
|
23
|
+
// The owner's two rules — a home account, and the soonest weekly reset — asked
|
|
24
|
+
// before the engine, inside the same lock. With neither set, nothing.
|
|
25
|
+
import { chooseBeforeEngine, forgetPicksByHand, noteEngineTick } from "./cswap-auto-choice.mjs";
|
|
23
26
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
24
27
|
import { join } from "node:path";
|
|
25
28
|
import { homedir } from "node:os";
|
|
@@ -51,6 +54,9 @@ function summarise(stdout) {
|
|
|
51
54
|
// `switch` event for a decision it did not carry out, and a false positive
|
|
52
55
|
// here throws away readings that cost a subprocess each.
|
|
53
56
|
switched: events.some(e => e.event === "switch" && e.dryRun !== true),
|
|
57
|
+
// Why the engine moved — "proactive", "at-limit", "failover" — beside the
|
|
58
|
+
// deck's own "home" and "weekly-reset" (cswap-auto-choice.mjs).
|
|
59
|
+
trigger: action?.trigger ?? null,
|
|
54
60
|
reason: action?.reason ?? null,
|
|
55
61
|
detail: action?.detail ?? null,
|
|
56
62
|
from: action?.from ?? null,
|
|
@@ -94,9 +100,30 @@ async function runAutoTick() {
|
|
|
94
100
|
const r = await withStoreLock(async () => {
|
|
95
101
|
if (!_enabled) return { skipped: "disabled" };
|
|
96
102
|
if (await externalAutoRunning()) return { skipped: "external-engine" };
|
|
97
|
-
|
|
103
|
+
// The deck's own rules first. A move they make is the tick; when they have
|
|
104
|
+
// nothing to say — or cannot say it — the engine decides, as it always did.
|
|
105
|
+
let chose = await chooseBeforeEngine().catch(() => ({ result: null, accounts: null }));
|
|
106
|
+
if (chose.result) return { chose: chose.result };
|
|
107
|
+
// The store may still have a reading from before the active account
|
|
108
|
+
// reached its limit. The engine polls before deciding; letting
|
|
109
|
+
// its real tick run now would switch by `best` before our weekly/home
|
|
110
|
+
// rules ever saw that reading. Poll without moving first, then give the
|
|
111
|
+
// configured rules the refreshed roster under this same store lock.
|
|
112
|
+
// No rule configured (accounts === null) keeps the original single run.
|
|
113
|
+
if (chose.accounts) {
|
|
114
|
+
const preview = await run(await cswapBin(), ["auto", "--once", "--json", "--dry-run"], { timeout: TICK_TIMEOUT_MS });
|
|
115
|
+
if (preview.timedOut || (!preview.ok && !preview.stdout)) return preview;
|
|
116
|
+
if (!_enabled) return { skipped: "disabled" };
|
|
117
|
+
if (await externalAutoRunning()) return { skipped: "external-engine" };
|
|
118
|
+
invalidateClaudeAccountsCache();
|
|
119
|
+
chose = await chooseBeforeEngine().catch(() => ({ result: null, accounts: null }));
|
|
120
|
+
if (chose.result) return { chose: chose.result };
|
|
121
|
+
}
|
|
122
|
+
const out = await run(await cswapBin(), ["auto", "--once", "--json"], { timeout: TICK_TIMEOUT_MS });
|
|
123
|
+
return { ...out, accounts: chose.accounts };
|
|
98
124
|
});
|
|
99
125
|
if (r.skipped) return { event: "skipped", reason: r.skipped };
|
|
126
|
+
if (r.chose) return r.chose;
|
|
100
127
|
// A KILLED RUN IS NOT A QUIET ONE. `run`'s timeout path deliberately keeps an
|
|
101
128
|
// 8 KB tail of whatever the child managed to print, so `!r.ok && !r.stdout`
|
|
102
129
|
// is false for a tick that emitted its `{"event":"poll"}` line and then
|
|
@@ -109,7 +136,10 @@ async function runAutoTick() {
|
|
|
109
136
|
if (!r.ok && !r.stdout) {
|
|
110
137
|
return { ok: false, reason: "tick_failed", detail: failureDetail(r, 300) };
|
|
111
138
|
}
|
|
112
|
-
|
|
139
|
+
const result = { ok: true, ...summarise(r.stdout) };
|
|
140
|
+
// Where the engine left the user is a tick's pick, not one made by hand.
|
|
141
|
+
if (result.switched) await noteEngineTick(result, r.accounts).catch(() => {});
|
|
142
|
+
return result;
|
|
113
143
|
}
|
|
114
144
|
|
|
115
145
|
// ── deck-managed loop ──────────────────────────────────────────────────────
|
|
@@ -340,6 +370,7 @@ export function lastTick() {
|
|
|
340
370
|
export async function setAutoEnabled(enabled) {
|
|
341
371
|
_enabled = Boolean(enabled);
|
|
342
372
|
noteAutoSwitch(_enabled);
|
|
373
|
+
if (_enabled) await forgetPicksByHand();
|
|
343
374
|
await saveState({ ...(await loadState()), enabled: _enabled });
|
|
344
375
|
if (_enabled) await startLoop(); else stopLoop();
|
|
345
376
|
return { ok: true, enabled: _enabled };
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// it re-exports all five names, which is where the suite reaches them.
|
|
10
10
|
import { run } from "./exec.mjs";
|
|
11
11
|
import { cswapBin, cswapRefused } from "./cswap-install.mjs";
|
|
12
|
+
import { stripAnsi } from "./term.mjs";
|
|
12
13
|
|
|
13
14
|
// ── one reading at a time ──────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -157,7 +158,9 @@ export function invalidateCswapAutoCache() {
|
|
|
157
158
|
* whatever shape it is given, and the interval collapses four fields to one
|
|
158
159
|
* number that is clamped anyway. The parse itself is a regex over
|
|
159
160
|
* human-formatted output from a separate Python tool, on both line-ending
|
|
160
|
-
* conventions
|
|
161
|
+
* conventions, and with or without colour: the tool dims its `(default)` marker
|
|
162
|
+
* whenever the environment asks for colour, and a deck launched from a terminal
|
|
163
|
+
* inherits that terminal's FORCE_COLOR. See cswap-auto-readers.test.ts.
|
|
161
164
|
*
|
|
162
165
|
* One reading at a time and one every CONFIG_MIN_GAP_MS at most; the parse below
|
|
163
166
|
* is what a reading is, and admission control is the wrapper. See throttled.
|
|
@@ -174,7 +177,7 @@ async function readCswapConfigNow() {
|
|
|
174
177
|
const r = await run(bin, ["config"]);
|
|
175
178
|
if (!r.ok) return null;
|
|
176
179
|
const out = {};
|
|
177
|
-
for (const line of r.stdout.split("\n")) {
|
|
180
|
+
for (const line of stripAnsi(r.stdout).split("\n")) {
|
|
178
181
|
const m = line.match(/^(\S+)\s+(.*?)\s*(\(default\))?\s*$/);
|
|
179
182
|
if (!m || !m[1].includes(".")) continue;
|
|
180
183
|
const raw = m[2].trim();
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
commandTokens, externalAutoRunning, invalidateCswapAutoCache, looksLikeAutoLoop, readCswapConfig,
|
|
21
21
|
} from "./cswap-auto-readers.mjs";
|
|
22
22
|
import { initCswapAuto, lastTick, loopEnabled, restartLoop, setAutoEnabled } from "./cswap-auto-loop.mjs";
|
|
23
|
+
import { choiceStatus, setHomeAccount, setNextChoice } from "./cswap-auto-choice.mjs";
|
|
23
24
|
import { invalidateClaudeAccountsCache, slotNumber } from "./claude-accounts.mjs";
|
|
24
25
|
// The one store mutex, which the rotation flag below holds like every other
|
|
25
26
|
// writer of claude-swap's store.
|
|
@@ -135,14 +136,19 @@ export { commandTokens, externalAutoRunning, looksLikeAutoLoop };
|
|
|
135
136
|
// cswap-auto-loop.mjs.
|
|
136
137
|
export { initCswapAuto, setAutoEnabled };
|
|
137
138
|
|
|
139
|
+
// Which account a tick moves to — the home it comes back to and the strategy
|
|
140
|
+
// for the next one — kept by the deck, not by claude-swap: cswap-auto-choice.mjs.
|
|
141
|
+
export { setHomeAccount, setNextChoice };
|
|
142
|
+
|
|
138
143
|
export async function autoStatus() {
|
|
139
|
-
const [config, external] = await Promise.all([readCswapConfig(), externalAutoRunning()]);
|
|
144
|
+
const [config, external, choice] = await Promise.all([readCswapConfig(), externalAutoRunning(), choiceStatus()]);
|
|
140
145
|
return {
|
|
141
146
|
ok: config != null,
|
|
142
147
|
enabled: loopEnabled(),
|
|
143
148
|
external, // user is running their own `cswap auto`
|
|
144
149
|
lastTick: lastTick(),
|
|
145
150
|
settings: config ?? {},
|
|
151
|
+
choice, // { next: "room"|"weekly", home: {email, orgUuid}|null }
|
|
146
152
|
};
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -72,6 +72,8 @@ export { prefsPath };
|
|
|
72
72
|
*/
|
|
73
73
|
export const DEFAULTS = Object.freeze({
|
|
74
74
|
notifications: false,
|
|
75
|
+
recoverCodexSessions: true,
|
|
76
|
+
recoverClaudeSessions: true,
|
|
75
77
|
// ACCOUNT NOTIFICATIONS, one switch each (account-notify.mjs). Not part of
|
|
76
78
|
// `notifications` above, which is about a deck nobody is looking at: these
|
|
77
79
|
// are said whether a page is open or not, because no page says them. The
|
|
@@ -314,6 +316,8 @@ export function normalise(raw) {
|
|
|
314
316
|
const src = raw && typeof raw === "object" ? raw : {};
|
|
315
317
|
return {
|
|
316
318
|
notifications: flagOr(src.notifications, DEFAULTS.notifications),
|
|
319
|
+
recoverClaudeSessions: flagOr(src.recoverClaudeSessions, DEFAULTS.recoverClaudeSessions),
|
|
320
|
+
recoverCodexSessions: flagOr(src.recoverCodexSessions, DEFAULTS.recoverCodexSessions),
|
|
317
321
|
notifySwap: flagOr(src.notifySwap, DEFAULTS.notifySwap),
|
|
318
322
|
notifyQuota: flagOr(src.notifyQuota, DEFAULTS.notifyQuota),
|
|
319
323
|
notifyReset: flagOr(src.notifyReset, DEFAULTS.notifyReset),
|
|
@@ -701,7 +705,7 @@ export function publicPrefs(prefs) {
|
|
|
701
705
|
* here — prefs-route-fields.test.ts lists them.
|
|
702
706
|
*/
|
|
703
707
|
export const PAGE_FIELDS = Object.freeze({
|
|
704
|
-
top: Object.freeze(["notifications", "notifySwap", "notifyQuota", "notifyReset", "tourSeen", "autoUpdate"]),
|
|
708
|
+
top: Object.freeze(["notifications", "notifySwap", "notifyQuota", "notifyReset", "tourSeen", "autoUpdate", "recoverCodexSessions", "recoverClaudeSessions"]),
|
|
705
709
|
lan: Object.freeze([
|
|
706
710
|
"enabled", "name", "shared", "manual", "shareActive", "pairingMode",
|
|
707
711
|
"autoAsk", "autoAccept", "tailscale", "tailscaleAsk", "tailscaleAccept",
|
|
@@ -31,6 +31,7 @@ import { notifyTrays, pageCount, sseClients, trayClients, writeSse } from "./sse
|
|
|
31
31
|
import { eventLogPath, maybeRotatePersistFile, writesLogFor } from "./event-log.mjs";
|
|
32
32
|
import { appendLogLine } from "./log-writer.mjs";
|
|
33
33
|
// The away-update's word on whether a turn is running — see lifecycle.mjs.
|
|
34
|
+
import { shouldTrackActivity } from "./activity.mjs";
|
|
34
35
|
import { activity } from "./lifecycle.mjs";
|
|
35
36
|
// The session LRU and the transcript watch — see session-tracking.mjs.
|
|
36
37
|
import { outputWatch, touchSession } from "./session-tracking.mjs";
|
|
@@ -204,9 +205,9 @@ export function pushEvent(raw, source, opts = {}) {
|
|
|
204
205
|
// know the other exists. block-notify.mjs holds the gates and the cooldown.
|
|
205
206
|
blockNotifier.consider(raw, { clients: pageCount(), replay: !!opts.replay });
|
|
206
207
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
if (
|
|
208
|
+
// Persisted history is not current activity. Fresh recovery snapshots are
|
|
209
|
+
// current evidence even though they bypass replayed-tool accounting.
|
|
210
|
+
if (shouldTrackActivity(raw, opts)) activity.note(raw, evt.receivedAt);
|
|
210
211
|
|
|
211
212
|
if (persisting) {
|
|
212
213
|
// Fire-and-forget append. JSONL = newline-delimited JSON, so the whole line
|
|
@@ -64,6 +64,8 @@ const nonEmpty = v => typeof v === "string" && v.trim() !== "";
|
|
|
64
64
|
const STAMPED = [
|
|
65
65
|
["model_context_window", v => typeof v === "number" && v > 0],
|
|
66
66
|
["approval_policy", v => typeof v === "string" && v !== ""],
|
|
67
|
+
// Null is a value here: a turn that names no effort clears the chip (#2155).
|
|
68
|
+
["reasoning_effort", v => (typeof v === "string" && v !== "") || v === null],
|
|
67
69
|
["context_tokens", v => typeof v === "number" && v >= 0],
|
|
68
70
|
];
|
|
69
71
|
|
package/src/server/index.mjs
CHANGED
|
@@ -83,6 +83,7 @@ import { ccProjectSlug } from "./claude-dir.mjs";
|
|
|
83
83
|
export { ccProjectSlug };
|
|
84
84
|
// The Codex half of capture: the rollouts directory tailed for its events —
|
|
85
85
|
// see codex-watch.mjs.
|
|
86
|
+
import { createClaudeRecovery } from "./claude-recovery.mjs";
|
|
86
87
|
import { startCodexWatcher } from "./codex-watch.mjs";
|
|
87
88
|
// Exported from this file before they moved, and still. scanCodexNow is new,
|
|
88
89
|
// and is here for the suites that import this file to drive the watcher.
|
|
@@ -514,6 +515,14 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
514
515
|
server.once("close", () => trafficCapture.dispose());
|
|
515
516
|
// Codex has no working hooks on Windows — tail its rollout files instead.
|
|
516
517
|
if (codex) startCodexWatcher(workspace);
|
|
518
|
+
if (claude) {
|
|
519
|
+
const recovery = createClaudeRecovery({ workspace });
|
|
520
|
+
const recover = () => { void recovery.scan().catch(() => {}); };
|
|
521
|
+
recover();
|
|
522
|
+
const timer = setInterval(recover, 1500);
|
|
523
|
+
timer.unref?.();
|
|
524
|
+
server.once("close", () => { clearInterval(timer); recovery.stop(); });
|
|
525
|
+
}
|
|
517
526
|
// What a session is producing between its tool calls. Claude only — it
|
|
518
527
|
// reads `transcript_path`, which Codex hooks never send — and unref'd
|
|
519
528
|
// like its neighbours.
|
|
@@ -94,6 +94,10 @@ const PINNED_MODULES = [
|
|
|
94
94
|
// The plans the daily report says, read through import() by depth-facts.mjs.
|
|
95
95
|
"quota-oauth.mjs",
|
|
96
96
|
"codex-auth.mjs",
|
|
97
|
+
// The paired decks' current accounts, which Auto-switch's own rules skip
|
|
98
|
+
// (cswap-auto-choice.mjs). Already in memory through prefs-routes.mjs; named
|
|
99
|
+
// here because the tick reaches it through import().
|
|
100
|
+
"lan-deck.mjs",
|
|
97
101
|
];
|
|
98
102
|
|
|
99
103
|
let _pinned = null;
|
|
@@ -95,7 +95,14 @@ export async function readRadarConfig({ home = homedir(), env = process.env, pla
|
|
|
95
95
|
...v,
|
|
96
96
|
...(BOOLEAN_KEYS.includes(v.key) ? { editable: !uncertain && !blockedKeys.has(v.key),
|
|
97
97
|
...((uncertain || blockedKeys.has(v.key)) ? { blockedReason: uncertain ? "Settings could not be verified. Try again after they can be read." : "Controlled by organization or managed settings. Ask your administrator to change this flag." } : {}) } : {}),
|
|
98
|
-
})) };
|
|
98
|
+
})), unsetFlags: unsetFlags(new Set(variables.map(v => v.key))) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Flags no settings file names, so Radar can offer a switch that adds them.
|
|
102
|
+
// Enhanced telemetry answers to two names: offer the first unless either is set.
|
|
103
|
+
function unsetFlags(present) {
|
|
104
|
+
const enhanced = ["CLAUDE_CODE_ENHANCED_TELEMETRY_BETA", "ENABLE_ENHANCED_TELEMETRY_BETA"];
|
|
105
|
+
return BOOLEAN_KEYS.filter(key => !present.has(key) && key !== enhanced[1] && !(key === enhanced[0] && present.has(enhanced[1])));
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
// Serialize read/modify/write requests so toggling different flags preserves both.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{j as e,r,l as Q,m as _,n as Y,v as z,o as V,q as X,t as Z,w as ee,s as se,u as te,d as ne,x as ae,y as ie,R as re,h as oe}from"./index-DzDnLqDX.js";function le({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(a=>e.jsxs("div",{className:`bw-log-line ${a.level}${a.parts?"":" sys"}`,children:[e.jsx("span",{className:"bw-log-time",children:new Date(a.atMs).toLocaleTimeString("en-GB",{hour12:!1})}),a.parts?e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"bw-log-browser",children:a.parts.browser}),e.jsx("span",{className:"bw-log-profile",title:a.parts.profile,children:a.parts.profile}),e.jsxs("span",{className:"bw-log-value",children:[a.parts.value,a.parts.flagged>0&&e.jsxs("span",{className:"bw-log-flag",children:[" · ",a.parts.flagged," flagged"]})]})]}):e.jsx("span",{className:"bw-log-sys",children:a.text})]},`${a.atMs}-${a.level}-${a.text}`))})]})}const E=s=>`${s.host}-${s.startMs}`;function ce(s){const a=h=>new Date(h).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1});return s.count===1||s.endMs-s.startMs<6e4?a(s.startMs):`${a(s.startMs)} → ${a(s.endMs)}`}function de(s){return new Date(s).toLocaleDateString(void 0,{weekday:"short",day:"numeric",month:"short"})}function K(s){const a=Math.round((s.endMs-s.startMs)/6e4);return a<1?"":`${a} min`}function he({snap:s,dismiss:a,refreshRef:h}){const[c,d]=r.useState(null),u=r.useRef(new Map),t=r.useRef(null);r.useEffect(()=>{var i,w;const o=t.current;!o||s.episodes.some(b=>E(b)===o.id)||(t.current=null,Q(((i=document.activeElement)==null?void 0:i.tagName)??null)&&((w=(o.next?u.current.get(o.next):void 0)??h.current)==null||w.focus()))},[s]);const n=o=>{t.current={id:E(o),next:_(s.episodes.map(E),E(o))},a(o)},l=r.useMemo(()=>{const o=[];for(const i of s.episodes){const w=de(i.startMs),b=o[o.length-1];b&&b.label===w?b.episodes.push(i):o.push({label:w,episodes:[i]})}return o},[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:l.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."})]}):l.map(o=>e.jsxs("div",{className:"bw-day",children:[e.jsx("h4",{className:"bw-sec-head",children:o.label}),o.episodes.map(i=>{const w=E(i),b=c===w;return e.jsxs("div",{className:`bw-ep${b?" open":""}`,children:[e.jsxs("div",{className:"bw-ep-row",children:[e.jsxs("button",{className:"bw-ep-head",ref:p=>{p?u.current.set(w,p):u.current.delete(w)},onClick:()=>d(b?null:w),"aria-expanded":b,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:b?"▾":"▸"}),e.jsx("span",{className:"bw-ep-host",children:i.host}),e.jsxs("span",{className:"bw-ep-meta",children:[ce(i),K(i)&&e.jsxs(e.Fragment,{children:[" · ",K(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:"×"})]}),b&&e.jsx("ul",{className:"bw-urls",children:i.urls.map((p,y)=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-url-time",children:new Date(p.timeMs).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1})}),e.jsx("span",{className:"bw-url",children:p.url})]},`${p.url}-${p.timeMs}-${y}`))})]},w)})]},o.label))})]})}function ue(s){const a=/^#?([0-9a-f]{6})$/i.exec((s??"").trim());if(!a)return!1;const h=parseInt(a[1],16),c=[h>>16&255,h>>8&255,h&255].map(d=>{const u=d/255;return u<=.03928?u/12.92:Math.pow((u+.055)/1.055,2.4)});return .2126*c[0]+.7152*c[1]+.0722*c[2]>.5}const U=1e4,we=12e3;function me({browsers:s,findings:a,watching:h,palette:c}){const d=r.useRef(null),u=r.useRef(null);return u.current={browsers:s,findings:a,watching:h},r.useEffect(()=>{var b;const t=d.current;if(!t)return;const n=t.getContext("2d");if(!n)return;const l=((b=window.matchMedia)==null?void 0:b.call(window,"(prefers-reduced-motion: reduce)").matches)??!1;let o=0,i=!1;const w=()=>{var q;if(i)return;const p=u.current;if(!p)return;const{browsers:y,findings:j,watching:P}=p,S=c["--accent"]||"#7dd3fc",g=c["--line"]||"#1f2229",v=c["--text-dim"]||"#6e727c",$=ue(c["--bg"]),N=$?42:26,k=$?.85:.65,A=Date.now(),R=Math.min(window.devicePixelRatio||1,2),C=t.clientWidth,W=t.clientHeight;(t.width!==C*R||t.height!==W*R)&&(t.width=C*R,t.height=W*R),n.setTransform(R,0,0,R,0,0),n.clearRect(0,0,C,W);const m=C/2,x=W/2,M=Math.min(C,W)/2-6;n.strokeStyle=g,n.lineWidth=1,n.globalAlpha=$?1:.9;for(const f of[.4,.7,1])n.beginPath(),n.arc(m,x,M*f,0,Math.PI*2),n.stroke();n.beginPath(),n.moveTo(m-M,x),n.lineTo(m+M,x),n.moveTo(m,x-M),n.lineTo(m,x+M),n.stroke(),n.globalAlpha=1;const I=(P&&!l?A%U/U:0)*Math.PI*2-Math.PI/2;if(P){const f=(q=n.createConicGradient)==null?void 0:q.call(n,I-.85,m,x);f&&(f.addColorStop(0,"transparent"),f.addColorStop(.85/(Math.PI*2),`color-mix(in srgb, ${S} ${N}%, transparent)`),f.addColorStop(1,"transparent"),n.fillStyle=f,n.beginPath(),n.moveTo(m,x),n.arc(m,x,M,I-.85,I),n.closePath(),n.fill()),n.strokeStyle=S,n.globalAlpha=k,n.beginPath(),n.moveTo(m,x),n.lineTo(m+Math.cos(I)*M,x+Math.sin(I)*M),n.stroke(),n.globalAlpha=1}y.forEach((f,T)=>{const B=T/Math.max(y.length,1)*Math.PI*2-Math.PI/2,D=f.lastReadMs===null?1:Math.min(1,(A-f.lastReadMs)/12e4),O=M*(.42+D*.5),F=m+Math.cos(B)*O,L=x+Math.sin(B)*O,J=Math.abs((I-B+Math.PI*3)%(Math.PI*2)-Math.PI),G=P?Math.max(0,1-J/.55):0;n.beginPath(),n.arc(F,L,3+G*1.6,0,Math.PI*2),n.fillStyle=f.running?S:v,n.globalAlpha=f.running?.55+G*.45:.35,n.fill(),n.globalAlpha=1});for(const f of j){const T=(A-f.atMs)/we;if(T<0||T>1)continue;const D=Math.max(0,y.findIndex(L=>L.key===f.browser))/Math.max(y.length,1)*Math.PI*2-Math.PI/2,O=m+Math.cos(D)*M*.6,F=x+Math.sin(D)*M*.6;n.beginPath(),n.arc(O,F,4+T*22,0,Math.PI*2),n.strokeStyle=S,n.globalAlpha=(1-T)*.8,n.lineWidth=1.5,n.stroke(),n.globalAlpha=1}o=requestAnimationFrame(w)};return w(),()=>{i=!0,cancelAnimationFrame(o)}},[c]),e.jsx("canvas",{ref:d,className:"bw-radar",role:"img","aria-label":h?`Watching ${s.length} browser${s.length===1?"":"s"}`:"Not watching"})}function be({snap:s,watching:a,palette:h}){const[c,d]=r.useState(!1),[u,t]=r.useState(()=>Date.now());r.useEffect(()=>{const l=setInterval(()=>t(Date.now()),1e3);return()=>clearInterval(l)},[]);const n=Y(s.coverage.lastHumanMs,s.coverage.quietMs,u);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":c,"aria-label":"What the sweep and the dots mean",children:"?"})]}),e.jsxs("div",{className:"bw-glance",children:[e.jsx(me,{browsers:a.map(l=>{var o;return{key:l.key,name:l.name,running:l.running,lastReadMs:((o=s.profiles.find(i=>i.browser===l.key))==null?void 0:o.lastWrittenMs)??null}}),findings:s.episodes.slice(0,6).map(l=>({browser:l.browser??null,atMs:l.endMs})),watching:s.settings.enabled,palette:h}),e.jsx("ul",{className:"bw-counts",children:z(a,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 ",V(s.coverage.checkedMs,u),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 ",X(n)]}),c&&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 pe({watching:s,rest:a}){const[h,c]=r.useState(!1),[d,u]=r.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:()=>c(t=>!t),"aria-expanded":h,"aria-label":"What running, idle and connected to Anthropic mean",children:"?"})]}),h&&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(t=>e.jsxs("li",{children:[e.jsx("span",{className:`bw-prof-dot${t.running?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-prof-name",children:t.name}),e.jsx("span",{className:"bw-prof-state",children:t.running?"running":"idle"}),t.running&&t.relay.state==="live"&&e.jsx("span",{className:"bw-relay",children:"connected to Anthropic"})]},t.key))}),a.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"bw-why bw-rest-head",onClick:()=>u(t=>!t),"aria-expanded":d,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:d?"▾":"▸"})," ",a.length," not watched"]}),d&&e.jsxs("p",{className:"bw-rest",children:[a.map(t=>t.name).join(", ")," — not installed, or installed and never opened, so there is no history to read."]})]})]})}function ge({snap:s,quiet:a,setQuiet:h,save:c,access:d,setAccess:u}){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:a??s.settings.quietMinutes,onChange:t=>{h(Number(t.target.value)),c({quietMinutes:Number(t.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:t=>void c({reaction:t.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(t=>e.jsx("option",{value:t,children:t==="notify"?"notify me":t==="close-tab"?"close the tab":"quit the browser"},t))})]}),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:()=>u(t=>!t),"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"?` (${Z(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,a){return a?{kind:"saving",word:"Saving"}:s.settings.enabled?{kind:"on",word:"Watching"}:{kind:"off",word:"Paused"}}function fe({snap:s,saving:a,save:h,why:c,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:H(s,a).word},H(s,a).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${c?" on":""}`,onClick:()=>d(u=>!u),"aria-expanded":c,"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 h({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 xe({relay:s}){const[a,h]=r.useState(!1),[c,d]=r.useState(!1),u=s.killswitch.blocked?"unblock":"block",t=s.command[u],n=r.useCallback(async()=>{await ee(t.command)&&(d(!0),window.setTimeout(()=>d(!1),1600))},[t.command]),l=s.profiles.filter(i=>{var w;return(w=i.report)==null?void 0:w.present}),o=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:()=>h(i=>!i),"aria-expanded":a,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:a?"▾":"▸"})," ",s.verdict==="protected"?"How to undo the block":"What it can reach, and how to stop it"]}),a&&e.jsxs("div",{className:"bw-rc-body",children:[l.length>0&&e.jsx("dl",{className:"bw-key",children:l.map(i=>e.jsxs(r.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}`))}),o.length>0&&e.jsxs("p",{className:"bw-rc-note",children:[o.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."]}),l.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:t.command}),e.jsx("button",{className:"btn bw-rc-copy",onClick:n,children:c?"Copied":"Copy"})]}),e.jsx("p",{className:"bw-rc-note",children:t.note})]})]})}function je(s){const[a,h]=r.useState(null),[c,d]=r.useState(null),[u,t]=r.useState(null),[n,l]=r.useState(!1),o=r.useRef(!1),[i,w]=r.useState(null),[b,p]=r.useState(!1),y=r.useRef(!1),j=r.useCallback(async g=>{var v,$;if(!(g&&!se(o.current))){g&&(o.current=!0,l(!0));try{const N=await fetch(`/api/browser-watch${g?"?refresh=1":""}`);if(!N.ok)throw new Error(`the deck answered ${N.status}`);const k=await N.json();h(k),y.current||w(((v=k==null?void 0:k.settings)==null?void 0:v.quietMinutes)??15),s((($=k==null?void 0:k.settings)==null?void 0:$.enabled)===!0),d(null)}catch(N){d(N instanceof Error?N.message:String(N))}finally{g&&(o.current=!1,l(!1))}}},[s]);r.useEffect(()=>{j(!1)},[j]),r.useEffect(()=>{const g=setInterval(()=>{j(!1)},1e4);return()=>clearInterval(g)},[j]);const P=r.useCallback(async g=>{try{await fetch("/api/browser-watch/dismiss",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({host:g.host,startMs:g.startMs})})}finally{await j(!1)}},[j]),S=r.useCallback(async g=>{y.current=!0,p(!0);try{const v=await fetch("/api/browser-watch",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(g)});if(!v.ok)throw new Error(`the deck answered ${v.status}`);await j(!1),t(null)}catch(v){t(v instanceof Error?v.message:String(v))}finally{y.current=!1,p(!1)}},[j]);return{snap:a,error:c,writeError:u,setWriteError:t,busy:n,quiet:i,setQuiet:w,saving:b,load:j,dismiss:P,save:S}}function Ne({onClose:s,onSeen:a,onWatching:h,palette:c}){const d=te(s),u=ne(s),{snap:t,error:n,writeError:l,setWriteError:o,busy:i,quiet:w,setQuiet:b,saving:p,load:y,dismiss:j,save:P}=je(h),[S,g]=r.useState(!1),[v,$]=r.useState(!1),N=r.useRef(null),k=r.useRef(a);k.current=a,r.useEffect(()=>()=>k.current(Date.now()),[]);const A=ae(t==null?void 0:t.browsers),R=((t==null?void 0:t.browsers)??[]).filter(m=>!(m.installed&&m.profiles>0)),C=t?ie(t):null,W=A.reduce((m,x)=>m+x.profiles,0);return e.jsx("div",{className:"modal-backdrop",...u,role:"presentation",children:e.jsxs("div",{ref:d,className:"modal bw-modal",onClick:m=>m.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":"bw-title",children:[e.jsxs("header",{className:"modal-head",children:[e.jsxs("div",{className:"modal-title",children:[e.jsx("span",{className:"modal-tool-name",id:"bw-title",children:"Browser watch"}),e.jsx("span",{className:"modal-tool-id",children:t?`${W} ${W===1?"profile":"profiles"} · local only`:"local only"})]}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{ref:N,className:"glyph-btn",onClick:()=>void y(!0),...oe(i),"aria-label":"Re-read every profile now",title:"Re-read every profile now",children:i?"…":e.jsx(re,{})}),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:[!t&&!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})]})}),t&&C&&e.jsx("p",{className:`bw-trouble ${C.kind}`,role:"status",children:C.text}),t&&e.jsxs("div",{className:"bw-work",children:[e.jsxs("aside",{className:"bw-side",children:[e.jsx(be,{snap:t,watching:A,palette:c}),e.jsx(pe,{watching:A,rest:R}),t.relay&&e.jsx(xe,{relay:t.relay})]}),e.jsxs("div",{className:"bw-main",children:[e.jsx(he,{snap:t,dismiss:j,refreshRef:N}),e.jsx(le,{snap:t})]})]}),t&&S&&e.jsx(ge,{snap:t,quiet:w,setQuiet:b,save:P,access:v,setAccess:$}),l&&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:[l," — the switches below show what is stored, not what you pressed."]}),e.jsx("button",{className:"glyph-btn bw-write-err-x",onClick:()=>o(null),"aria-label":"Dismiss",title:"Dismiss",children:"×"})]})}),t&&e.jsx(fe,{snap:t,saving:p,save:P,why:S,setWhy:g})]})]})})}export{Ne as default};
|