claude-code-watch 0.0.14 → 0.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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
@@ -147,18 +147,27 @@ body {
147
147
 
148
148
  /* ── Tree node styles ── */
149
149
  .tree-row {
150
- display: flex; align-items: center;
150
+ display: flex; align-items: flex-start;
151
+ }
152
+ .tree-content {
153
+ flex: 1; min-width: 0;
151
154
  }
155
+ .tree-content:hover { background: rgba(255,255,255,0.05); }
156
+ .tree-row.selected > .tree-content { background: rgba(124,58,237,0.3); }
157
+ .tree-content.dim { opacity: 0.4; }
152
158
  .tree-node {
153
- flex: 1; display: flex; align-items: center;
159
+ display: flex; align-items: center;
154
160
  padding: 3px 2px 3px 0;
155
161
  cursor: pointer; white-space: nowrap; gap: 4px;
156
- min-width: 0; overflow: hidden;
162
+ overflow: hidden;
157
163
  }
158
- .tree-node:hover { background: rgba(255,255,255,0.05); }
159
- .tree-node.selected { background: rgba(124,58,237,0.3); }
160
- .tree-node.dim { opacity: 0.4; }
161
164
  .tree-prefix { color: var(--dim); font-size: 12px; flex-shrink: 0; letter-spacing: 0; font-family: monospace; }
165
+ .tree-activity {
166
+ font-size: 10px; color: var(--dim); white-space: nowrap;
167
+ overflow: hidden; text-overflow: ellipsis;
168
+ padding: 0 2px 2px; line-height: 1.2;
169
+ cursor: pointer;
170
+ }
162
171
  .tree-node .ctx-pct { font-size: 10px; margin-left: 4px; flex-shrink: 0; }
163
172
  .tree-node .ctx-pct.warn { color: var(--yellow); }
164
173
  .tree-node .ctx-pct.danger { color: var(--red); }
@@ -191,6 +200,7 @@ body {
191
200
  .stream-line.tool-output { color: var(--green); }
192
201
  .stream-line.text { color: var(--text); }
193
202
  .stream-line.hook { color: var(--cyan); }
203
+ .stream-line .hook-label { color: var(--dim); }
194
204
  .stream-line.diag { color: var(--red); }
195
205
  .stream-line.debug { color: var(--gray); }
196
206
  .stream-line.marker { color: var(--dim); }
@@ -249,8 +259,8 @@ body {
249
259
  :root[data-theme="light"] .btn.on:hover { background: var(--purple2); color: #fff; }
250
260
  :root[data-theme="light"] .btn.on:hover::after { background: var(--purple2); color: #fff; }
251
261
  :root[data-theme="light"] .hljs { background: #f0f0f0 !important; }
252
- :root[data-theme="light"] .tree-node:hover { background: rgba(0,0,0,0.06); }
253
- :root[data-theme="light"] .tree-node.selected { background: rgba(124,58,237,0.2); }
262
+ :root[data-theme="light"] .tree-content:hover { background: rgba(0,0,0,0.06); }
263
+ :root[data-theme="light"] .tree-row.selected > .tree-content { background: rgba(124,58,237,0.2); }
254
264
  :root[data-theme="light"] .tree-node .active-dot.off { color: #bbb; }
255
265
  :root[data-theme="light"] #tree-resize-handle:hover,
256
266
  :root[data-theme="light"] #tree-resize-handle.active { background: var(--purple); }
@@ -288,6 +298,7 @@ body {
288
298
  <button class="btn btn-icon" onclick="selectAll()" data-tooltip="Show all sessions/agents">⊞</button>
289
299
  <button class="btn btn-icon accent" onclick="soloSelected()" data-tooltip="Solo selected">⊙</button>
290
300
  <button class="btn btn-icon danger" onclick="removeSelectedSession()" data-tooltip="Remove session">✕</button>
301
+ <button class="btn btn-icon on" id="btn-activity" onclick="toggleActivity()" data-tooltip="Toggle activity info">💬</button>
291
302
  <span style="flex:1"></span>
292
303
  <span id="tree-cursor-info" style="font-size:10px;color:var(--dim)"></span>
293
304
  </div>
@@ -357,6 +368,9 @@ class LRUCache {
357
368
  }
358
369
  const seenToolIDs = new LRUCache(5000);
359
370
  const toolNameMap = new LRUCache(2000);
371
+ const agentActivity = new Map(); // "sessionID:agentID" → { toolName, content }
372
+ const taskDescriptions = new Map(); // toolID → description string
373
+ const MAX_DESC_STORE = 200;
360
374
  let filters = new Map();
361
375
 
362
376
  let showThinking = true;
@@ -364,6 +378,7 @@ let showToolInput = true;
364
378
  let showToolOutput = true;
365
379
  let showText = true;
366
380
  let showHook = true;
381
+ let showActivity = true;
367
382
  let autoDiscovery = true;
368
383
 
369
384
  let renderPending = false;
@@ -646,6 +661,20 @@ function pushItem(item) {
646
661
  toolNameMap.set(item.toolID, item.toolName);
647
662
  }
648
663
 
664
+ if (item.type === 'tool_input') {
665
+ // Main 代理不追踪工具调用,只显示用户 prompt
666
+ if (item.agentID) {
667
+ agentActivity.set(item.sessionID + ':' + item.agentID, { toolName: item.toolName || '', content: (item.content || '').slice(0, MAX_DESC_STORE) });
668
+ }
669
+ if (item.toolID) {
670
+ taskDescriptions.set(item.toolID, (item.content || '').slice(0, MAX_DESC_STORE));
671
+ }
672
+ }
673
+
674
+ if (item.type === 'user_text') {
675
+ agentActivity.set(item.sessionID + ':' + (item.agentID || ''), { toolName: '', content: (item.content || '').slice(0, MAX_DESC_STORE) });
676
+ }
677
+
649
678
  if (item.toolID) {
650
679
  const key = `${item.toolID}:${item.type}`;
651
680
  if (seenToolIDs.has(key)) return;
@@ -671,6 +700,7 @@ function isItemVisible(item) {
671
700
  case 'tool_output': return showToolOutput;
672
701
  case 'text': return showText;
673
702
  case 'hook_output': return showHook;
703
+ case 'user_text': return false;
674
704
  default: return true;
675
705
  }
676
706
  }
@@ -694,14 +724,23 @@ function rebuildNodes() {
694
724
  );
695
725
  const lastTaskIdx = tasks.length - 1;
696
726
  const hasTasks = tasks.length > 0;
697
- treeNodes.push({ type: a.type, id: a.id, name: a.name, sessionID: s.id, level: 1, isLast: isLastAgent && !hasTasks });
727
+ const actKey = s.id + ':' + a.id;
728
+ const act = agentActivity.get(actKey);
729
+ treeNodes.push({
730
+ type: a.type, id: a.id, name: a.name, sessionID: s.id,
731
+ level: 1, isLast: isLastAgent && !hasTasks,
732
+ activityTool: act ? act.toolName : '',
733
+ activityDesc: act ? act.content : '',
734
+ });
698
735
  for (let ti = 0; ti < tasks.length; ti++) {
699
736
  const t = tasks[ti];
737
+ const tDesc = taskDescriptions.get(t.id);
700
738
  treeNodes.push({
701
739
  type: 'task', id: t.id, name: t.toolName,
702
740
  sessionID: s.id, parentAgentID: t.parentAgentID,
703
741
  outputPath: t.outputPath, isComplete: t.isComplete,
704
742
  level: 2, isLast: isLastAgent && ti === lastTaskIdx,
743
+ description: tDesc || '',
705
744
  });
706
745
  }
707
746
  }
@@ -736,10 +775,12 @@ function getNodeHTML(node, idx) {
736
775
  const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
737
776
  const agentCount = node.agents ? node.agents.filter(a => a.type === 'agent').length : 0;
738
777
  return `<div class="tree-row${selClass ? ' selected' : ''}">
739
- <div class="tree-node" onclick="treeClick(${idx})" data-idx="${idx}">
740
- <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
741
- ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
742
- ${subInfo}
778
+ <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
779
+ <div class="tree-node">
780
+ <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
781
+ ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
782
+ ${subInfo}
783
+ </div>
743
784
  </div>
744
785
  <span class="tree-actions">
745
786
  <button class="btn btn-icon accent" onclick="event.stopPropagation();selectIndex(${idx});soloSelected()" data-tooltip="Solo">⊙</button>
@@ -760,9 +801,19 @@ function getNodeHTML(node, idx) {
760
801
  ctxPct = `<span class="ctx-pct ${cls}">${pct}%</span>`;
761
802
  }
762
803
  const activeDot = ctx && (Date.now() - ctx.lastActivity < 120000) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
804
+ const actIcon = node.type === 'main' ? '🗣' : '⚡';
805
+ const actText = showActivity && (node.activityTool || node.activityDesc)
806
+ ? (node.activityTool && node.activityDesc ? `${node.activityTool}: ${node.activityDesc}` : (node.activityTool || node.activityDesc))
807
+ : '';
808
+ const activityHTML = actText
809
+ ? `<div class="tree-activity">${actIcon} ${esc(actText)}</div>`
810
+ : '';
763
811
  return `<div class="tree-row${selClass ? ' selected' : ''}">
764
- <div class="tree-node${enabled ? '' : ' dim'}" onclick="treeClick(${idx})" data-idx="${idx}">
812
+ <div class="tree-content${enabled ? '' : ' dim'}" onclick="treeClick(${idx})" data-idx="${idx}">
813
+ <div class="tree-node">
765
814
  <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${icon} ${esc(node.name || '')}${ctxPct}
815
+ </div>
816
+ ${activityHTML}
766
817
  </div>
767
818
  <span class="tree-actions">
768
819
  <button class="btn btn-icon accent" onclick="event.stopPropagation();selectIndex(${idx});soloSelected()" data-tooltip="Solo">⊙</button>
@@ -773,9 +824,15 @@ function getNodeHTML(node, idx) {
773
824
 
774
825
  if (node.type === 'task') {
775
826
  const icon = node.isComplete ? '✓' : '⏳';
827
+ const descHTML = showActivity && node.description
828
+ ? `<div class="tree-activity">📋 ${esc(node.description)}</div>`
829
+ : '';
776
830
  return `<div class="tree-row${selClass ? ' selected' : ''}">
777
- <div class="tree-node dim" onclick="treeClick(${idx})" data-idx="${idx}">
831
+ <div class="tree-content dim" onclick="treeClick(${idx})" data-idx="${idx}">
832
+ <div class="tree-node">
778
833
  <span class="tree-prefix">${treePrefix(node)}</span>${icon} ${esc(node.name || 'bg-task')}
834
+ </div>
835
+ ${descHTML}
779
836
  </div>
780
837
  <span class="tree-actions">
781
838
  <button class="btn btn-icon" onclick="event.stopPropagation();selectIndex(${idx});loadBgTask(${idx})" data-tooltip="Load output">▶</button>
@@ -799,7 +856,7 @@ function renderTree() {
799
856
  treeEl.innerHTML = html;
800
857
 
801
858
  // Scroll selected into view
802
- const sel = treeEl.querySelector('.tree-node.selected');
859
+ const sel = treeEl.querySelector('.tree-row.selected');
803
860
  if (sel) sel.scrollIntoView({ block: 'nearest' });
804
861
 
805
862
  treeCursorInfo.textContent = `${treeCursor + 1}/${treeNodes.length}`;
@@ -927,7 +984,11 @@ function renderItem(item) {
927
984
  if (item.toolName) label += ' ' + item.toolName;
928
985
  if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
929
986
  lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
930
- for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line hook', text: l });
987
+ if (item.hookCommand) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">command:</span> ${esc(item.hookCommand)}`, html: true });
988
+ if (item.hookContent) {
989
+ for (const l of truncContent(item.hookContent)) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">content:</span> ${esc(l)}`, html: true });
990
+ }
991
+ for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">stdout:</span> ${esc(l)}`, html: true });
931
992
  break;
932
993
  }
933
994
  case 'diagnostics': {
@@ -965,6 +1026,7 @@ function refreshButtons() {
965
1026
  document.getElementById('btn-tool-output').classList.toggle('on', showToolOutput);
966
1027
  document.getElementById('btn-text').classList.toggle('on', showText);
967
1028
  document.getElementById('btn-hook').classList.toggle('on', showHook);
1029
+ document.getElementById('btn-activity').classList.toggle('on', showActivity);
968
1030
  document.getElementById('btn-autoscroll').classList.toggle('on', autoScroll);
969
1031
  document.getElementById('btn-tree-toggle').classList.toggle('on', showTree);
970
1032
  document.getElementById('btn-autodisco').classList.toggle('on', autoDiscovery);
@@ -1144,6 +1206,7 @@ function toggleText() { showText = !showText; needsFullRender = true;
1144
1206
  visibleDirty = true; renderStream(); refreshButtons(); }
1145
1207
  function toggleHook() { showHook = !showHook; needsFullRender = true;
1146
1208
  visibleDirty = true; renderStream(); refreshButtons(); }
1209
+ function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
1147
1210
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1148
1211
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1149
1212
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
@@ -9,6 +9,7 @@ var StreamItemType = {
9
9
  TOOL_INPUT: 'tool_input',
10
10
  TOOL_OUTPUT: 'tool_output',
11
11
  TEXT: 'text',
12
+ USER_TEXT: 'user_text',
12
13
  TURN_MARKER: 'turn_marker',
13
14
  COMPACT_MARKER: 'compact_marker',
14
15
  HOOK_OUTPUT: 'hook_output',
@@ -27,6 +28,7 @@ function makeItem(overrides = {}) {
27
28
  return {
28
29
  type: '', sessionID: '', agentID: '', agentName: '', timestamp: 0,
29
30
  content: '', toolName: '', toolID: '', durationMs: 0,
31
+ hookContent: '', hookCommand: '',
30
32
  inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0,
31
33
  model: '',
32
34
  ...overrides,
@@ -211,14 +213,18 @@ function parseAttachment(raw, timestamp) {
211
213
  const name = agentDisplayName(raw.agentId);
212
214
  switch (raw.attachment.type) {
213
215
  case 'hook_success': {
214
- const body = raw.attachment.stdout || '';
216
+ const stdout = (raw.attachment.stdout || '').replace(/\n$/, '');
217
+ const stdin = raw.attachment.content || '';
218
+ const hookContent = stdin && stdin !== stdout ? stdin : '';
215
219
  return [makeItem({
216
220
  type: StreamItemType.HOOK_OUTPUT,
217
221
  sessionID: raw.sessionId,
218
222
  agentID: raw.agentId || '',
219
223
  agentName: name,
220
224
  toolName: raw.attachment.hookName || '',
221
- content: body,
225
+ content: stdout,
226
+ hookContent: hookContent,
227
+ hookCommand: raw.attachment.command || '',
222
228
  durationMs: raw.attachment.durationMs || 0,
223
229
  timestamp,
224
230
  })];
@@ -368,7 +374,7 @@ function parseAssistantMessage(raw, timestamp) {
368
374
 
369
375
  function parseUserMessage(raw, timestamp) {
370
376
  const msg = raw.message;
371
- if (!msg || !Array.isArray(msg.content)) return [];
377
+ if (!msg) return [];
372
378
 
373
379
  // Parse toolUseResult for duration
374
380
  let durationMs = 0;
@@ -379,20 +385,54 @@ function parseUserMessage(raw, timestamp) {
379
385
  const items = [];
380
386
  const name = agentDisplayName(raw.agentId);
381
387
 
382
- for (const result of msg.content) {
383
- if (result.type === 'tool_result') {
388
+ // String content user prompt
389
+ if (typeof msg.content === 'string' && msg.content) {
390
+ const text = stripNonUserContent(msg.content);
391
+ if (text) {
384
392
  items.push(makeItem({
385
- type: StreamItemType.TOOL_OUTPUT,
393
+ type: StreamItemType.USER_TEXT,
386
394
  agentID: raw.agentId || '',
387
395
  agentName: name,
388
- content: extractToolResultContent(result.content),
389
- toolID: result.tool_use_id || '',
390
- durationMs,
396
+ content: text,
391
397
  timestamp,
392
398
  }));
393
399
  }
394
400
  }
395
401
 
402
+ // Array content — mixed text blocks and tool_result blocks
403
+ if (Array.isArray(msg.content)) {
404
+ const textParts = [];
405
+ for (const block of msg.content) {
406
+ if (block.type === 'text' && block.text) {
407
+ const text = stripNonUserContent(block.text);
408
+ if (text) textParts.push(text);
409
+ }
410
+ }
411
+ if (textParts.length > 0) {
412
+ items.push(makeItem({
413
+ type: StreamItemType.USER_TEXT,
414
+ agentID: raw.agentId || '',
415
+ agentName: name,
416
+ content: textParts.join('\n'),
417
+ timestamp,
418
+ }));
419
+ }
420
+
421
+ for (const result of msg.content) {
422
+ if (result.type === 'tool_result') {
423
+ items.push(makeItem({
424
+ type: StreamItemType.TOOL_OUTPUT,
425
+ agentID: raw.agentId || '',
426
+ agentName: name,
427
+ content: extractToolResultContent(result.content),
428
+ toolID: result.tool_use_id || '',
429
+ durationMs,
430
+ timestamp,
431
+ }));
432
+ }
433
+ }
434
+ }
435
+
396
436
  return items;
397
437
  }
398
438
 
@@ -413,6 +453,19 @@ function extractToolResultContent(content) {
413
453
  }
414
454
  }
415
455
 
456
+ function stripNonUserContent(text) {
457
+ if (!text) return '';
458
+ // Remove tags that wrap non-user content
459
+ let s = text;
460
+ s = s.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '');
461
+ s = s.replace(/<command-name>[\s\S]*?<\/command-name>/g, '');
462
+ s = s.replace(/<command-message>[\s\S]*?<\/command-message>/g, '');
463
+ s = s.replace(/<command-args>[\s\S]*?<\/command-args>/g, '');
464
+ s = s.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '');
465
+ // Trim and return; empty string means no real user content
466
+ return s.trim();
467
+ }
468
+
416
469
  // ============================================================================
417
470
  // Tool Input Formatting
418
471
  // ============================================================================
@@ -497,5 +550,6 @@ module.exports = {
497
550
  formatToolInput,
498
551
  prettyToolName,
499
552
  agentDisplayName,
553
+ stripNonUserContent,
500
554
  MAX_TOOL_INPUT_LENGTH,
501
555
  };