claude-code-watch 0.0.6 → 0.0.8

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 CHANGED
@@ -12,7 +12,7 @@ Claude Code writes detailed JSONL logs under `~/.claude/projects/` as it works
12
12
  - **Multi-session** — watch all active Claude Code sessions simultaneously in a tree view
13
13
  - **Subagent tracking** — see subagent activity nested under their parent session
14
14
  - **Token & cost visibility** — tracks input/output/cache tokens per agent, with context window utilization
15
- - **Filter controls** — toggle thinking, tool input, tool output, and text visibility independently
15
+ - **Filter controls** — toggle thinking, tool input, tool output, hook output, and text visibility independently
16
16
  - **Auto-discovery** — automatically picks up new sessions as they start (toggleable)
17
17
 
18
18
  ## Quick Start
@@ -51,7 +51,7 @@ OPTIONS:
51
51
  -n Start from newest (skip history, live only)
52
52
  -l [N] List recent sessions (default 10) and exit
53
53
  -a [N] List active sessions (default all) and exit
54
- -w <dur> Active window duration (default 5m, e.g. 30s, 2m, 10m)
54
+ -w <dur> Active window duration (default 30m, e.g. 30s, 2m, 10m)
55
55
  -m <N> Max sessions to show in tree (default 0=unlimited)
56
56
  -c <dur> Auto-collapse sessions inactive for this duration (e.g. 2m)
57
57
  -D Debug: show raw type:subtype for every JSONL line we'd drop
package/README.zh-CN.md CHANGED
@@ -22,7 +22,7 @@ Claude Code 在运行时会将详细的 JSONL 日志写入 `~/.claude/projects/`
22
22
  - **多会话监视** — 同时查看所有活跃的 Claude Code 会话
23
23
  - **子代理追踪** — 在父会话下嵌套显示子代理活动
24
24
  - **Token/成本追踪** — 每个代理的输入/输出/缓存 token 及上下文窗口利用率
25
- - **过滤控制** — 独立切换 thinking、工具输入/输出、文本的可见性
25
+ - **过滤控制** — 独立切换 thinking、工具输入/输出、hook 输出、文本的可见性
26
26
  - **自动发现** — 新会话启动时自动纳入监控
27
27
 
28
28
  ## 致谢
@@ -150,7 +150,7 @@ async function main() {
150
150
  sessionID: '',
151
151
  skipHistory: false,
152
152
  pollMs: 500,
153
- activeWindow: 5 * 60 * 1000,
153
+ activeWindow: 30 * 60 * 1000,
154
154
  maxSessions: 0,
155
155
  collapseAfter: 0,
156
156
  debugAll: false,
@@ -190,7 +190,7 @@ async function main() {
190
190
  break;
191
191
  case '-w':
192
192
  try {
193
- options.activeWindow = parseDuration(args[++i] || '5m');
193
+ options.activeWindow = parseDuration(args[++i] || '30m');
194
194
  } catch {
195
195
  options.activeWindow = 5 * 60 * 1000;
196
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Web-based real-time monitor for Claude Code.",
5
5
  "main": "./src/server/server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -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>
@@ -227,12 +264,14 @@ body {
227
264
  <button class="btn on" id="btn-tool-input" onclick="toggleToolInput()" data-tooltip="Toggle tool input">🔧 Tools</button>
228
265
  <button class="btn on" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
229
266
  <button class="btn on" id="btn-text" onclick="toggleText()" data-tooltip="Toggle text responses">💬 Text</button>
267
+ <button class="btn on" id="btn-hook" onclick="toggleHook()" data-tooltip="Toggle hook output">🪝 Hook</button>
230
268
  <span class="sep">│</span>
231
269
  <button class="btn on" id="btn-autoscroll" onclick="toggleAutoScroll()" data-tooltip="Auto-scroll">⇣ Auto</button>
232
270
  <button class="btn" id="btn-tree-toggle" onclick="toggleTree()" data-tooltip="Toggle tree panel">📂 Tree</button>
233
271
  <span class="sep">│</span>
234
272
  <span id="session-info">Connecting...</span>
235
273
  <div class="auto">
274
+ <button class="btn btn-icon" id="btn-theme" onclick="toggleTheme()" data-tooltip="Toggle theme">🌙</button>
236
275
  <button class="btn on" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
237
276
  <span class="sep">│</span>
238
277
  <span id="token-info"></span>
@@ -342,6 +381,7 @@ let showThinking = true;
342
381
  let showToolInput = true;
343
382
  let showToolOutput = true;
344
383
  let showText = true;
384
+ let showHook = true;
345
385
  let autoDiscovery = true;
346
386
 
347
387
  let renderPending = false;
@@ -363,7 +403,7 @@ let collapseAfter = 0;
363
403
  let collapseTimer = null;
364
404
  let activeRefreshTimer = null;
365
405
 
366
- const MAX_ITEMS = 1000;
406
+ const MAX_ITEMS = 3000;
367
407
  const MAX_LINES = 50;
368
408
  let renderedItemCount = 0;
369
409
  let needsFullRender = true;
@@ -514,6 +554,7 @@ function handleSnapshot(payload) {
514
554
  }
515
555
  updateFilters();
516
556
  rebuildNodes();
557
+ needsFullRender = true;
517
558
  scheduleRender();
518
559
  }
519
560
 
@@ -528,6 +569,7 @@ function handleNewSession(payload) {
528
569
  });
529
570
  updateFilters();
530
571
  rebuildNodes();
572
+ needsFullRender = true;
531
573
  scheduleRender();
532
574
  }
533
575
 
@@ -541,6 +583,7 @@ function handleNewAgent(payload) {
541
583
  });
542
584
  updateFilters();
543
585
  rebuildNodes();
586
+ needsFullRender = true;
544
587
  scheduleRender();
545
588
  }
546
589
 
@@ -558,9 +601,13 @@ function handleNewBgTask(payload) {
558
601
 
559
602
  function handleSessionRemoved(payload) {
560
603
  const idx = sessions.findIndex(s => s.id === payload.sessionID);
561
- if (idx >= 0) sessions.splice(idx, 1);
604
+ if (idx >= 0) {
605
+ const session = sessions.splice(idx, 1)[0];
606
+ sessions.push(session);
607
+ }
562
608
  updateFilters();
563
609
  rebuildNodes();
610
+ needsFullRender = true;
564
611
  scheduleRender();
565
612
  }
566
613
 
@@ -627,6 +674,7 @@ function isItemVisible(item) {
627
674
  case 'tool_input': return showToolInput;
628
675
  case 'tool_output': return showToolOutput;
629
676
  case 'text': return showText;
677
+ case 'hook_output': return showHook;
630
678
  default: return true;
631
679
  }
632
680
  }
@@ -783,6 +831,7 @@ function isSessionActive(session) {
783
831
 
784
832
  function renderStream() {
785
833
  const visible = streamItems.filter(isItemVisible);
834
+ const wasAutoScroll = autoScroll;
786
835
 
787
836
  if (needsFullRender || renderedItemCount > visible.length) {
788
837
  // Full rebuild: filter changed, items trimmed, or initial render
@@ -806,10 +855,9 @@ function renderStream() {
806
855
  streamEl.innerHTML = html;
807
856
  renderedItemCount = visible.length;
808
857
  needsFullRender = false;
809
- if (autoScroll) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
858
+ if (wasAutoScroll) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
810
859
  } else {
811
860
  // Incremental append: only add new items since last render
812
- const wasAtBottom = streamEl.scrollHeight - streamEl.scrollTop - streamEl.clientHeight < 50;
813
861
  for (let i = renderedItemCount; i < visible.length; i++) {
814
862
  for (const l of renderItem(visible[i])) {
815
863
  const div = document.createElement('div');
@@ -819,7 +867,7 @@ function renderStream() {
819
867
  }
820
868
  }
821
869
  renderedItemCount = visible.length;
822
- if (autoScroll && wasAtBottom) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
870
+ if (autoScroll) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
823
871
  }
824
872
 
825
873
  const maxScroll = streamEl.scrollHeight - streamEl.clientHeight;
@@ -913,6 +961,7 @@ function refreshButtons() {
913
961
  document.getElementById('btn-tool-input').classList.toggle('on', showToolInput);
914
962
  document.getElementById('btn-tool-output').classList.toggle('on', showToolOutput);
915
963
  document.getElementById('btn-text').classList.toggle('on', showText);
964
+ document.getElementById('btn-hook').classList.toggle('on', showHook);
916
965
  document.getElementById('btn-autoscroll').classList.toggle('on', autoScroll);
917
966
  document.getElementById('btn-tree-toggle').classList.toggle('on', showTree);
918
967
  document.getElementById('btn-autodisco').classList.toggle('on', autoDiscovery);
@@ -1066,8 +1115,12 @@ function removeSelectedSession() {
1066
1115
  if (node.type === 'session') sid = node.id;
1067
1116
  else sid = node.sessionID;
1068
1117
  if (!sid) return;
1069
- if (!confirm(`Remove session ${sid.slice(0, 12)}...?`)) return;
1070
- sessions = sessions.filter(s => s.id !== sid);
1118
+ if (!confirm(`Move session ${sid.slice(0, 12)}... to bottom?`)) return;
1119
+ const idx = sessions.findIndex(s => s.id === sid);
1120
+ if (idx >= 0) {
1121
+ const session = sessions.splice(idx, 1)[0];
1122
+ sessions.push(session);
1123
+ }
1071
1124
  sendCmd('removeSession', { sessionID: sid });
1072
1125
  updateFilters();
1073
1126
  rebuildNodes();
@@ -1082,13 +1135,14 @@ function toggleThinking() { showThinking = !showThinking; needsFullRender = true
1082
1135
  function toggleToolInput() { showToolInput = !showToolInput; needsFullRender = true; renderStream(); refreshButtons(); }
1083
1136
  function toggleToolOutput() { showToolOutput = !showToolOutput; needsFullRender = true; renderStream(); refreshButtons(); }
1084
1137
  function toggleText() { showText = !showText; needsFullRender = true; renderStream(); refreshButtons(); }
1138
+ function toggleHook() { showHook = !showHook; needsFullRender = true; renderStream(); refreshButtons(); }
1085
1139
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1086
1140
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1087
1141
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
1088
1142
 
1089
1143
  function scrollToTop() { streamEl.scrollTop = 0; autoScroll = false; renderAll(); }
1090
1144
  function scrollUp() { streamEl.scrollTop -= 80; autoScroll = false; renderAll(); }
1091
- function scrollDown() { streamEl.scrollTop += 80; renderAll(); }
1145
+ function scrollDown() { streamEl.scrollTop += 80; if (autoScroll) autoScroll = false; renderAll(); }
1092
1146
  function scrollToBottom() { streamEl.scrollTop = streamEl.scrollHeight; autoScroll = true; renderAll(); }
1093
1147
 
1094
1148
  // ══════════════════════════════════════════════════════════════════════════════
@@ -1232,11 +1286,44 @@ function scheduleRender() {
1232
1286
  renderPending = true;
1233
1287
  requestAnimationFrame(() => {
1234
1288
  renderPending = false;
1235
- renderAll();
1289
+ renderTree();
1290
+ renderStream();
1291
+ refreshButtons();
1236
1292
  });
1237
1293
  }
1238
1294
  }
1239
1295
 
1296
+ // ══════════════════════════════════════════════════════════════════════════════
1297
+ // Theme toggle
1298
+ // ══════════════════════════════════════════════════════════════════════════════
1299
+
1300
+ function applyTheme(theme) {
1301
+ document.documentElement.setAttribute('data-theme', theme);
1302
+ const btn = document.getElementById('btn-theme');
1303
+ if (btn) {
1304
+ btn.textContent = theme === 'dark' ? '🌙' : '☀️';
1305
+ btn.setAttribute('data-tooltip', theme === 'dark' ? 'Switch to light' : 'Switch to dark');
1306
+ }
1307
+ // Swap highlight.js stylesheet for theme
1308
+ const hlLink = document.querySelector('link[rel="stylesheet"][href*="github"]');
1309
+ if (hlLink) {
1310
+ hlLink.href = theme === 'dark' ? 'vendor/github-dark.min.css' : 'vendor/github-light.min.css';
1311
+ }
1312
+ }
1313
+
1314
+ function toggleTheme() {
1315
+ const current = document.documentElement.getAttribute('data-theme') || 'dark';
1316
+ const next = current === 'dark' ? 'light' : 'dark';
1317
+ localStorage.setItem('theme', next);
1318
+ applyTheme(next);
1319
+ }
1320
+
1321
+ // Apply saved theme on load (default dark)
1322
+ (function() {
1323
+ const saved = localStorage.getItem('theme');
1324
+ applyTheme(saved || 'dark');
1325
+ })();
1326
+
1240
1327
  // ══════════════════════════════════════════════════════════════════════════════
1241
1328
  // Init
1242
1329
  // ══════════════════════════════════════════════════════════════════════════════
@@ -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}
@@ -21,7 +21,7 @@ var MIME = {
21
21
  };
22
22
 
23
23
  var MAX_ITEM_BUFFER = 2000;
24
- var CONTEXT_STALE_MS = 30 * 60 * 1000; // 30 minutes
24
+ var CONTEXT_STALE_MS = 60 * 60 * 1000; // 60 minutes
25
25
 
26
26
  class DashboardServer {
27
27
  constructor(options = {}) {
@@ -52,7 +52,7 @@ class DashboardServer {
52
52
  ctx = { inputTokens: 0, outputTokens: 0, cacheCreation: 0, cacheRead: 0, model: '', contextWindow: 200000, lastActivity: Date.now() };
53
53
  this.contextMap.set(key, ctx);
54
54
  }
55
- if (item.inputTokens) ctx.inputTokens += item.inputTokens;
55
+ if (item.inputTokens) ctx.inputTokens = Math.max(ctx.inputTokens, item.inputTokens);
56
56
  if (item.outputTokens) ctx.outputTokens += item.outputTokens;
57
57
  if (item.cacheCreationTokens) ctx.cacheCreation += item.cacheCreationTokens;
58
58
  if (item.cacheReadTokens) ctx.cacheRead += item.cacheReadTokens;
@@ -353,7 +353,7 @@ class DashboardServer {
353
353
  }
354
354
  const skipHistory = options.skipHistory || false;
355
355
  const pollMs = options.pollMs || 500;
356
- const activeWindow = options.activeWindow || 5 * 60 * 1000;
356
+ const activeWindow = options.activeWindow || 100 * 60 * 1000;
357
357
  const maxSessions = options.maxSessions || 0;
358
358
  const openBrowser = options.openBrowser !== false;
359
359
 
@@ -400,19 +400,6 @@ class DashboardServer {
400
400
  await w.init();
401
401
  if (skipHistory) w.setSkipHistory(true);
402
402
  await w.start();
403
-
404
- // Open browser AFTER sessions are discovered, so new clients get a full snapshot
405
- if (openBrowser) {
406
- const url = `http://localhost:${this.port}`;
407
- const platform = process.platform;
408
- if (platform === 'darwin') {
409
- cp.spawn('open', [url]);
410
- } else if (platform === 'win32') {
411
- cp.spawn('cmd', ['/c', 'start', '', url]);
412
- } else {
413
- cp.spawn('xdg-open', [url]);
414
- }
415
- }
416
403
  } catch (err) {
417
404
  console.error('Watcher init error:', err.message);
418
405
  process.exit(1);
@@ -420,15 +407,32 @@ class DashboardServer {
420
407
 
421
408
  this._contextCleanupTimer = setInterval(() => this.cleanupContextMap(), CONTEXT_STALE_MS);
422
409
 
423
- this.server.listen(this.port, this.host, () => {
424
- const url = `http://localhost:${this.port}`;
425
- console.log(`\n claude-watch web server`);
426
- console.log(` ───────────────────────────`);
427
- console.log(` Local: ${url}`);
428
- console.log(` Network: http://${this.host}:${this.port}`);
429
- console.log(` Quit: Ctrl+C\n`);
410
+ // Start listening and wait for server to be ready before opening browser
411
+ await new Promise((resolve) => {
412
+ this.server.listen(this.port, this.host, () => {
413
+ const url = `http://localhost:${this.port}`;
414
+ console.log(`\n claude-watch web server`);
415
+ console.log(` ───────────────────────────`);
416
+ console.log(` Local: ${url}`);
417
+ console.log(` Network: http://${this.host}:${this.port}`);
418
+ console.log(` Quit: Ctrl+C\n`);
419
+ resolve();
420
+ });
430
421
  });
431
422
 
423
+ // Open browser AFTER server is confirmed listening and watcher is ready
424
+ if (openBrowser) {
425
+ const url = `http://localhost:${this.port}`;
426
+ const platform = process.platform;
427
+ if (platform === 'darwin') {
428
+ cp.spawn('open', [url]);
429
+ } else if (platform === 'win32') {
430
+ cp.spawn('cmd', ['/c', 'start', '', url]);
431
+ } else {
432
+ cp.spawn('xdg-open', [url]);
433
+ }
434
+ }
435
+
432
436
  return { server: this.server, watcher: w };
433
437
  }
434
438
 
@@ -111,7 +111,7 @@ class Watcher extends EventEmitter {
111
111
  super();
112
112
  this.claudeDir = getClaudeProjectsDir();
113
113
  this.pollInterval = pollInterval || 500;
114
- this.activeWindow = activeWindow || 5 * 60 * 1000;
114
+ this.activeWindow = activeWindow || 100 * 60 * 1000;
115
115
  this.maxSessions = maxSessions || 0;
116
116
  this.sessions = new Map();
117
117
  this.filePositions = new Map();