claude-code-watch 0.0.5 → 0.0.7
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 +13 -11
- package/README.zh-CN.md +1 -1
- package/bin/claude-watch.js +108 -2
- package/package.json +5 -4
- package/public/index.html +132 -15
- package/public/vendor/github-dark.min.css +10 -0
- package/public/vendor/github-light.min.css +10 -0
- package/public/vendor/highlight.min.js +1213 -0
- package/public/vendor/marked.min.js +6 -0
- package/public/vendor/purify.min.js +3 -0
- package/src/parser/parser.js +34 -83
- package/src/server/server.js +99 -60
- package/src/watcher/watcher.js +286 -239
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Claude Code writes detailed JSONL logs under `~/.claude/projects/` as it works
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx claude-watch
|
|
21
|
+
npx claude-code-watch
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
This starts the dashboard at `http://localhost:23000` and opens it in your browser.
|
|
@@ -28,19 +28,21 @@ It will auto-discover active Claude Code sessions from `~/.claude/projects/` and
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
npm install -g claude-watch
|
|
31
|
+
npm install -g claude-code-watch
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Then run:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
claude-watch
|
|
37
|
+
claude-code-watch
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Usage
|
|
41
41
|
|
|
42
42
|
```
|
|
43
|
-
claude-watch [OPTIONS]
|
|
43
|
+
claude-code-watch [OPTIONS]
|
|
44
|
+
|
|
45
|
+
Shorter alias: `cc-watch` (equivalent to `claude-code-watch`).
|
|
44
46
|
|
|
45
47
|
OPTIONS:
|
|
46
48
|
-p, --port <port> HTTP port (default: 23000)
|
|
@@ -62,25 +64,25 @@ OPTIONS:
|
|
|
62
64
|
|
|
63
65
|
```bash
|
|
64
66
|
# List recent sessions
|
|
65
|
-
claude-watch -l
|
|
67
|
+
claude-code-watch -l
|
|
66
68
|
|
|
67
69
|
# List active sessions from last 10 minutes
|
|
68
|
-
claude-watch -a -w 10m
|
|
70
|
+
claude-code-watch -a -w 10m
|
|
69
71
|
|
|
70
72
|
# Watch a specific session
|
|
71
|
-
claude-watch -s abc123-def456
|
|
73
|
+
claude-code-watch -s abc123-def456
|
|
72
74
|
|
|
73
75
|
# Live-only mode (don't replay history)
|
|
74
|
-
claude-watch -n
|
|
76
|
+
claude-code-watch -n
|
|
75
77
|
|
|
76
78
|
# Custom port and host
|
|
77
|
-
claude-watch -p 8080 -h 0.0.0.0
|
|
79
|
+
claude-code-watch -p 8080 -h 0.0.0.0
|
|
78
80
|
|
|
79
81
|
# Limit tree to 5 most recent sessions, auto-collapse after 2m of inactivity
|
|
80
|
-
claude-watch -m 5 -c 2m
|
|
82
|
+
claude-code-watch -m 5 -c 2m
|
|
81
83
|
|
|
82
84
|
# Debug mode: see every unknown JSONL line type
|
|
83
|
-
claude-watch -D
|
|
85
|
+
claude-code-watch -D
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
## How It Works
|
package/README.zh-CN.md
CHANGED
package/bin/claude-watch.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const cp = require('child_process');
|
|
7
|
+
|
|
5
8
|
const { startServer } = require('../src/server/server');
|
|
6
9
|
const { listSessions, listActiveSessions } = require('../src/watcher/watcher');
|
|
7
10
|
|
|
8
|
-
const VERSION = '
|
|
11
|
+
const { version: VERSION } = require('../package.json');
|
|
9
12
|
|
|
10
13
|
function printHelp() {
|
|
11
14
|
console.log(`claude-watch v${VERSION}
|
|
@@ -15,6 +18,7 @@ to a web browser.
|
|
|
15
18
|
|
|
16
19
|
USAGE:
|
|
17
20
|
claude-watch [OPTIONS]
|
|
21
|
+
claude-watch update Check for updates and install latest
|
|
18
22
|
|
|
19
23
|
OPTIONS:
|
|
20
24
|
-p, --port <port> HTTP port (default: 23000)
|
|
@@ -28,6 +32,7 @@ OPTIONS:
|
|
|
28
32
|
-c <dur> Auto-collapse sessions inactive for this duration (e.g. 2m)
|
|
29
33
|
-D Debug: show raw type:subtype for every JSONL line we'd drop
|
|
30
34
|
--poll <ms> Polling interval in milliseconds (default: 500)
|
|
35
|
+
--no-open Do not auto-open browser on start
|
|
31
36
|
-v Show version
|
|
32
37
|
--help Show this help
|
|
33
38
|
|
|
@@ -36,6 +41,93 @@ ENVIRONMENT:
|
|
|
36
41
|
`);
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function compareVersions(a, b) {
|
|
45
|
+
const pa = a.split('.').map(Number);
|
|
46
|
+
const pb = b.split('.').map(Number);
|
|
47
|
+
for (let i = 0; i < 3; i++) {
|
|
48
|
+
if (pa[i] > pb[i]) return 1;
|
|
49
|
+
if (pa[i] < pb[i]) return -1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fetchLatestVersion() {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const opts = {
|
|
57
|
+
hostname: 'registry.npmjs.org',
|
|
58
|
+
path: '/claude-code-watch/latest',
|
|
59
|
+
timeout: 5000,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const req = https.get(opts, (res) => {
|
|
63
|
+
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
|
64
|
+
let data = '';
|
|
65
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
try {
|
|
68
|
+
const json = JSON.parse(data);
|
|
69
|
+
resolve(json.version);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
reject(err);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
req.on('error', reject);
|
|
77
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
78
|
+
req.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function checkForUpdate() {
|
|
83
|
+
fetchLatestVersion().then((latest) => {
|
|
84
|
+
if (compareVersions(latest, VERSION) > 0) {
|
|
85
|
+
console.log(`\n New version available: v${latest} (current: v${VERSION})`);
|
|
86
|
+
console.log(' Updating in background...\n');
|
|
87
|
+
const child = cp.spawn('npm', ['install', '-g', 'claude-code-watch@latest'], {
|
|
88
|
+
stdio: 'ignore',
|
|
89
|
+
detached: true,
|
|
90
|
+
});
|
|
91
|
+
child.unref();
|
|
92
|
+
child.on('exit', (code) => {
|
|
93
|
+
if (code === 0) {
|
|
94
|
+
console.log(` Updated to v${latest}. Changes take effect on next start.\n`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}).catch(() => { /* network unavailable, skip */ });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function runUpdate() {
|
|
102
|
+
console.log(` Current version: v${VERSION}`);
|
|
103
|
+
console.log(' Checking for latest version...\n');
|
|
104
|
+
|
|
105
|
+
let latest;
|
|
106
|
+
try {
|
|
107
|
+
latest = await fetchLatestVersion();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(` Failed to check for updates: ${err.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (compareVersions(latest, VERSION) <= 0) {
|
|
114
|
+
console.log(` Already up to date (v${VERSION}).`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(` Latest version: v${latest}`);
|
|
119
|
+
console.log(' Running npm install -g claude-code-watch@latest...\n');
|
|
120
|
+
|
|
121
|
+
const { execSync } = require('child_process');
|
|
122
|
+
try {
|
|
123
|
+
execSync('npm install -g claude-code-watch@latest', { stdio: 'inherit' });
|
|
124
|
+
console.log(`\n Updated to v${latest}. Restart to use the new version.`);
|
|
125
|
+
} catch {
|
|
126
|
+
console.error('\n Update failed. Try manually: npm install -g claude-code-watch@latest');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
39
131
|
function parseDuration(s) {
|
|
40
132
|
const match = s.match(/^(\d+)(ms|s|m|h)$/);
|
|
41
133
|
if (!match) throw new Error(`Invalid duration: ${s}`);
|
|
@@ -62,6 +154,7 @@ async function main() {
|
|
|
62
154
|
maxSessions: 0,
|
|
63
155
|
collapseAfter: 0,
|
|
64
156
|
debugAll: false,
|
|
157
|
+
openBrowser: true,
|
|
65
158
|
};
|
|
66
159
|
|
|
67
160
|
// First pass: collect all option values
|
|
@@ -74,7 +167,7 @@ async function main() {
|
|
|
74
167
|
options.skipHistory = true;
|
|
75
168
|
break;
|
|
76
169
|
case '-p':
|
|
77
|
-
case '--port':
|
|
170
|
+
case '--port': {
|
|
78
171
|
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
79
172
|
console.error(`Error: ${args[i]} requires a port number`);
|
|
80
173
|
process.exit(1);
|
|
@@ -86,6 +179,7 @@ async function main() {
|
|
|
86
179
|
}
|
|
87
180
|
options.port = pv;
|
|
88
181
|
break;
|
|
182
|
+
}
|
|
89
183
|
case '-h':
|
|
90
184
|
case '--host':
|
|
91
185
|
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
@@ -117,6 +211,9 @@ async function main() {
|
|
|
117
211
|
case '--poll':
|
|
118
212
|
options.pollMs = parseInt(args[++i], 10) || 500;
|
|
119
213
|
break;
|
|
214
|
+
case '--no-open':
|
|
215
|
+
options.openBrowser = false;
|
|
216
|
+
break;
|
|
120
217
|
default:
|
|
121
218
|
break;
|
|
122
219
|
}
|
|
@@ -169,6 +266,14 @@ async function main() {
|
|
|
169
266
|
case '--help':
|
|
170
267
|
printHelp();
|
|
171
268
|
return;
|
|
269
|
+
case 'update':
|
|
270
|
+
await runUpdate();
|
|
271
|
+
return;
|
|
272
|
+
// Skip option flags already handled in first pass
|
|
273
|
+
case '-s': case '-n': case '-p': case '--port':
|
|
274
|
+
case '-h': case '--host': case '-w': case '-c':
|
|
275
|
+
case '-m': case '-D': case '--poll': case '--no-open':
|
|
276
|
+
break;
|
|
172
277
|
default:
|
|
173
278
|
if (args[i].startsWith('-')) {
|
|
174
279
|
console.error(`Unknown option: ${args[i]}`);
|
|
@@ -178,6 +283,7 @@ async function main() {
|
|
|
178
283
|
}
|
|
179
284
|
}
|
|
180
285
|
|
|
286
|
+
checkForUpdate();
|
|
181
287
|
startServer(options);
|
|
182
288
|
}
|
|
183
289
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-watch",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Web-based real-time monitor for Claude Code.",
|
|
5
5
|
"main": "./src/server/server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"claude-code-watch": "
|
|
7
|
+
"claude-code-watch": "bin/claude-watch.js",
|
|
8
|
+
"cc-watch": "bin/claude-watch.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"start": "node bin/claude-watch.js",
|
|
11
|
-
"dev": "node --watch bin/claude-watch.js",
|
|
12
|
+
"dev": "node --watch bin/claude-watch.js --no-open",
|
|
12
13
|
"test": "node --test tests/all.test.js tests/watcher.test.js tests/server.test.js"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"license": "MIT",
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/shuxuecode/claude-watch"
|
|
30
|
+
"url": "git+https://github.com/shuxuecode/claude-watch.git"
|
|
30
31
|
},
|
|
31
32
|
"engines": {
|
|
32
33
|
"node": ">=18.0.0"
|
package/public/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>claude-watch</title>
|
|
7
|
-
<link rel="stylesheet" href="
|
|
7
|
+
<link rel="stylesheet" href="vendor/github-dark.min.css">
|
|
8
8
|
<style>
|
|
9
9
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
10
|
|
|
@@ -30,6 +30,28 @@
|
|
|
30
30
|
--orange: #fb923c;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
:root[data-theme="light"] {
|
|
34
|
+
--bg: #f8f9fa;
|
|
35
|
+
--bg2: #e9ecef;
|
|
36
|
+
--bg3: #ced4da;
|
|
37
|
+
--border: #adb5bd;
|
|
38
|
+
--text: #495057;
|
|
39
|
+
--dim: #868e96;
|
|
40
|
+
--white: #212529;
|
|
41
|
+
--purple: #6741d9;
|
|
42
|
+
--purple2: #5b21b6;
|
|
43
|
+
--blue: #2563eb;
|
|
44
|
+
--magenta: #9333ea;
|
|
45
|
+
--yellow: #d97706;
|
|
46
|
+
--yellow2: #92400e;
|
|
47
|
+
--green: #059669;
|
|
48
|
+
--cyan: #0891b2;
|
|
49
|
+
--red: #dc2626;
|
|
50
|
+
--red2: #b91c1c;
|
|
51
|
+
--gray: #6b7280;
|
|
52
|
+
--orange: #ea580c;
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
body {
|
|
34
56
|
background: var(--bg);
|
|
35
57
|
color: var(--text);
|
|
@@ -218,6 +240,21 @@ body {
|
|
|
218
240
|
|
|
219
241
|
/* Override highlight.js background to match our theme */
|
|
220
242
|
.hljs { background: #0d1117 !important; }
|
|
243
|
+
|
|
244
|
+
/* Light theme overrides */
|
|
245
|
+
:root[data-theme="light"] .btn.on { background: var(--purple); border-color: var(--purple); color: #fff; }
|
|
246
|
+
:root[data-theme="light"] .btn.on:hover { background: var(--purple2); color: #fff; }
|
|
247
|
+
:root[data-theme="light"] .btn.on:hover::after { background: var(--purple2); color: #fff; }
|
|
248
|
+
:root[data-theme="light"] .hljs { background: #f0f0f0 !important; }
|
|
249
|
+
:root[data-theme="light"] .tree-node:hover { background: rgba(0,0,0,0.06); }
|
|
250
|
+
:root[data-theme="light"] .tree-node.selected { background: rgba(124,58,237,0.2); }
|
|
251
|
+
:root[data-theme="light"] .tree-node .active-dot.off { color: #bbb; }
|
|
252
|
+
:root[data-theme="light"] #tree-resize-handle:hover,
|
|
253
|
+
:root[data-theme="light"] #tree-resize-handle.active { background: var(--purple); }
|
|
254
|
+
:root[data-theme="light"] .stream-line.text { color: var(--text); }
|
|
255
|
+
|
|
256
|
+
/* Theme toggle button */
|
|
257
|
+
#btn-theme { font-size: 14px; }
|
|
221
258
|
</style>
|
|
222
259
|
</head>
|
|
223
260
|
<body>
|
|
@@ -233,6 +270,7 @@ body {
|
|
|
233
270
|
<span class="sep">│</span>
|
|
234
271
|
<span id="session-info">Connecting...</span>
|
|
235
272
|
<div class="auto">
|
|
273
|
+
<button class="btn btn-icon" id="btn-theme" onclick="toggleTheme()" data-tooltip="Toggle theme">🌙</button>
|
|
236
274
|
<button class="btn on" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
|
|
237
275
|
<span class="sep">│</span>
|
|
238
276
|
<span id="token-info"></span>
|
|
@@ -269,8 +307,9 @@ body {
|
|
|
269
307
|
<span class="sep">│</span>
|
|
270
308
|
</div>
|
|
271
309
|
|
|
272
|
-
<script src="
|
|
273
|
-
<script src="
|
|
310
|
+
<script src="vendor/highlight.min.js"></script>
|
|
311
|
+
<script src="vendor/marked.min.js"></script>
|
|
312
|
+
<script src="vendor/purify.min.js"></script>
|
|
274
313
|
<script>
|
|
275
314
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
276
315
|
// DOM refs
|
|
@@ -290,6 +329,8 @@ let ws = null;
|
|
|
290
329
|
let reconnectTimer = null;
|
|
291
330
|
let reconnectDelay = 1000;
|
|
292
331
|
const MaxReconnectDelay = 30000;
|
|
332
|
+
const MaxReconnectAttempts = 20;
|
|
333
|
+
let reconnectAttempts = 0;
|
|
293
334
|
let showTree = true;
|
|
294
335
|
let autoScroll = true;
|
|
295
336
|
let lastMsgTime = 0;
|
|
@@ -299,8 +340,40 @@ let sessions = [];
|
|
|
299
340
|
let treeNodes = [];
|
|
300
341
|
let treeCursor = 0;
|
|
301
342
|
let streamItems = [];
|
|
302
|
-
|
|
343
|
+
const seenToolIDsKeys = [];
|
|
344
|
+
const seenToolIDsSet = new Set();
|
|
345
|
+
const seenToolIDsMax = 5000;
|
|
346
|
+
|
|
347
|
+
function seenToolIDsHas(key) {
|
|
348
|
+
return seenToolIDsSet.has(key);
|
|
349
|
+
}
|
|
350
|
+
function seenToolIDsAdd(key) {
|
|
351
|
+
seenToolIDsSet.add(key);
|
|
352
|
+
seenToolIDsKeys.push(key);
|
|
353
|
+
if (seenToolIDsKeys.length > seenToolIDsMax) {
|
|
354
|
+
const evictCount = seenToolIDsKeys.length >> 1;
|
|
355
|
+
for (let i = 0; i < evictCount; i++) {
|
|
356
|
+
seenToolIDsSet.delete(seenToolIDsKeys[i]);
|
|
357
|
+
}
|
|
358
|
+
seenToolIDsKeys.splice(0, evictCount);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const toolNameMapMax = 2000;
|
|
303
362
|
let toolNameMap = new Map(); // toolID -> toolName
|
|
363
|
+
let toolNameMapKeys = [];
|
|
364
|
+
|
|
365
|
+
function toolNameMapSet(toolID, toolName) {
|
|
366
|
+
if (toolNameMap.has(toolID)) return;
|
|
367
|
+
toolNameMap.set(toolID, toolName);
|
|
368
|
+
toolNameMapKeys.push(toolID);
|
|
369
|
+
if (toolNameMapKeys.length > toolNameMapMax) {
|
|
370
|
+
const evictCount = toolNameMapKeys.length >> 1;
|
|
371
|
+
for (let i = 0; i < evictCount; i++) {
|
|
372
|
+
toolNameMap.delete(toolNameMapKeys[i]);
|
|
373
|
+
}
|
|
374
|
+
toolNameMapKeys.splice(0, evictCount);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
304
377
|
let filters = new Map();
|
|
305
378
|
|
|
306
379
|
let showThinking = true;
|
|
@@ -314,6 +387,16 @@ let renderPending = false;
|
|
|
314
387
|
let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0;
|
|
315
388
|
let contextData = {};
|
|
316
389
|
|
|
390
|
+
function computeTokensFromContext() {
|
|
391
|
+
totalInput = 0; totalOutput = 0; totalCacheCreate = 0; totalCacheRead = 0;
|
|
392
|
+
for (const ctx of Object.values(contextData)) {
|
|
393
|
+
totalInput += ctx.inputTokens || 0;
|
|
394
|
+
totalOutput += ctx.outputTokens || 0;
|
|
395
|
+
totalCacheCreate += ctx.cacheCreationTokens || 0;
|
|
396
|
+
totalCacheRead += ctx.cacheReadTokens || 0;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
317
400
|
let collapseAfter = 0;
|
|
318
401
|
let collapseTimer = null;
|
|
319
402
|
let activeRefreshTimer = null;
|
|
@@ -349,7 +432,7 @@ marked.setOptions({ renderer: mdRenderer, breaks: true, gfm: true });
|
|
|
349
432
|
|
|
350
433
|
function mdRender(text) {
|
|
351
434
|
try {
|
|
352
|
-
return marked.parse(text);
|
|
435
|
+
return DOMPurify.sanitize(marked.parse(text));
|
|
353
436
|
} catch {
|
|
354
437
|
return esc(text);
|
|
355
438
|
}
|
|
@@ -367,10 +450,16 @@ function connect() {
|
|
|
367
450
|
sessionInfo.textContent = 'Connected';
|
|
368
451
|
lastMsgTime = Date.now();
|
|
369
452
|
reconnectDelay = 1000;
|
|
453
|
+
reconnectAttempts = 0;
|
|
370
454
|
startStaleCheck();
|
|
371
455
|
startActiveRefresh();
|
|
372
456
|
};
|
|
373
457
|
ws.onclose = () => {
|
|
458
|
+
reconnectAttempts++;
|
|
459
|
+
if (reconnectAttempts >= MaxReconnectAttempts) {
|
|
460
|
+
sessionInfo.textContent = 'Disconnected. Please refresh to reconnect.';
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
374
463
|
sessionInfo.textContent = 'Disconnected, reconnecting...';
|
|
375
464
|
stopStaleCheck();
|
|
376
465
|
if (activeRefreshTimer) { clearInterval(activeRefreshTimer); activeRefreshTimer = null; }
|
|
@@ -544,10 +633,8 @@ function handleItemBatch(items) {
|
|
|
544
633
|
}
|
|
545
634
|
|
|
546
635
|
function pushItem(item) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (item.cacheCreationTokens > 0) totalCacheCreate += item.cacheCreationTokens;
|
|
550
|
-
if (item.cacheReadTokens > 0) totalCacheRead += item.cacheReadTokens;
|
|
636
|
+
// Token counts are sourced exclusively from server context messages
|
|
637
|
+
// to avoid divergence between frontend accumulation and server tracking
|
|
551
638
|
|
|
552
639
|
if (item.model) {
|
|
553
640
|
const s = sessions.find(s => s.id === item.sessionID);
|
|
@@ -555,14 +642,13 @@ function pushItem(item) {
|
|
|
555
642
|
}
|
|
556
643
|
|
|
557
644
|
if (item.type === 'tool_input' && item.toolID && item.toolName) {
|
|
558
|
-
|
|
645
|
+
toolNameMapSet(item.toolID, item.toolName);
|
|
559
646
|
}
|
|
560
647
|
|
|
561
648
|
if (item.toolID) {
|
|
562
649
|
const key = `${item.toolID}:${item.type}`;
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
if (seenToolIDs.size > 5000) seenToolIDs.clear();
|
|
650
|
+
if (seenToolIDsHas(key)) return;
|
|
651
|
+
seenToolIDsAdd(key);
|
|
566
652
|
}
|
|
567
653
|
|
|
568
654
|
streamItems.push(item);
|
|
@@ -639,7 +725,6 @@ function getNodeHTML(node, idx) {
|
|
|
639
725
|
if (node.type === 'session') {
|
|
640
726
|
const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
|
|
641
727
|
const parts = [];
|
|
642
|
-
if (node.folder) parts.push(`📂 ${esc(node.folder)}`);
|
|
643
728
|
if (node.model) parts.push(`🧠 ${esc(node.model)}`);
|
|
644
729
|
const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
|
|
645
730
|
const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
|
|
@@ -881,6 +966,7 @@ function refreshButtons() {
|
|
|
881
966
|
sessionInfo.textContent = info;
|
|
882
967
|
|
|
883
968
|
// Token info
|
|
969
|
+
computeTokensFromContext();
|
|
884
970
|
let tokStr = '';
|
|
885
971
|
if (totalInput > 0 || totalOutput > 0) {
|
|
886
972
|
tokStr = `${fmtTok(totalInput)} in / ${fmtTok(totalOutput)} out`;
|
|
@@ -1155,7 +1241,7 @@ function folderName(projectPath) {
|
|
|
1155
1241
|
}
|
|
1156
1242
|
|
|
1157
1243
|
function esc(s) {
|
|
1158
|
-
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1244
|
+
return (s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
1159
1245
|
}
|
|
1160
1246
|
|
|
1161
1247
|
function fmtDur(ms) {
|
|
@@ -1189,6 +1275,37 @@ function scheduleRender() {
|
|
|
1189
1275
|
}
|
|
1190
1276
|
}
|
|
1191
1277
|
|
|
1278
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1279
|
+
// Theme toggle
|
|
1280
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1281
|
+
|
|
1282
|
+
function applyTheme(theme) {
|
|
1283
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
1284
|
+
const btn = document.getElementById('btn-theme');
|
|
1285
|
+
if (btn) {
|
|
1286
|
+
btn.textContent = theme === 'dark' ? '🌙' : '☀️';
|
|
1287
|
+
btn.setAttribute('data-tooltip', theme === 'dark' ? 'Switch to light' : 'Switch to dark');
|
|
1288
|
+
}
|
|
1289
|
+
// Swap highlight.js stylesheet for theme
|
|
1290
|
+
const hlLink = document.querySelector('link[rel="stylesheet"][href*="github"]');
|
|
1291
|
+
if (hlLink) {
|
|
1292
|
+
hlLink.href = theme === 'dark' ? 'vendor/github-dark.min.css' : 'vendor/github-light.min.css';
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function toggleTheme() {
|
|
1297
|
+
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
1298
|
+
const next = current === 'dark' ? 'light' : 'dark';
|
|
1299
|
+
localStorage.setItem('theme', next);
|
|
1300
|
+
applyTheme(next);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Apply saved theme on load (default dark)
|
|
1304
|
+
(function() {
|
|
1305
|
+
const saved = localStorage.getItem('theme');
|
|
1306
|
+
applyTheme(saved || 'dark');
|
|
1307
|
+
})();
|
|
1308
|
+
|
|
1192
1309
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
1193
1310
|
// Init
|
|
1194
1311
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
|
2
|
+
Theme: GitHub Dark
|
|
3
|
+
Description: Dark theme as seen on github.com
|
|
4
|
+
Author: github.com
|
|
5
|
+
Maintainer: @Hirse
|
|
6
|
+
Updated: 2021-05-15
|
|
7
|
+
|
|
8
|
+
Outdated base version: https://github.com/primer/github-syntax-dark
|
|
9
|
+
Current colors taken from GitHub's CSS
|
|
10
|
+
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
|
2
|
+
Theme: GitHub
|
|
3
|
+
Description: Light theme as seen on github.com
|
|
4
|
+
Author: github.com
|
|
5
|
+
Maintainer: @Hirse
|
|
6
|
+
Updated: 2021-05-15
|
|
7
|
+
|
|
8
|
+
Outdated base version: https://github.com/primer/github-syntax-light
|
|
9
|
+
Current colors taken from GitHub's CSS
|
|
10
|
+
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|