ccdeck 3.41.0 → 3.41.3
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 +41 -24
- package/dist/web/assets/BrowserWatchModal-6TctCbc4.js +1 -0
- package/dist/web/assets/{RadioPanel-UcoMDWAH.js → RadioPanel-ZDDwNYnL.js} +1 -1
- package/dist/web/assets/TrafficRadar-lfG5ekUR.js +1 -0
- package/dist/web/assets/UsageHistoryModal-Be-9GVIE.js +1 -0
- package/dist/web/assets/index-BgSnAnP6.css +1 -0
- package/dist/web/assets/index-C80Hp_2s.js +307 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +50 -21
- package/src/server/claude-accounts.mjs +18 -10
- package/src/server/quota-shape.mjs +23 -20
- package/dist/web/assets/BrowserWatchModal-s7_Wr7WT.js +0 -1
- package/dist/web/assets/TrafficRadar-9Qlon_77.js +0 -1
- package/dist/web/assets/UsageHistoryModal-CGh4GESl.js +0 -1
- package/dist/web/assets/index-B9pQ4U7M.js +0 -305
- package/dist/web/assets/index-BhNayHbp.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
document.documentElement.setAttribute("data-color-scheme", lightThemes.indexOf(theme) >= 0 ? "light" : "dark");
|
|
67
67
|
})();
|
|
68
68
|
</script>
|
|
69
|
-
<script type="module" crossorigin src="/assets/index-
|
|
70
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
69
|
+
<script type="module" crossorigin src="/assets/index-C80Hp_2s.js"></script>
|
|
70
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BgSnAnP6.css">
|
|
71
71
|
</head>
|
|
72
72
|
<body>
|
|
73
73
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "3.41.
|
|
3
|
+
"version": "3.41.3",
|
|
4
4
|
"description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
|
@@ -75,7 +75,26 @@
|
|
|
75
75
|
"3.38.2",
|
|
76
76
|
"3.38.6",
|
|
77
77
|
"3.39.4",
|
|
78
|
-
"3.40.1",
|
|
78
|
+
"3.40.1",
|
|
79
|
+
"3.41.3"
|
|
80
|
+
],
|
|
81
|
+
"3.41.1": [
|
|
82
|
+
{
|
|
83
|
+
"title": "🪟 More reliable Windows updates",
|
|
84
|
+
"body": "Restart to update stops the app’s background server before installation begins."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"title": "📊 More accurate account capacity",
|
|
88
|
+
"body": "Elapsed resets and older readings no longer imply newly available quota. Account refreshes keep the latest valid readings."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"title": "🖥️ Better layouts on portrait displays",
|
|
92
|
+
"body": "Sessions fill the right columns, and the canvas starts near the top. Usage history controls stay reachable in narrow windows."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"title": "✨ Clearer load history and consistent icons",
|
|
96
|
+
"body": "Load history includes the current core-capacity multiplier below the charts. Refresh and external-link icons look consistent across system fonts."
|
|
97
|
+
}
|
|
79
98
|
],
|
|
80
99
|
"3.41.0": [
|
|
81
100
|
{
|
|
@@ -103,6 +122,12 @@
|
|
|
103
122
|
"body": "Change individual telemetry flags and use a simpler Monitor view."
|
|
104
123
|
}
|
|
105
124
|
],
|
|
125
|
+
"3.40.2": [
|
|
126
|
+
{
|
|
127
|
+
"title": "📻 Music is in Radio now, not in Settings",
|
|
128
|
+
"body": "Settings no longer has a Music section. Claude FM, Lofi Girl and the streams you added are under Workspace streams in Radio, on the right sidebar, and Radio’s player has the volume and mute. Your station, volume and streams are kept. Adding, renaming or removing a stream link is no longer offered."
|
|
129
|
+
}
|
|
130
|
+
],
|
|
106
131
|
"3.40.0": [
|
|
107
132
|
{
|
|
108
133
|
"title": "📻 Explore radio around the world",
|
|
@@ -110,7 +135,7 @@
|
|
|
110
135
|
},
|
|
111
136
|
{
|
|
112
137
|
"title": "🦊 Choose your canvas companion",
|
|
113
|
-
"body": "Choose Deck companion, Fox or Penguin in Settings ›
|
|
138
|
+
"body": "Choose Deck companion, Fox or Penguin in Settings › General. Your companion shares the existing canvas movements and music controls, and your choice stays saved."
|
|
114
139
|
},
|
|
115
140
|
{
|
|
116
141
|
"title": "📊 See what each session used",
|
|
@@ -129,12 +154,16 @@
|
|
|
129
154
|
{
|
|
130
155
|
"title": "Clearer account usage and Telemetry Radar",
|
|
131
156
|
"body": "Account usage shows reset times together with capacity. Telemetry Radar has a clearer monitoring workspace and keeps history for up to 24 hours. Radar remains Beta."
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"title": "🔎 Telemetry Radar on Linux and Windows, with no Terminal step",
|
|
160
|
+
"body": "Radar now works on macOS, Linux and Windows. Press Start monitoring and ccdeck runs the capture itself, with no command to paste into Terminal, and keeps listening until you stop it. Capture needs a one-time setup for your user: Wireshark’s ChmodBPF helper on macOS, tcpdump with capture permission on Linux, or Wireshark with Npcap on Windows. Radar says what is missing, and once permission is granted it starts listening by itself."
|
|
132
161
|
}
|
|
133
162
|
],
|
|
134
163
|
"3.39.1": [
|
|
135
164
|
{
|
|
136
165
|
"title": "🎨 More themes, with clearer contrast",
|
|
137
|
-
"body": "Choose Omarchy, Matrix, Black Contrast, White Contrast or Catppuccin Mocha in Settings ›
|
|
166
|
+
"body": "Choose Omarchy, Matrix, Black Contrast, White Contrast or Catppuccin Mocha in Settings › General. Fresh installations start with Rider Black; your saved theme choice is preserved."
|
|
138
167
|
},
|
|
139
168
|
{
|
|
140
169
|
"title": "A little more room on the canvas",
|
|
@@ -144,7 +173,7 @@
|
|
|
144
173
|
"3.39.0": [
|
|
145
174
|
{
|
|
146
175
|
"title": "🔎 Inspect Claude Code telemetry on macOS",
|
|
147
|
-
"body": "Telemetry Radar shows Claude Code's configured telemetry and can inspect traffic at destinations you choose. When settings are absent, it can show observed connections separately; a connection alone does not prove telemetry or reveal message contents.
|
|
176
|
+
"body": "Telemetry Radar shows Claude Code's configured telemetry and can inspect traffic at destinations you choose. When settings are absent, it can show observed connections separately; a connection alone does not prove telemetry or reveal message contents. Captured telemetry can contain prompt or response content, and other apps sending to the same destination may appear too. Captured messages stay in ccdeck's memory, and no capture file is written."
|
|
148
177
|
},
|
|
149
178
|
{
|
|
150
179
|
"title": "🧭 Open panels from the edges",
|
|
@@ -156,13 +185,13 @@
|
|
|
156
185
|
},
|
|
157
186
|
{
|
|
158
187
|
"title": "🎨 Two new dark themes",
|
|
159
|
-
"body": "Choose Rider Black or VS Code Black in Settings ›
|
|
188
|
+
"body": "Choose Rider Black or VS Code Black in Settings › General. Both add a darker canvas with flatter panels and distinct focus accents; your existing Light or Dark choice stays as it was."
|
|
160
189
|
}
|
|
161
190
|
],
|
|
162
191
|
"3.38.5": [
|
|
163
192
|
{
|
|
164
193
|
"title": "🔧 Settings are in one place now",
|
|
165
|
-
"body": "The theme, the sounds, notifications
|
|
194
|
+
"body": "The theme, the sounds, notifications and the minimap character are all in Settings, behind the gear at the right of the topbar, or Cmd+, (Ctrl+, on Windows and Linux). V opens it at Sounds, with each tone's volume and sound, and M turns sounds off or on from anywhere. Nothing you had set has changed, it has only moved, and T still switches the theme. The auto-switch threshold also takes a custom value now: pick Custom… and type a whole percent from 50 to 99."
|
|
166
195
|
},
|
|
167
196
|
{
|
|
168
197
|
"title": "Windows: no more temperature stuck at 28 °C",
|
|
@@ -172,7 +201,7 @@
|
|
|
172
201
|
"3.38.4": [
|
|
173
202
|
{
|
|
174
203
|
"title": "🔔 A notification when Auto-switch changes your account",
|
|
175
|
-
"body": "When Auto-switch moves Claude Code to another account, ccdeck now tells you once, with where it went and why, for example \"Switched to work · personal@example.com reached 90%\". You can turn it off in
|
|
204
|
+
"body": "When Auto-switch moves Claude Code to another account, ccdeck now tells you once, with where it went and why, for example \"Switched to work · personal@example.com reached 90%\". You can turn it off in Settings › Notifications, where two more wait, off until you turn them on: one when a quota window of the Claude or Codex account you are on reaches 90% and again at 100%, and one when a window that had reached 90% is available again. Each is said once per window and a restart does not repeat it. With either quota notification on, the deck reads quota about every two minutes even while no page is open."
|
|
176
205
|
}
|
|
177
206
|
],
|
|
178
207
|
"3.38.3": [
|
|
@@ -210,7 +239,7 @@
|
|
|
210
239
|
},
|
|
211
240
|
{
|
|
212
241
|
"title": "💬 Send feedback from inside the deck",
|
|
213
|
-
"body": "
|
|
242
|
+
"body": "Feedback, beside Settings at the right of the topbar (under More on a phone), opens a short form: pick what kind it is, write a few lines, and add a way to reach you under Add details if you want an answer. It goes to the people who make ccdeck, who may open a public issue from it, never with your contact in it. If it cannot be sent, the form stays filled in and says why."
|
|
214
243
|
}
|
|
215
244
|
],
|
|
216
245
|
"3.33.0": [
|
|
@@ -364,7 +393,7 @@
|
|
|
364
393
|
},
|
|
365
394
|
{
|
|
366
395
|
"title": "📅 Projects opens on Today",
|
|
367
|
-
"body": "The Projects report per account starts with today's work, and an empty window offers the next wider one. The
|
|
396
|
+
"body": "The Projects report per account starts with today's work, and an empty window offers the next wider one. The desktop app can also restart into an update from its own window."
|
|
368
397
|
}
|
|
369
398
|
],
|
|
370
399
|
"3.27.0": [
|
|
@@ -422,7 +451,7 @@
|
|
|
422
451
|
},
|
|
423
452
|
{
|
|
424
453
|
"title": "🎨 See each theme before you pick it",
|
|
425
|
-
"body": "
|
|
454
|
+
"body": "Settings › General shows a small preview of the deck in each theme. Claude FM also keeps its volume when a notification chime plays, instead of dipping under it."
|
|
426
455
|
}
|
|
427
456
|
],
|
|
428
457
|
"3.24.1": [
|
|
@@ -580,11 +609,11 @@
|
|
|
580
609
|
},
|
|
581
610
|
{
|
|
582
611
|
"title": "⎋ Esc leaves the machine panel where it is",
|
|
583
|
-
"body": "The × on **This machine** read `Close (Esc)`, so Escape put the panel away. Escape belongs to the canvas — it deselects and hands the keyboard back — and pressing it while you are reading your instruments should not close them.\n\nThe panel now closes the
|
|
612
|
+
"body": "The × on **This machine** read `Close (Esc)`, so Escape put the panel away. Escape belongs to the canvas — it deselects and hands the keyboard back — and pressing it while you are reading your instruments should not close them.\n\nThe panel now closes the ways it opens: its ×, its button on the window's right edge, and S. The × says `Close`, and names no key it does not answer."
|
|
584
613
|
},
|
|
585
614
|
{
|
|
586
615
|
"title": "🖥 The machine moved out of the topbar and into a button",
|
|
587
|
-
"body": "The small CPU sparkline and memory bar in the topbar are gone. A chip glyph
|
|
616
|
+
"body": "The small CPU sparkline and memory bar in the topbar are gone. A chip glyph, on the window's right edge now, opens the same **This machine** panel — cores, memory, swap, load, temperature, busiest processes — and the bar stops moving in the corner of your eye.\n\nIt also stops asking. The deck read your machine every three seconds for the life of every tab to feed that meter; it now reads only while the panel is open, and the history behind the readings is kept by the deck itself, so a panel opened an hour from now still has the hour behind it."
|
|
588
617
|
},
|
|
589
618
|
{
|
|
590
619
|
"title": "🔎 The panel's eight process rows became one way in",
|
|
@@ -642,7 +671,7 @@
|
|
|
642
671
|
"3.9.0": [
|
|
643
672
|
{
|
|
644
673
|
"title": "🎛️ Five switches that were not where the thing they control is",
|
|
645
|
-
"body": "**The session list has a button again.** It lost one a while back and only the `L` key was left — and `L` is written down in one place, a shortcuts sheet behind a small `?` in the corner of the canvas. If you use a mouse there was no way to open the list at all.
|
|
674
|
+
"body": "**The session list has a button again.** It lost one a while back and only the `L` key was left — and `L` is written down in one place, a shortcuts sheet behind a small `?` in the corner of the canvas. If you use a mouse there was no way to open the list at all. Its button is on the window's left edge now.\n\n**The Notifications switch now tells you whether the browser will actually show one.** It said `on` while this browser had never been asked for permission, which is not the same thing. It now says so on that row and offers the ask right there. Before, the only place to grant it was a button that appears solely while a session is stuck waiting — so on a machine where that rarely happens you could not switch notifications on at all.\n\n**Started with `--workspace` or `--scope` and seeing nothing?** The empty screen used to tell everyone to \"run `claude` or `codex` in any folder\", which is the one thing that cannot work for you. It now names the folder the deck is actually watching.\n\n**Browser Watch says when a setting did not save.** Flipping the switch and having it fail showed a red line reading \"Could not read\" — about reading history, not about your press — and ten seconds later the panel wiped it and looked perfectly healthy with the setting unchanged. A failed save now says so, in its own words, and stays until you dismiss it. The \"nobody browsing for\" menu also follows the stored value again: change it in a second tab and this one never caught up.\n\n**The deck no longer restarts itself when nothing on screen offers to stop it.** Thirty seconds after the last agent went quiet, with a new version on disk, the server would exit, come back and reload the page. The `auto when idle` switch that controls that lives inside the update banner — so if you had closed the banner with its `×`, it happened with no visible setting anywhere. Closing the banner now also stands down the restart; clicking the version chip brings back both together."
|
|
646
675
|
}
|
|
647
676
|
],
|
|
648
677
|
"3.8.9": [
|
|
@@ -696,25 +725,25 @@
|
|
|
696
725
|
"3.8.1": [
|
|
697
726
|
{
|
|
698
727
|
"title": "🔒 Security: update if the deck runs on a Mac",
|
|
699
|
-
"body": "A local process able to reach the deck's port — no token, no browser, no tab — could get a string of its choosing compiled as AppleScript when the deck raised a desktop notification with no page open. On macOS that is code running as you.\n\nThe strings were already passed as arguments rather than pasted into the script, which is normally the whole defence. It is not enough for `osascript`: its option parser reads any argument starting with a dash as an option, so a name beginning with `-e` was taken as more script instead of as text. Every place the deck hands `osascript` or `notify-send` something from outside now carries an explicit end-of-options marker, so nothing from outside can be read as an instruction again.\n\nNothing to change on your side beyond updating. If you would rather keep the deck off the desktop entirely, the Notifications switch in
|
|
728
|
+
"body": "A local process able to reach the deck's port — no token, no browser, no tab — could get a string of its choosing compiled as AppleScript when the deck raised a desktop notification with no page open. On macOS that is code running as you.\n\nThe strings were already passed as arguments rather than pasted into the script, which is normally the whole defence. It is not enough for `osascript`: its option parser reads any argument starting with a dash as an option, so a name beginning with `-e` was taken as more script instead of as text. Every place the deck hands `osascript` or `notify-send` something from outside now carries an explicit end-of-options marker, so nothing from outside can be read as an instruction again.\n\nNothing to change on your side beyond updating. If you would rather keep the deck off the desktop entirely, the Notifications switch in Settings › Notifications does that, and `AGENTS_DECK_NO_NOTIFY=1` does it at launch — but the fix is the thing to have."
|
|
700
729
|
}
|
|
701
730
|
],
|
|
702
731
|
"3.8.0": [
|
|
703
732
|
{
|
|
704
733
|
"title": "🔕 Notifications now have an off switch",
|
|
705
|
-
"body": "3.7.0 gave you a \"notify me\" button and no way back. Turning them off meant digging into your browser's site settings — which no page can do for you — and that still left the other notifier, the one the deck raises when no page is open at all. Silencing that one meant quitting the deck and starting it again with `AGENTS_DECK_NO_NOTIFY=1` in front. The person who wanted quiet was the one doing the work.\n\nThere is a **Notifications** switch in
|
|
734
|
+
"body": "3.7.0 gave you a \"notify me\" button and no way back. Turning them off meant digging into your browser's site settings — which no page can do for you — and that still left the other notifier, the one the deck raises when no page is open at all. Silencing that one meant quitting the deck and starting it again with `AGENTS_DECK_NO_NOTIFY=1` in front. The person who wanted quiet was the one doing the work.\n\nThere is a **Notifications** switch in Settings › Notifications now. One switch governs both notifiers, it takes effect immediately without a restart, and it is remembered across restarts.\n\nIt is kept by the deck rather than by your browser, because the second notifier runs when no page exists and a setting living in a page could never reach it. `AGENTS_DECK_NO_NOTIFY=1` still wins over the switch — a machine told at launch to stay off the desktop stays off it — and when that is the case Settings says so instead of showing an \"on\" that will not happen."
|
|
706
735
|
}
|
|
707
736
|
],
|
|
708
737
|
"3.7.1": [
|
|
709
738
|
{
|
|
710
739
|
"title": "🔔 The notifications now fire on the thing that actually stops your agent",
|
|
711
|
-
"body": "3.7.0 could tell you a session was waiting — but only when Claude Code asked permission to run a tool. If you work with permissions bypassed, that never happens, and the whole feature was silent for you. Measured on one real log: 1683 events, every one of them in bypassPermissions, and a single permission prompt in the entire history.\n\nWhat fires constantly on that setup is the other kind: the agent asking you a question and stopping for the answer. The deck was throwing those away — no amber chip, no row in the sidebar, no notification, and no \"notify me\" button, because the button only
|
|
740
|
+
"body": "3.7.0 could tell you a session was waiting — but only when Claude Code asked permission to run a tool. If you work with permissions bypassed, that never happens, and the whole feature was silent for you. Measured on one real log: 1683 events, every one of them in bypassPermissions, and a single permission prompt in the entire history.\n\nWhat fires constantly on that setup is the other kind: the agent asking you a question and stopping for the answer. The deck was throwing those away — no amber chip, no row in the sidebar, no notification, and no \"notify me\" button, because the button only appeared while something was actually blocked.\n\nNow they count. The card carries the question itself rather than a generic \"Your turn\" — \"paycore needs your input: merge both branches to main, or just one?\" — and so does the notification, on the desktop, whether the tab is behind something or not open at all.\n\nAn idle prompt still stays quiet: a turn that ended is not a session that is stuck, and a channel that cries wolf is one you switch off."
|
|
712
741
|
}
|
|
713
742
|
],
|
|
714
743
|
"3.7.0": [
|
|
715
744
|
{
|
|
716
745
|
"title": "🔔 The deck can reach you when you are not looking at it",
|
|
717
|
-
"body": "The deck's whole point is telling you which agent is waiting on you, and until now it could only say so on screen — the amber chip, the tab title, the favicon. Every one of those is drawn inside a page, so the one moment it matters, when you have walked away and four agents have gone quiet, the deck was silent.\n\nNow it reaches the desktop, two ways that never overlap. With the deck open behind something, the page raises the notification itself:
|
|
746
|
+
"body": "The deck's whole point is telling you which agent is waiting on you, and until now it could only say so on screen — the amber chip, the tab title, the favicon. Every one of those is drawn inside a page, so the one moment it matters, when you have walked away and four agents have gone quiet, the deck was silent.\n\nNow it reaches the desktop, two ways that never overlap. With the deck open behind something, the page raises the notification itself: the browser asks for permission when you turn notifications on in Settings › Notifications — nothing is requested before you do. With no page open at all, the server raises it, because it can see that nobody is connected and therefore that no other surface is saying anything.\n\nOne notification per session, replaced rather than stacked if a second deck raises it too. Clicking it brings the tab forward. `AGENTS_DECK_NO_NOTIFY=1` turns the desktop side off entirely, and if you refuse the browser prompt the deck says so plainly instead of leaving you thinking you switched something on — after a refusal only your browser's site settings can undo it, which no page is allowed to touch."
|
|
718
747
|
},
|
|
719
748
|
{
|
|
720
749
|
"title": "🧩 A blocked session now says what it is asking for",
|
|
@@ -744,7 +773,7 @@
|
|
|
744
773
|
},
|
|
745
774
|
{
|
|
746
775
|
"title": "🧹 The board's own figure is out of the Usage panel",
|
|
747
|
-
"body": "3.3.0 put the canvas's live spend on its own labelled line, under whichever period you had chosen. In front of a panel that answers today, this month and all time from the logs on disk, that second money figure answers a question nobody asked at that moment — and it invited a comparison it could never win, $7,385 on the board beneath $170 for today.\n\nIt is gone. The
|
|
776
|
+
"body": "3.3.0 put the canvas's live spend on its own labelled line, under whichever period you had chosen. In front of a panel that answers today, this month and all time from the logs on disk, that second money figure answers a question nobody asked at that moment — and it invited a comparison it could never win, $7,385 on the board beneath $170 for today.\n\nIt is gone. The panel still shows the board's tokens and cost when ccusage cannot answer at all."
|
|
748
777
|
}
|
|
749
778
|
],
|
|
750
779
|
"3.3.0": [
|
|
@@ -758,7 +787,7 @@
|
|
|
758
787
|
},
|
|
759
788
|
{
|
|
760
789
|
"title": "👁 Browser Watch reads nothing while it is switched off",
|
|
761
|
-
"body": "The switch used to control only what was KEPT. The read happened anyway — every five minutes, on every deck, the panel's badge poll discovered each Chromium profile and copied its whole History database to a temp file to query it. A switch that says off while that continues is not a switch.\n\nWith the watch off, nothing touches a browser now. Opening the panel still reads live, because that is you looking, and a watch that is ON still records in the background — that is the feature. The trade is that the
|
|
790
|
+
"body": "The switch used to control only what was KEPT. The read happened anyway — every five minutes, on every deck, the panel's badge poll discovered each Chromium profile and copied its whole History database to a temp file to query it. A switch that says off while that continues is not a switch.\n\nWith the watch off, nothing touches a browser now. Opening the panel still reads live, because that is you looking, and a watch that is ON still records in the background — that is the feature. The trade is that the Browser Watch badge no longer counts findings while the watch is off; open the panel to see them.\n\nThe README has a Browser Watch section now: what is read, when, where the copy goes, and that only visits after the deck started are ever considered."
|
|
762
791
|
},
|
|
763
792
|
{
|
|
764
793
|
"title": "🔒 The deck's own data is no longer readable by anything on the machine",
|
|
@@ -828,7 +857,7 @@
|
|
|
828
857
|
"1.48.0": [
|
|
829
858
|
{
|
|
830
859
|
"title": "👁 Browser Watch — see what a program opened while you were away",
|
|
831
|
-
"body": "
|
|
860
|
+
"body": "Browser Watch is new, on the window's right edge (or press B). It watches the browsers on this machine and tells you when a program — an extension, an agent, a command — opened pages while nobody was touching the browser.\n\nUsually that is your own agent doing what you asked, and the panel says so rather than crying intrusion. What it gives you is the evidence: which pages, in which browser, at what time, with every address written out in full.\n\nIt is off until you switch it on, and nothing leaves this machine. It reads a copy of the browser's own history and only pages newer than the moment the deck started — never anything from before, never cookies, never saved passwords. Open ⚙ inside the panel and \"What Browser Watch can access\" says exactly what it reads, keeps and sends.\n\nWhen it finds something, its button shows a count. You can dismiss a finding once you have looked at it and it stays gone, while the log file on disk keeps the record. On a Mac it can also close the tab it found, or quit the browser."
|
|
832
861
|
},
|
|
833
862
|
{
|
|
834
863
|
"title": "🔑 Move all your accounts to another machine in one paste",
|
|
@@ -848,7 +877,7 @@
|
|
|
848
877
|
"1.46.0": [
|
|
849
878
|
{
|
|
850
879
|
"title": "🔊 Pick a volume and a sound for each of the two tones",
|
|
851
|
-
"body": "
|
|
880
|
+
"body": "Open Settings › Sounds, or press V. There are two tones — one when a turn finishes, one when Claude is waiting for you — and each has its own volume and its own sound, six to choose from. Press \"Hear it\" to try one straight away, without waiting for a real turn.\n\nThe two lists are separate on purpose, so you can never end up with both tones sounding the same. A finish tone always falls; an asking tone always rises. That is how you tell them apart without looking at the screen.\n\nM still turns the sound on and off from anywhere."
|
|
852
881
|
},
|
|
853
882
|
{
|
|
854
883
|
"title": "✨ This box is new",
|
|
@@ -253,7 +253,7 @@ async function readUsageRows(root) {
|
|
|
253
253
|
* trust a row the other two refuse. Exported for its test.
|
|
254
254
|
*/
|
|
255
255
|
export function usageRows(usage) {
|
|
256
|
-
return usage?.schemaVersion === 2
|
|
256
|
+
return usage?.schemaVersion === 2 && usage.accounts && typeof usage.accounts === "object" && !Array.isArray(usage.accounts) ? usage.accounts : {};
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/**
|
|
@@ -267,14 +267,14 @@ export function usageRows(usage) {
|
|
|
267
267
|
* cannot disagree about whose row it is. Exported for its test.
|
|
268
268
|
*/
|
|
269
269
|
export function rowIsFor(row, acct) {
|
|
270
|
-
return Boolean(row)
|
|
270
|
+
return Boolean(row) && typeof acct?.email === "string" && acct.email.trim().length > 0
|
|
271
271
|
&& row.email === acct.email
|
|
272
272
|
&& (row.organizationUuid ?? "") === (acct.organizationUuid ?? "");
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
function pctOf(win) {
|
|
276
276
|
const p = win?.pct;
|
|
277
|
-
return typeof p === "number" && Number.isFinite(p) ? p : null;
|
|
277
|
+
return typeof p === "number" && Number.isFinite(p) ? Math.min(100, Math.max(0, p)) : null;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/** One lane, in the shape the panel's bars already speak. */
|
|
@@ -288,7 +288,7 @@ function lane(id, label, win) {
|
|
|
288
288
|
pct,
|
|
289
289
|
// claude-swap stores a countdown string too, but it was computed when the
|
|
290
290
|
// row was written and drifts — the client recomputes from the timestamp.
|
|
291
|
-
resetAt:
|
|
291
|
+
resetAt: !Number.isFinite(resetAt) || resetAt <= 0 ? null : Math.floor(resetAt / 1000),
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
294
|
|
|
@@ -480,9 +480,12 @@ async function readRoster(now, gen) {
|
|
|
480
480
|
? await currentIdentity().catch(() => null)
|
|
481
481
|
: null;
|
|
482
482
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
483
|
+
// Sequence is an ordering hint, not a second roster. Preserve all slots
|
|
484
|
+
// exactly once when an older or partially written order omits one.
|
|
485
|
+
const order = [...new Set([
|
|
486
|
+
...(Array.isArray(seq.sequence) ? seq.sequence.map(String) : []),
|
|
487
|
+
...Object.keys(seq.accounts).sort((a, b) => Number(a) - Number(b)),
|
|
488
|
+
])].filter(num => Object.hasOwn(seq.accounts, num));
|
|
486
489
|
|
|
487
490
|
const accounts = [];
|
|
488
491
|
const recovered = [];
|
|
@@ -515,14 +518,19 @@ async function readRoster(now, gen) {
|
|
|
515
518
|
* verdict cache, and asks the registered repair about a `stale-copy` row —
|
|
516
519
|
* which is what starts that repair (see repairStaleCopyWith).
|
|
517
520
|
*/
|
|
521
|
+
function observedMs(seconds) {
|
|
522
|
+
const ms = typeof seconds === "number" ? seconds * 1000 : NaN;
|
|
523
|
+
return Number.isFinite(ms) && ms > 0 && Number.isFinite(new Date(ms).getTime()) ? ms : null;
|
|
524
|
+
}
|
|
525
|
+
|
|
518
526
|
function rosterRow({ seq, num, acct, row, identity, now, key, origin = null }) {
|
|
519
527
|
const matches = rowIsFor(row, acct);
|
|
520
528
|
const good = matches ? row.lastGood : null;
|
|
521
529
|
|
|
522
|
-
const fetchedAtMs = matches
|
|
530
|
+
const fetchedAtMs = matches ? observedMs(row.fetchedAt) : null;
|
|
523
531
|
// When claude-swap last TRIED, well or not. A refusal counts against a login
|
|
524
532
|
// only when it came after the deck last signed that login in (#1893).
|
|
525
|
-
const attemptedAtMs = matches
|
|
533
|
+
const attemptedAtMs = matches ? observedMs(row.lastAttemptAt) : null;
|
|
526
534
|
const isActive = String(seq.activeAccountNumber) === num;
|
|
527
535
|
const trouble = authTrouble(row, {
|
|
528
536
|
matches, isActive, identity, email: acct.email, fetchedAt: fetchedAtMs, now,
|
|
@@ -703,7 +711,7 @@ export async function activeAccountUsage() {
|
|
|
703
711
|
const row = (await readUsageRows(root))[num];
|
|
704
712
|
// Same identity guard the panel uses: rows are keyed by slot, and slots are
|
|
705
713
|
// reused, so a row can outlive the account it was written for.
|
|
706
|
-
if (!row?.lastGood || !rowIsFor(row, acct) ||
|
|
714
|
+
if (!row?.lastGood || !rowIsFor(row, acct) || observedMs(row.fetchedAt) == null) return null;
|
|
707
715
|
|
|
708
716
|
return {
|
|
709
717
|
num: Number(num),
|
|
@@ -35,7 +35,8 @@ export const WIN_7D_SEC = 604800;
|
|
|
35
35
|
* Exported for its test.
|
|
36
36
|
*/
|
|
37
37
|
export function clampPct(v) {
|
|
38
|
-
return
|
|
38
|
+
return typeof v === "number" && Number.isFinite(v)
|
|
39
|
+
? Math.min(100, Math.max(0, Math.round(v))) : null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// ISO-8601 → "Jun 19, 1:19pm" (local time, matching the CLI display format).
|
|
@@ -48,13 +49,13 @@ export function clampPct(v) {
|
|
|
48
49
|
const fmtResetIso = resetLabelIso;
|
|
49
50
|
|
|
50
51
|
function isoToSec(iso) {
|
|
51
|
-
if (!iso) return null;
|
|
52
|
+
if (typeof iso !== "string" || !iso) return null;
|
|
52
53
|
const t = new Date(iso).getTime();
|
|
53
|
-
return
|
|
54
|
+
return !Number.isFinite(t) || t <= 0 ? null : Math.floor(t / 1000);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// Map the OAuth usage JSON to our quota result shape.
|
|
57
|
-
// utilization is already a 0–100 percentage.
|
|
58
|
+
// utilization is already a 0–100 percentage. Each window stays independent.
|
|
58
59
|
//
|
|
59
60
|
// A 7d window that is absent is UNKNOWN, and published as null (#1627). The
|
|
60
61
|
// endpoint can send `seven_day: null`, and this used to fill in a zero there —
|
|
@@ -67,25 +68,26 @@ export function mapOAuthUsage(data) {
|
|
|
67
68
|
const son = data?.seven_day_sonnet;
|
|
68
69
|
const opus = data?.seven_day_opus;
|
|
69
70
|
|
|
70
|
-
const
|
|
71
|
-
|
|
71
|
+
const fivePct = clampPct(fh?.utilization);
|
|
72
|
+
const sevenPct = clampPct(sd?.utilization);
|
|
73
|
+
if (fivePct == null && sevenPct == null) return null;
|
|
72
74
|
|
|
73
75
|
const result = {
|
|
74
|
-
session5hPct:
|
|
76
|
+
session5hPct: fivePct,
|
|
75
77
|
session5hWindowSec: WIN_5H_SEC,
|
|
76
|
-
session5hReset: fmtResetIso(
|
|
77
|
-
session5hResetAt: isoToSec(
|
|
78
|
+
session5hReset: fmtResetIso(fivePct == null ? null : fh?.resets_at),
|
|
79
|
+
session5hResetAt: isoToSec(fivePct == null ? null : fh?.resets_at),
|
|
78
80
|
week7dWindowSec: WIN_7D_SEC,
|
|
79
81
|
};
|
|
80
|
-
if (
|
|
81
|
-
result.week7dPct =
|
|
82
|
+
if (sevenPct != null) {
|
|
83
|
+
result.week7dPct = sevenPct;
|
|
82
84
|
result.week7dReset = fmtResetIso(sd.resets_at);
|
|
83
85
|
result.week7dResetAt = isoToSec(sd.resets_at);
|
|
84
86
|
} else {
|
|
85
87
|
result.week7dPct = null;
|
|
86
88
|
}
|
|
87
|
-
if (son?.utilization != null) result.weekSonnetPct = clampPct(son.utilization);
|
|
88
|
-
if (opus?.utilization != null) result.weekOpusPct = clampPct(opus.utilization);
|
|
89
|
+
if (clampPct(son?.utilization) != null) result.weekSonnetPct = clampPct(son.utilization);
|
|
90
|
+
if (clampPct(opus?.utilization) != null) result.weekOpusPct = clampPct(opus.utilization);
|
|
89
91
|
|
|
90
92
|
// extra usage credits (pay-as-you-go top-up), if enabled
|
|
91
93
|
const extra = data?.extra_usage;
|
|
@@ -114,21 +116,22 @@ export function quotaFromStore(entry) {
|
|
|
114
116
|
const good = entry?.lastGood;
|
|
115
117
|
const fh = good?.five_hour;
|
|
116
118
|
const sd = good?.seven_day;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
+
const fivePct = clampPct(fh?.pct);
|
|
120
|
+
const sevenPct = clampPct(sd?.pct);
|
|
121
|
+
if (fivePct == null && sevenPct == null) return null;
|
|
119
122
|
|
|
120
123
|
const out = {
|
|
121
124
|
ok: true,
|
|
122
125
|
source: "claude-swap",
|
|
123
|
-
session5hPct:
|
|
126
|
+
session5hPct: fivePct,
|
|
124
127
|
session5hWindowSec: WIN_5H_SEC,
|
|
125
|
-
session5hReset: fmtResetIso(
|
|
126
|
-
session5hResetAt: isoToSec(
|
|
128
|
+
session5hReset: fmtResetIso(fivePct == null ? null : fh?.resets_at),
|
|
129
|
+
session5hResetAt: isoToSec(fivePct == null ? null : fh?.resets_at),
|
|
127
130
|
week7dWindowSec: WIN_7D_SEC,
|
|
128
131
|
// Unknown rather than 0 when the row has no 7d window, as mapOAuthUsage
|
|
129
132
|
// publishes it (#1627): claude-swap writes no `seven_day` at all when the
|
|
130
133
|
// endpoint sent none.
|
|
131
|
-
week7dPct:
|
|
134
|
+
week7dPct: sevenPct,
|
|
132
135
|
week7dReset: fmtResetIso(sd?.resets_at),
|
|
133
136
|
week7dResetAt: isoToSec(sd?.resets_at),
|
|
134
137
|
// The age of the DATA, not of our read of it. The panel prints this, and
|
|
@@ -139,7 +142,7 @@ export function quotaFromStore(entry) {
|
|
|
139
142
|
// claude-swap keeps per-model windows in a named list rather than fixed
|
|
140
143
|
// fields, because which ones an account has depends on its plan.
|
|
141
144
|
for (const s of Array.isArray(good.scoped) ? good.scoped : []) {
|
|
142
|
-
if (
|
|
145
|
+
if (clampPct(s?.pct) == null) continue;
|
|
143
146
|
if (/sonnet/i.test(s.name ?? "")) out.weekSonnetPct = clampPct(s.pct);
|
|
144
147
|
else if (/opus/i.test(s.name ?? "")) out.weekOpusPct = clampPct(s.pct);
|
|
145
148
|
}
|
|
@@ -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,h as re}from"./index-B9pQ4U7M.js";function oe({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 le(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 ce(s){return new Date(s).toLocaleDateString(void 0,{weekday:"short",day:"numeric",month:"short"})}function U(s){const a=Math.round((s.endMs-s.startMs)/6e4);return a<1?"":`${a} min`}function de({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=ce(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:[le(i),U(i)&&e.jsxs(e.Fragment,{children:[" · ",U(i)]}),i.provisional&&e.jsxs(e.Fragment,{children:[" · ",e.jsx("span",{title:"Its quiet window has not closed yet. If somebody used the browser in the minutes after it, it is withdrawn; until then it is not written down and no reaction runs.",children:"provisional"})]})]}),e.jsxs("span",{className:"bw-ep-count",children:[i.count," ",i.count===1?"page":"pages"]})]}),e.jsx("button",{className:"glyph-btn bw-ep-x",onClick:()=>n(i),"aria-label":`Dismiss ${i.host}`,title:"Dismiss — it leaves this list for good, and stays in the log file",children:"×"})]}),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 he(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 G=1e4,ue=12e3;function we({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",$=he(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%G/G: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),K=P?Math.max(0,1-J/.55):0;n.beginPath(),n.arc(F,L,3+K*1.6,0,Math.PI*2),n.fillStyle=f.running?S:v,n.globalAlpha=f.running?.55+K*.45:.35,n.fill(),n.globalAlpha=1});for(const f of j){const T=(A-f.atMs)/ue;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 me({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(we,{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 be({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 pe({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 ge({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 fe({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 xe(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 ye({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}=xe(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),...re(i),"aria-label":"Re-read every profile now",title:"Re-read every profile now",children:i?"…":"↻"}),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(me,{snap:t,watching:A,palette:c}),e.jsx(be,{watching:A,rest:R}),t.relay&&e.jsx(fe,{relay:t.relay})]}),e.jsxs("div",{className:"bw-main",children:[e.jsx(de,{snap:t,dismiss:j,refreshRef:N}),e.jsx(oe,{snap:t})]})]}),t&&S&&e.jsx(pe,{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(ge,{snap:t,saving:p,save:P,why:S,setWhy:g})]})]})})}export{ye as default};
|