claude-code-watch 0.0.7 → 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.7",
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
@@ -264,6 +264,7 @@ body {
264
264
  <button class="btn on" id="btn-tool-input" onclick="toggleToolInput()" data-tooltip="Toggle tool input">🔧 Tools</button>
265
265
  <button class="btn on" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
266
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>
267
268
  <span class="sep">│</span>
268
269
  <button class="btn on" id="btn-autoscroll" onclick="toggleAutoScroll()" data-tooltip="Auto-scroll">⇣ Auto</button>
269
270
  <button class="btn" id="btn-tree-toggle" onclick="toggleTree()" data-tooltip="Toggle tree panel">📂 Tree</button>
@@ -380,6 +381,7 @@ let showThinking = true;
380
381
  let showToolInput = true;
381
382
  let showToolOutput = true;
382
383
  let showText = true;
384
+ let showHook = true;
383
385
  let autoDiscovery = true;
384
386
 
385
387
  let renderPending = false;
@@ -401,7 +403,7 @@ let collapseAfter = 0;
401
403
  let collapseTimer = null;
402
404
  let activeRefreshTimer = null;
403
405
 
404
- const MAX_ITEMS = 1000;
406
+ const MAX_ITEMS = 3000;
405
407
  const MAX_LINES = 50;
406
408
  let renderedItemCount = 0;
407
409
  let needsFullRender = true;
@@ -552,6 +554,7 @@ function handleSnapshot(payload) {
552
554
  }
553
555
  updateFilters();
554
556
  rebuildNodes();
557
+ needsFullRender = true;
555
558
  scheduleRender();
556
559
  }
557
560
 
@@ -566,6 +569,7 @@ function handleNewSession(payload) {
566
569
  });
567
570
  updateFilters();
568
571
  rebuildNodes();
572
+ needsFullRender = true;
569
573
  scheduleRender();
570
574
  }
571
575
 
@@ -579,6 +583,7 @@ function handleNewAgent(payload) {
579
583
  });
580
584
  updateFilters();
581
585
  rebuildNodes();
586
+ needsFullRender = true;
582
587
  scheduleRender();
583
588
  }
584
589
 
@@ -596,9 +601,13 @@ function handleNewBgTask(payload) {
596
601
 
597
602
  function handleSessionRemoved(payload) {
598
603
  const idx = sessions.findIndex(s => s.id === payload.sessionID);
599
- if (idx >= 0) sessions.splice(idx, 1);
604
+ if (idx >= 0) {
605
+ const session = sessions.splice(idx, 1)[0];
606
+ sessions.push(session);
607
+ }
600
608
  updateFilters();
601
609
  rebuildNodes();
610
+ needsFullRender = true;
602
611
  scheduleRender();
603
612
  }
604
613
 
@@ -665,6 +674,7 @@ function isItemVisible(item) {
665
674
  case 'tool_input': return showToolInput;
666
675
  case 'tool_output': return showToolOutput;
667
676
  case 'text': return showText;
677
+ case 'hook_output': return showHook;
668
678
  default: return true;
669
679
  }
670
680
  }
@@ -821,6 +831,7 @@ function isSessionActive(session) {
821
831
 
822
832
  function renderStream() {
823
833
  const visible = streamItems.filter(isItemVisible);
834
+ const wasAutoScroll = autoScroll;
824
835
 
825
836
  if (needsFullRender || renderedItemCount > visible.length) {
826
837
  // Full rebuild: filter changed, items trimmed, or initial render
@@ -844,10 +855,9 @@ function renderStream() {
844
855
  streamEl.innerHTML = html;
845
856
  renderedItemCount = visible.length;
846
857
  needsFullRender = false;
847
- if (autoScroll) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
858
+ if (wasAutoScroll) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
848
859
  } else {
849
860
  // Incremental append: only add new items since last render
850
- const wasAtBottom = streamEl.scrollHeight - streamEl.scrollTop - streamEl.clientHeight < 50;
851
861
  for (let i = renderedItemCount; i < visible.length; i++) {
852
862
  for (const l of renderItem(visible[i])) {
853
863
  const div = document.createElement('div');
@@ -857,7 +867,7 @@ function renderStream() {
857
867
  }
858
868
  }
859
869
  renderedItemCount = visible.length;
860
- if (autoScroll && wasAtBottom) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
870
+ if (autoScroll) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
861
871
  }
862
872
 
863
873
  const maxScroll = streamEl.scrollHeight - streamEl.clientHeight;
@@ -951,6 +961,7 @@ function refreshButtons() {
951
961
  document.getElementById('btn-tool-input').classList.toggle('on', showToolInput);
952
962
  document.getElementById('btn-tool-output').classList.toggle('on', showToolOutput);
953
963
  document.getElementById('btn-text').classList.toggle('on', showText);
964
+ document.getElementById('btn-hook').classList.toggle('on', showHook);
954
965
  document.getElementById('btn-autoscroll').classList.toggle('on', autoScroll);
955
966
  document.getElementById('btn-tree-toggle').classList.toggle('on', showTree);
956
967
  document.getElementById('btn-autodisco').classList.toggle('on', autoDiscovery);
@@ -1104,8 +1115,12 @@ function removeSelectedSession() {
1104
1115
  if (node.type === 'session') sid = node.id;
1105
1116
  else sid = node.sessionID;
1106
1117
  if (!sid) return;
1107
- if (!confirm(`Remove session ${sid.slice(0, 12)}...?`)) return;
1108
- 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
+ }
1109
1124
  sendCmd('removeSession', { sessionID: sid });
1110
1125
  updateFilters();
1111
1126
  rebuildNodes();
@@ -1120,13 +1135,14 @@ function toggleThinking() { showThinking = !showThinking; needsFullRender = true
1120
1135
  function toggleToolInput() { showToolInput = !showToolInput; needsFullRender = true; renderStream(); refreshButtons(); }
1121
1136
  function toggleToolOutput() { showToolOutput = !showToolOutput; needsFullRender = true; renderStream(); refreshButtons(); }
1122
1137
  function toggleText() { showText = !showText; needsFullRender = true; renderStream(); refreshButtons(); }
1138
+ function toggleHook() { showHook = !showHook; needsFullRender = true; renderStream(); refreshButtons(); }
1123
1139
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1124
1140
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1125
1141
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
1126
1142
 
1127
1143
  function scrollToTop() { streamEl.scrollTop = 0; autoScroll = false; renderAll(); }
1128
1144
  function scrollUp() { streamEl.scrollTop -= 80; autoScroll = false; renderAll(); }
1129
- function scrollDown() { streamEl.scrollTop += 80; renderAll(); }
1145
+ function scrollDown() { streamEl.scrollTop += 80; if (autoScroll) autoScroll = false; renderAll(); }
1130
1146
  function scrollToBottom() { streamEl.scrollTop = streamEl.scrollHeight; autoScroll = true; renderAll(); }
1131
1147
 
1132
1148
  // ══════════════════════════════════════════════════════════════════════════════
@@ -1270,7 +1286,9 @@ function scheduleRender() {
1270
1286
  renderPending = true;
1271
1287
  requestAnimationFrame(() => {
1272
1288
  renderPending = false;
1273
- renderAll();
1289
+ renderTree();
1290
+ renderStream();
1291
+ refreshButtons();
1274
1292
  });
1275
1293
  }
1276
1294
  }
@@ -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();