ccdeck 3.41.3 → 3.43.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 +38 -18
- package/bin/deck.js +4 -0
- package/dist/web/assets/BrowserWatchModal-BPws0psH.js +1 -0
- package/dist/web/assets/RadioPanel-CDl7ZImm.js +1 -0
- package/dist/web/assets/TrafficRadar-D1hp6BgZ.js +1 -0
- package/dist/web/assets/UsageHistoryModal-DPcdpBpI.js +1 -0
- package/dist/web/assets/app-9p127oSR.js +310 -0
- package/dist/web/assets/app-DsHSufBt.css +1 -0
- package/dist/web/assets/index-Bkx27-wy.js +1 -0
- package/dist/web/index.html +18 -4
- package/package.json +1 -1
- package/release-notes.json +40 -0
- 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/agent-activity.mjs +33 -10
- package/src/server/browser-watch-decks.mjs +3 -2
- package/src/server/browser-watch.mjs +3 -4
- 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 +116 -12
- package/src/server/cswap-auto-choice.mjs +296 -0
- package/src/server/cswap-auto-loop.mjs +18 -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 +6 -3
- package/src/server/evicted-enrichment.mjs +2 -0
- package/src/server/feedback-form.mjs +1 -0
- package/src/server/index.mjs +13 -0
- package/src/server/pinned-build.mjs +5 -0
- package/src/server/reports-routes.mjs +31 -12
- package/src/server/request-gates.mjs +2 -0
- package/src/server/tool-audit-operations.mjs +112 -0
- package/src/server/tool-audit-report.mjs +115 -0
- package/src/server/tool-audit-routes.mjs +38 -0
- package/src/server/tool-audit.mjs +126 -0
- package/src/server/traffic-radar-config.mjs +8 -1
- package/dist/web/assets/BrowserWatchModal-6TctCbc4.js +0 -1
- package/dist/web/assets/RadioPanel-ZDDwNYnL.js +0 -1
- package/dist/web/assets/TrafficRadar-lfG5ekUR.js +0 -1
- package/dist/web/assets/UsageHistoryModal-Be-9GVIE.js +0 -1
- package/dist/web/assets/index-BgSnAnP6.css +0 -1
- package/dist/web/assets/index-C80Hp_2s.js +0 -307
package/README.md
CHANGED
|
@@ -96,8 +96,9 @@ The deck opens on these eight pictures the first time it runs — they are the w
|
|
|
96
96
|
| **Blocked on you** | A permission prompt, or a finished turn waiting for your next instruction, sorts that session to the top of the sidebar with how long it has been stuck — longest wait first, so the oldest block is the first row. A permission prompt also puts a count in the topbar that jumps straight to it, and the session's name beside it, with how long it has waited and on what — one click from its card. Claude Code only — the deck reads Codex from its rollout log, and a rollout carries no such signal. |
|
|
97
97
|
| **What each session is doing** | The note tied to each session's card — and the line under its row in the sidebar — says it while it works: the sentence the newest reply wrote, or the description on the command it ran. Once the turn is over it keeps the last thing the session did, until Claude Code's recap takes its place. A background session (`claude --bg`, or one started from `claude agents`) shows Claude Code's own line instead, read from its job folder in `~/.claude/jobs`: the question it is stuck on and the reply Claude Code suggests, or the headline of what it got done. No model is called for any of it. A Codex session, read from its rollout log, has the note only while a turn runs, and it says the first line of the prompt that began the turn: no line is taken from a Codex reply, and no recap follows. |
|
|
98
98
|
| **Live DAG** | Nodes are agents and edges are spawns; each agent's latest tool calls sit beside its node and light up while they run. In-flight edges animate, settled ones dim. |
|
|
99
|
-
| **Both providers, one canvas** | Claude Code through hooks, Codex through its rollout log. The model chip (`Opus 5`, `GPT-5.5`) tells them apart.
|
|
99
|
+
| **Both providers, one canvas** | Claude Code through hooks, Codex through its rollout log. The model chip (`Opus 5`, `GPT-5.5`) tells them apart. Codex child sessions are connected when their rollout records an explicit parent. Each thread keeps its own tool calls and token totals. |
|
|
100
100
|
| **Cost and quota, live** | Spend per model and per session, plus Claude and Codex quota windows as they refill. |
|
|
101
|
+
| **Tool activity** | Which tools and commands your Claude Code and Codex agents ran on this machine, over the last day or up to 90 days: how many calls, how long they took, which ones fail. Kept on this machine and nowhere else. |
|
|
101
102
|
| **Double-click to inspect** | Any node opens its prompt, tool calls, token usage and timing in the side panel. A single click selects it and frames its session. |
|
|
102
103
|
| **Survives restarts** | Events are appended to this platform's log directory (see `--history` below) and replayed on open. |
|
|
103
104
|
| **Accounts without a terminal** | Sign a new Claude account in, move one or your whole set to another machine, rename, reorder, remove — from the panel. |
|
|
@@ -120,14 +121,18 @@ log in. **`ccdeck --stop` is the off switch**; `Ctrl+C` only cancels a start tha
|
|
|
120
121
|
is still printing. `ccdeck --status` says what is running, and `--foreground`
|
|
121
122
|
holds the terminal the way every version before 3.20 did.
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
Sessions already working when ccdeck starts can appear with a **restored** marker. Codex recovery reads recent unfinished turns from local rollouts; an absent ending does not confirm that its process is still running. On macOS and Linux, Claude recovery verifies a busy session against its process-start identity. **Settings › General** has a separate startup-recovery switch for each provider. Earlier tool calls are not replayed. [Recovery behavior and limits](docs/features/session-recovery.md).
|
|
125
|
+
|
|
126
|
+
The panels open from the window's edges: the session list and the Claude accounts panel from the stripe on the left, Usage, This machine, Usage history, Tool activity and Browser Watch from the one on the right. Each shows or hides beside the canvas, and several can be open at once. Point at one, or Tab to it, and its hint names its key — `L`, `A`, `U`, `S`, `H`, `B` — while the single-key shortcuts are on; Tool activity has none. On a phone they are in a dock along the bottom, and the session list and the Claude accounts panel open as a drawer over the canvas rather than beside it: picking a session closes the list, so the session is on screen.
|
|
124
127
|
|
|
125
128
|
Two notes can appear at the bottom of the canvas: **Undo**, for a few seconds after `R` or the re-arrange button lays the canvas out again — `Ctrl+Z` (`⌘Z` on a Mac) does the same — and **Resume**, which turns auto-fit back on after you have panned or zoomed.
|
|
126
129
|
|
|
127
|
-
Everything the page lets you set apart from the music — the theme, the character on the canvas, the single-key shortcuts, the sounds and notifications — is in **Settings**, behind the gear at the right of the topbar, or `Cmd+,` (`Ctrl+,` on Windows and Linux); the music is in **Radio**, below.
|
|
130
|
+
Everything the page lets you set apart from the music — the theme, the text size and spacing, the character on the canvas, the single-key shortcuts, the sounds and notifications, how long Tool activity keeps its history — is in **Settings**, behind the gear at the right of the topbar, or `Cmd+,` (`Ctrl+,` on Windows and Linux); the music is in **Radio**, below. Changes apply immediately except startup recovery, which applies at the next server start. **Settings › General** holds twelve color themes, Gruvbox Dark, Gruvbox Light and Gruvbox Material the newest — a fresh install starts in Rider Black rather than following your system's light or dark setting, a theme you picked stays, `T` steps through all twelve, and the deck fades from one theme to the next over 180 ms, or switches at once when your system asks for reduced motion — and the character: Deck companion, Fox or Penguin, with a switch to hide it. **Settings › Text & density** has five text sizes — Smallest, Smaller, Default, Larger and Largest, 11, 12, 13, 15 and 17 px for body text — and five densities for the spacing in lists, panels and controls: Tightest, Tighter, Compact, Default and Comfortable, at 30%, 50%, 70%, 100% and 140% of the designed spacing. Both are kept in this browser, and neither is a zoom: your browser's zoom and the canvas's stay where they were, the panels widen for larger text, and the cards on the canvas follow it. **Reset to defaults** puts both back. The sounds and how loud each tone is are in **Settings › Sounds**, which `V` opens straight away; `M` turns sounds off or on from anywhere. `?` lists every key. If you use voice control or press letters by accident, turn off **Single-key shortcuts** in **Settings › General**: letters, `?` and Space then do nothing, while `Cmd+,`, `Esc`, `Enter`, `Tab`, `Delete` and the arrows keep working.
|
|
128
131
|
|
|
129
132
|
**Radio**, on the right sidebar beside the character (in **More** on a phone), opens a responsive Radio Explorer with a large interactive map, station discovery and a persistent player. Filter by country, sound or language, save favorites, return to recent stations, or choose **Surprise me**. Your existing Claude FM, Lofi Girl and custom streams are under **Your streams**. Select a station to inspect it, then press Play to listen; closing Radio or hiding the character keeps it playing. Favorites, history and the last catalogue are saved in this browser. Control music directly in Radio Explorer; character settings are in **Settings › General**.
|
|
130
133
|
|
|
134
|
+
**Tool activity**, on the stripe on the right below Usage history (in **More** on a phone), is a page of which tools and commands the Claude Code and Codex agents on this machine used. For the last 24 hours, 7, 30 or 90 days, and both CLIs or one, it lists each tool's calls, its total and usual time and its failures, sortable by any of them and searchable, under up to three findings worth a look: the tool that takes the most time, one that fails often, the slowest. A row opens that tool's details beside the list: who used it, its calls per day, its latest and slowest calls. A shell command counts per operation — `git status`, `git push` and `npm run build` are rows of their own — and an MCP tool under its own name. A call's time runs from the agent starting the tool to its result, so it includes any wait for your approval, and Claude Code and Codex time their tools differently. It counts what the deck captures from the first time it runs at 3.42.0 or later — earlier sessions are not read back — and it is kept apart from the canvas: Clear empties the canvas and leaves the tool history. What it keeps, and where, is in [What it touches](#what-it-touches).
|
|
135
|
+
|
|
131
136
|
No config file. No account. What your sessions contain — your prompts, the replies, the files they touch — is never reported anywhere. The deck does send usage reports, on by default: its version, your system, your IP address and a device fingerprint, rough counts such as how many sessions ran in a day, and the errors it hits. It has no switch for them; `AGENTS_DECK_NO_REPORTS=1` keeps them off from the first start.
|
|
132
137
|
|
|
133
138
|
**The deck cannot steer your agent.** The hook it installs is a one-way forwarder: it POSTs the event, exits `0`, and writes nothing to stdout. Those are the two channels Claude Code's hook protocol gives a hook for allowing, denying, deferring or rewriting the tool call it was told about, and this one uses neither — it has no way to answer at all. `src/web/__tests__/hook-read-only.test.ts` pins both halves, over the source and by running the real script.
|
|
@@ -153,7 +158,7 @@ The same deck as an app: it starts the deck itself and puts an icon in the menu
|
|
|
153
158
|
|
|
154
159
|
The window is the deck under one compact toolbar that follows its theme, with the system's own window controls; on Windows and Linux the **Application menu** button at the toolbar's left end opens the app's menu.
|
|
155
160
|
|
|
156
|
-
The app keeps itself current from this repository's releases, and installs an update only if it carries ccdeck's own signature. It installs on Quit, or by itself once the app has been left alone for a minute
|
|
161
|
+
The app keeps itself current from this repository's releases, and installs an update only if it carries ccdeck's own signature. It installs on Quit, when you choose **Restart to update**, or by itself and restarts into the new version — but only once the app has been left alone for a minute (its window closed or not in focus) and none of your agents has been working or waiting for a minute. If your agents never stop, it waits a day for them and then installs at the next quiet minute; the deck is gone for a second or two, which can leave a gap in the drawing of a turn, and the agents carry on. **Later**, on the notice that an update is ready, holds it off for 24 hours. While it waits, the app looks again every half hour for a newer release, so it installs the latest one rather than one that is already behind. On the .deb, where installing asks for your password, it installs only when you choose Restart to update. **Check for updates**, in the icon's menu or in What's new, asks the app's own updater. On Windows, **Restart to update** stops the deck the app started before the installer runs.
|
|
157
162
|
|
|
158
163
|
Step by step: [Install the ccdeck app on Mac, Windows or Linux](https://ccdeck.dev/guides/desktop-app/)
|
|
159
164
|
|
|
@@ -181,8 +186,10 @@ phone. Three views keep configuration, observed traffic and saved files separate
|
|
|
181
186
|
are shown as written. These are settings-file evidence, not confirmation of a
|
|
182
187
|
running session's effective environment. Credentials and endpoint paths stay
|
|
183
188
|
hidden. Each recognized telemetry/content flag has a switch that saves its
|
|
184
|
-
value to Claude's user settings for new sessions
|
|
185
|
-
|
|
189
|
+
value to Claude's user settings for new sessions, including a flag no settings
|
|
190
|
+
file names yet, which shows **Not set** with its switch off. Flags set by
|
|
191
|
+
organization or managed settings show **Managed** beside a locked switch and
|
|
192
|
+
require an administrator to change;
|
|
186
193
|
Radar does not rewrite cached organization policy. Controls are unavailable
|
|
187
194
|
when configuration cannot be verified. Unknown values offer explicit
|
|
188
195
|
**Enable** and **Disable** actions.
|
|
@@ -320,13 +327,17 @@ It never steers an agent or edits your code, but it is not read-only either —
|
|
|
320
327
|
|
|
321
328
|
Telemetry Radar's flag switches write to `~/.claude/settings.json` too: each puts the flag you set — `CLAUDE_CODE_ENABLE_TELEMETRY`, `OTEL_LOG_USER_PROMPTS` and the other telemetry and content flags Radar lists — in that file's `env` block, as `"1"` or `"0"`. Switching a flag off writes `"0"` rather than removing it, and `--uninstall`, which removes ccdeck's hook entries, leaves these values as you set them. Starting Radar's monitoring writes `~/.agents-deck/radar-monitor-<port>.json`: the opt-in and the destinations a deck on that port resumes, never packets or decoded messages. And the quota readings the deck takes are kept in `quota-history.json`, in its data directory (see [Uninstall](#uninstall)), for 35 days and up to 2 MB, under a hash of the account rather than its address, for the Usage panel's quota history.
|
|
322
329
|
|
|
323
|
-
|
|
330
|
+
Tool activity keeps its history in `tool-audit/`, in the same data directory: a file a day with a line per tool call — Claude Code or Codex, when it started and ended, whether it failed, and the tool's name or, for a shell command, its operation (`git push`, `npm run build`) — and a `settings.json` holding how long to keep it, when it started counting, and a random id for this device that nothing sends anywhere. The rest of a command line, the tool's arguments and output, paths and session ids are not saved, and nothing in the folder leaves the machine. It keeps 90 days unless you pick 30 or 60 under **Keep history** in **Settings › General › Tool history**, which also shows the space it takes and has **Clear history…**; older days are deleted as they fall out. `--no-persist` does not stop it — that flag is about the events log — and `--uninstall` leaves it, `--purge` too (see [Uninstall](#uninstall)).
|
|
331
|
+
|
|
332
|
+
What does go out is short and ordinary: a ~20-byte version check against the npm registry (plus one small request to confirm a version it has not seen before), installs and daily version checks for the two tools the deck manages (claude-swap from PyPI, ccusage from npm), on an Apple Silicon Mac whose sensors stay silent one release lookup and one binary download from GitHub for `macmon`, and, while the page is open or one of the two quota notifications is on, quota reads to Anthropic and OpenAI signed with your own credentials — that is where those numbers live. While the page is open, and while the desktop app has a session running or waiting, it also reads Anthropic's and OpenAI's public status pages (`status.claude.com`, `status.openai.com`), unsigned, about every three minutes, and once a minute at most while a page is not answering, so an outage on their side shows as a chip in the topbar instead of looking like a fault on yours. While the machine panel is open, on a machine with Claude Code, it times the way to Claude every thirty seconds — a DNS lookup of `api.anthropic.com` and a TCP handshake on port 443, with nothing sent over it — for the panel's latency reading. While the Browser Watch panel is open, it looks up the address of Claude's browser relay, `bridge.claudeusercontent.com`, about every thirty seconds — a DNS query to your resolver — to tell whether a running browser holds a connection to it. The [desktop app](#desktop-app) also asks this repository's GitHub releases for its own updates. With **Local network** on, which it is unless you switch it off, the deck also announces itself to other decks on your local network over UDP (port 45317) and pairs with the ones that answer — see [Local network](#local-network). The deck also sends usage reports to `api.ccdeck.dev`: an install event (naming the ccdeck.dev page the command was copied from, when the command says so with `--ref`, which the site's copy buttons add; a page name and nothing else, sent once), an update event (saying how the new version arrived: the deck's own update, the desktop app's updater, npx, npm or a source checkout), one "active" a day, one "uninstall" when you run `ccdeck --uninstall` (which asks, at a terminal, one skippable question about why, from a short list), one "activated" when a new install's first session arrives (how long after the install, in rough buckets such as "under five minutes", and whether it was a Claude or a Codex session), one "rated" if you answer the one question the deck asks about itself after about a week of use — "How useful is ccdeck to you?", 0 to 10, which you can skip — with the number you picked and how many days you had used the deck, as a range (nothing is sent for a question you put off or leave), and the errors it runs into, with every path outside ccdeck's own code, project folder names, the machine name in every web address, every IP address and the machine name a network error carries (the loopback, such as `127.0.0.1:4317`, stays, so a local port error still reads as one), email addresses and keys scrubbed out (a line of ccdeck's own code keeps its place in the package, such as `ccdeck/src/server/tail.mjs:42`, or in the page, such as `<deck>/assets/index-abc.js:1:2345`, so the error still says where it happened). Each carries the version, your OS and CPU architecture, the channel (desktop, npm or a source checkout), your IP address and the machine's rough shape — the number of CPU cores, the amount of RAM, the locale, the shell, the terminal, and the Claude Code and Codex versions. The install, update, active, activated, rated and uninstall reports also carry the runtime and a device fingerprint (a stable hashed machine id); an error report carries neither. The first four also say whether the Claude hooks went in and Codex is being watched, with the daily "active" also carrying how many sessions, subagents and projects you ran on the last day you used the deck, that day's date, and which of the deck's own features you used that day — fixed names such as "usage history" or "account switch", a yes for each and nothing about what you did with them — plus how the deck held up and what it runs with: how long this run took to start, that day's peak memory and how many events it took in, each as a rough bucket such as "under two seconds" or "256–512 MB", the Claude and Codex plan as its category only (such as "Max 5x" or "Plus", read from the file each CLI keeps beside its login, and nothing else from it), and how many Claude accounts and paired machines the deck holds, as counts. The fingerprint is a one-way hash of stable machine traits, never those traits in the clear; everything else is coarse counts and short tokens — no path, no project name, no free text. These reports are no longer anonymous: the install id is random, but the fingerprint follows this machine and the server records the IP the report arrives from. What still never leaves is your sessions, prompts, files, project names and paths. The deck has no switch for them; `AGENTS_DECK_NO_REPORTS=1` keeps them off from the first start. While the deck runs it also sends a tiny heartbeat every few minutes, the install id and nothing else, so the count of decks running right now, and how many hours a day a deck stays open, can be seen. **Send feedback** (the topbar's Feedback button; on a phone, under More) sends what you write, and any screenshots and contact you add, to the same place, with the ccdeck version and your system (such as `darwin-arm64`), which the dialog names before you send, and, while the usage reports are on, the same device fingerprint they carry, which it does not show; `AGENTS_DECK_NO_REPORTS=1` keeps the fingerprint home and the feedback still goes. As with a report, the server records the IP address it arrives from. The people who make ccdeck read it there and may open a public issue from it, never with the contact you gave or your screenshots. `AGENTS_DECK_NO_INSTALL=1` turns off everything but the quota reads; `AGENTS_DECK_NO_DOWNLOAD=1` is the narrower version — no `uv` binary is fetched, the managed installs stay.
|
|
324
333
|
|
|
325
334
|
Local network, Claude accounts, Usage and the empty canvas each carry a small **Watch · 30 s** link to their 30-second video on [ccdeck.dev](https://ccdeck.dev/guides/). Opening one is your click, in your own browser; the deck itself sends nothing for it.
|
|
326
335
|
|
|
327
336
|
## Browser Watch
|
|
328
337
|
|
|
329
|
-
The panel behind the eye on the window's right edge answers one question: **did something drive your browser while you were away?**
|
|
338
|
+
The panel behind the eye on the window's right edge answers one question: **did something drive your browser while you were away?** Chromium marks the most common way a program goes to an address: a page loaded with the DevTools protocol's `Page.navigate` (what Playwright and Puppeteer navigate with), or by an extension's `tabs.update`, carries the `FROM_API` flag in `visits.transition`, on top of its type (`LINK`, `TYPED` and the rest). So a run of those, in a stretch when nobody opened a page by hand in that profile, is a finding worth showing. The deck has no idle timer: it goes by the history alone.
|
|
339
|
+
|
|
340
|
+
**What it misses.** Not every way a program opens a page sets the flag. A tab opened straight at an address (`Target.createTarget` over the DevTools protocol, or an extension's `tabs.create`), a link a program clicks, and a URL handed to the browser by `open` or `xdg-open` carry none (tested on Google Chrome 155 on macOS and Brave 1.95 on Linux, against Chromium's source). Those pages are not reported, and because any visit without the flag counts as a person at the browser, they can also cancel a finding close to them.
|
|
330
341
|
|
|
331
342
|
To answer it the deck reads the browsers' own history. That means, on each poll: it finds every Chromium profile (Chrome, Brave, Edge, Vivaldi), **copies each profile's `History` database** to a temp file — a live database cannot be opened safely — queries the visits newer than the moment this deck started, and deletes the copy. None of what it reads leaves the machine, and the copy exists for as long as one query takes. The one thing the watch sends out is a DNS query: while the panel is open, it looks up the address of Claude's browser relay (`bridge.claudeusercontent.com`) about every thirty seconds, to show whether a running browser is connected to it. The background poll does not.
|
|
332
343
|
|
|
@@ -344,11 +355,13 @@ The deck installs that package itself, so it installs it **bounded**: `claude-sw
|
|
|
344
355
|
|
|
345
356
|
**Switch** on an account's row makes it the active one. New sessions start on it; sessions already running are not restarted, and pick it up on their next message — up to about 30 seconds later on macOS. **Auto-switch**, off until you turn it on, does the same without a press: about once a minute the deck runs claude-swap's own engine, which, once the active account passes your threshold (90% unless you choose another) of its 5-hour or 7-day window, moves Claude Code to the account with the most room left. Every account keeps its own limits; when no other account is under the threshold, nothing switches. **Custom…**, last in the threshold picker, takes any whole percent from 50 to 99 and refuses anything else under the field without saving it; the threshold is claude-swap's own setting, kept on this machine.
|
|
346
357
|
|
|
358
|
+
Two choices change where Auto-switch goes, and do nothing while it is off. **Next account**, under the switch, is **Most room left** — claude-swap's own pick, and the default — or **Soonest weekly reset**: the account whose 7-day window resets soonest, as long as that is within 95 hours, so quota that is about to reset is used first. With it, the deck also moves below the threshold, to an account that resets at least an hour sooner than the one in use, and never sooner than half an hour after the last move. **Set as home**, in an account's **⋯**, marks the row with ⌂: Auto-switch takes you there, does not move you off it before the threshold, and comes back once its 5-hour window has reset. Only an account comfortably under the threshold, read in the last 15 minutes, signed in, in rotation, not set aside by claude-swap and not in use on a paired deck or in a `cswap run` session is moved to. A switch you make yourself is left alone until that account reaches the threshold, or until you set a home or pick a rule again. When no account fits either rule, claude-swap decides, as before. Both choices are kept on this machine.
|
|
359
|
+
|
|
347
360
|
**Account capacity**, the gauge in the panel's header, says how many accounts are ready and what their 5-hour and 7-day windows have left. Its **Reset calendar** view lays out today and the next six days, with each account's 7-day reset on the day it falls, and marks the readings that need refreshing.
|
|
348
361
|
|
|
349
362
|
**Account notifications** are three switches in **Settings › Notifications** (the gear in the topbar, or `Cmd+,`; `Ctrl+,` on Windows and Linux), each on its own and none of them tied to **Notifications while closed**: they are sent whether the deck is open or not, because nothing on the page says them. The desktop app shows them as ccdeck, without a sound.
|
|
350
363
|
|
|
351
|
-
- **Account auto-switched**, on unless you turn it off. When Auto-switch moves Claude Code to another account, one notification says where to and, when the engine's own reading shows it, why: `Switched to work · personal@example.com reached 90%`, with an account's alias when it has one. A switch you make yourself is not announced.
|
|
364
|
+
- **Account auto-switched**, on unless you turn it off. When Auto-switch moves Claude Code to another account, one notification says where to and, when the engine's own reading shows it, why: `Switched to work · personal@example.com reached 90%`, `Back on work · your home account` or `Switched to side · weekly limit resets in 1d 7h`, with an account's alias when it has one. A switch you make yourself is not announced.
|
|
352
365
|
- **Quota at 90% and 100%**, off until you turn it on. When a window of the Claude or Codex account you are on reaches 90%, and once more at 100%: Claude's 5-hour, 7-day and per-model windows, and every Codex window the Usage panel shows, the extra limits included — `5-hour usage reached 90% · resets in 2h 14m`. Only the account in use is watched, not every account you have stored. When reaching it is what makes Auto-switch move the account, the switch notification says it and this one is not sent, unless the switch notification is off.
|
|
353
366
|
- **Quota reset**, off until you turn it on. When one of those windows that had reached 90% starts over: `5-hour window available again`. A window that never got that high resets without a word.
|
|
354
367
|
|
|
@@ -541,7 +554,7 @@ Environment:
|
|
|
541
554
|
| `AGENTS_DECK_NO_FRESHEN=1` | Never nudge claude-swap to collect usage early |
|
|
542
555
|
| `AGENTS_DECK_NO_NOTIFY=1` | Never raise a desktop notification: not when a session blocks and no page is open, not when Browser Watch finds something, and not about your accounts |
|
|
543
556
|
| `AGENTS_DECK_NO_LAN=1` | Keep **Local network** off, whatever its switch in the panel says |
|
|
544
|
-
| `AGENTS_DECK_NO_REPORTS=1` | Never send usage reports |
|
|
557
|
+
| `AGENTS_DECK_NO_REPORTS=1` | Never send usage reports, nor the device fingerprint with feedback |
|
|
545
558
|
| `AGENTS_DECK_CSWAP` | Full path to `cswap`, when it lives somewhere unusual |
|
|
546
559
|
| `AGENTS_DECK_CLAUDE` | Full path to the `claude` CLI |
|
|
547
560
|
| `AGENTS_DECK_CCUSAGE` | Full path to your own `ccusage`, used ahead of everything else |
|
|
@@ -574,9 +587,9 @@ Removes every hook entry ccdeck injected from `~/.claude/settings.json`, and `~/
|
|
|
574
587
|
|
|
575
588
|
It removes the hook entries and nothing else. The forwarder script
|
|
576
589
|
(`~/.claude/agent-dag/hook.js`), the discovery directory around it, the events
|
|
577
|
-
log, the deck's own state,
|
|
578
|
-
ccusage, and a `uv` binary if it had to
|
|
579
|
-
each has its own uninstaller.
|
|
590
|
+
log, the deck's own state, Tool activity's history included, and the tools
|
|
591
|
+
ccdeck installed for you — claude-swap, ccusage, and a `uv` binary if it had to
|
|
592
|
+
fetch one — are all left in place, and each has its own uninstaller.
|
|
580
593
|
|
|
581
594
|
Three directories hold ccdeck's own files, and the second one matters most:
|
|
582
595
|
`~/.claude/agent-dag/` (the forwarder and the discovery directory), the
|
|
@@ -584,7 +597,7 @@ platform's **data** directory, and the platform's **log** directory.
|
|
|
584
597
|
|
|
585
598
|
| | macOS | Windows | Linux |
|
|
586
599
|
|---|---|---|---|
|
|
587
|
-
| data — `prefs.json` | `~/Library/Application Support/ccdeck` | `%LOCALAPPDATA%\ccdeck\Data` | `$XDG_DATA_HOME/ccdeck` |
|
|
600
|
+
| data — `prefs.json`, `tool-audit/` | `~/Library/Application Support/ccdeck` | `%LOCALAPPDATA%\ccdeck\Data` | `$XDG_DATA_HOME/ccdeck` |
|
|
588
601
|
| log — `events.jsonl` | `~/Library/Logs/ccdeck` | `%LOCALAPPDATA%\ccdeck\Log` | `$XDG_STATE_HOME/ccdeck` |
|
|
589
602
|
|
|
590
603
|
`prefs.json` is the one to delete deliberately: it holds this deck's **Local
|
|
@@ -593,6 +606,13 @@ behind leaves that identity on the machine. (`~/.agents-deck/` holds the managed
|
|
|
593
606
|
tools and the deck's records about them, and `radar-monitor-<port>.json` once
|
|
594
607
|
Telemetry Radar has monitored — the destinations it resumes — but no key.)
|
|
595
608
|
|
|
609
|
+
The data directory also holds Tool activity's `tool-audit/` folder — up to 90
|
|
610
|
+
days of which tools and commands your agents ran, and how long they took — and
|
|
611
|
+
`quota-history.json`. Neither holds a key, so `--uninstall` leaves both, and so
|
|
612
|
+
does `--purge` below, which deletes only the files that can hold one. **Clear
|
|
613
|
+
history…** under **Settings › General › Tool history** empties the tool history
|
|
614
|
+
while the deck runs; deleting the folder removes it.
|
|
615
|
+
|
|
596
616
|
`uv tool uninstall claude-swap` (or `pipx uninstall claude-swap`) removes the
|
|
597
617
|
account switcher.
|
|
598
618
|
|
|
@@ -637,7 +657,7 @@ Nothing is ever installed unless you click, the argument vector is fixed in the
|
|
|
637
657
|
|
|
638
658
|
ccdeck runs as a two-process pair: a supervisor that owns nothing but the lifecycle, and the deck itself. When newer code is found, the deck exits with code 75 and the supervisor brings it back **on the port it actually bound**, which is not always the one it asked for. Both live in their own process group since 3.20, so stdout goes to `deck.log` rather than to the terminal you started from — `ccdeck --logs` reads it back. The supervisor also puts the deck back after a crash, five times in ten minutes with the wait doubling, and then stops and says why rather than spinning.
|
|
639
659
|
|
|
640
|
-
It restarts on its own only after 30 seconds with nothing running, because hook events are fire-and-forget and anything fired during the gap is lost. The toggle in the banner turns that off; the
|
|
660
|
+
It restarts on its own only after 30 seconds with nothing running, because hook events are fire-and-forget and anything fired during the gap is lost. The toggle in the banner, or Settings › General › Updates, turns that off; the deck keeps the choice in its `prefs.json`, so every tab and the next start agree. Under `--no-persist` a restart is refused outright — with no event log there is nothing to replay, and the canvas would be gone.
|
|
641
661
|
|
|
642
662
|
Step by step: [Update ccdeck to the latest version](https://ccdeck.dev/guides/update-and-restart/)
|
|
643
663
|
|
|
@@ -670,9 +690,9 @@ so existing clones, links and bookmarks keep working.
|
|
|
670
690
|
|
|
671
691
|
**Does it work with Codex, or only Claude Code?**
|
|
672
692
|
Both, on one canvas. Claude Code arrives through a hook, Codex through its
|
|
673
|
-
rollout log, and the model chip tells them apart.
|
|
674
|
-
|
|
675
|
-
on you* queue, because a rollout log is a record of what happened and the
|
|
693
|
+
rollout log, and the model chip tells them apart. Codex threads with a recorded spawning parent appear as connected child
|
|
694
|
+
sessions, grouped together for layout, focus and dragging. Missing links are
|
|
695
|
+
left unconnected. The *blocked on you* queue remains Claude Code only, because a rollout log is a record of what happened and the
|
|
676
696
|
queue needs to know what is happening now. Codex does report it — its
|
|
677
697
|
`app-server` pushes a thread status carrying `waitingOnApproval` — and reading
|
|
678
698
|
that is open work, not a wall. Step by step:
|
package/bin/deck.js
CHANGED
|
@@ -634,6 +634,10 @@ async function shutdown(code = 0) {
|
|
|
634
634
|
const { drainAppends } = await import(pathToFileURL(join(PKG_ROOT, "src/server/log-writer.mjs")).href);
|
|
635
635
|
await drainAppends();
|
|
636
636
|
} catch { /* nothing queued, or the module never loaded */ }
|
|
637
|
+
try {
|
|
638
|
+
const { drainToolAudit } = await import(pathToFileURL(join(PKG_ROOT, "src/server/tool-audit.mjs")).href);
|
|
639
|
+
await drainToolAudit();
|
|
640
|
+
} catch { /* tool history was unavailable */ }
|
|
637
641
|
// No server yet means nothing to drain and nothing to hand the port over to,
|
|
638
642
|
// so the exit is the whole of the shutdown.
|
|
639
643
|
if (!server) return process.exit(code);
|
|
@@ -0,0 +1 @@
|
|
|
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"./app-9p127oSR.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};
|