claude-code-watch 0.0.12 → 0.0.15

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.12",
3
+ "version": "0.0.15",
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); }
@@ -194,9 +203,11 @@ body {
194
203
  .stream-line.diag { color: var(--red); }
195
204
  .stream-line.debug { color: var(--gray); }
196
205
  .stream-line.marker { color: var(--dim); }
197
- .stream-line.agent-tag { font-weight: bold; }
206
+ .stream-line.agent-tag { font-weight: bold; display: flex; justify-content: space-between; align-items: baseline; white-space: nowrap; }
198
207
  .stream-line.agent-main { color: var(--blue); }
199
208
  .stream-line.agent-sub { color: var(--magenta); }
209
+ .stream-line.agent-tag .tag-label { flex-shrink: 0; }
210
+ .stream-line.agent-tag .timestamp { font-weight: normal; font-size: 0.85em; color: var(--dim); white-space: nowrap; }
200
211
  .stream-line.separator { color: var(--dim); }
201
212
 
202
213
  /* ── Footer ── */
@@ -247,8 +258,8 @@ body {
247
258
  :root[data-theme="light"] .btn.on:hover { background: var(--purple2); color: #fff; }
248
259
  :root[data-theme="light"] .btn.on:hover::after { background: var(--purple2); color: #fff; }
249
260
  :root[data-theme="light"] .hljs { background: #f0f0f0 !important; }
250
- :root[data-theme="light"] .tree-node:hover { background: rgba(0,0,0,0.06); }
251
- :root[data-theme="light"] .tree-node.selected { background: rgba(124,58,237,0.2); }
261
+ :root[data-theme="light"] .tree-content:hover { background: rgba(0,0,0,0.06); }
262
+ :root[data-theme="light"] .tree-row.selected > .tree-content { background: rgba(124,58,237,0.2); }
252
263
  :root[data-theme="light"] .tree-node .active-dot.off { color: #bbb; }
253
264
  :root[data-theme="light"] #tree-resize-handle:hover,
254
265
  :root[data-theme="light"] #tree-resize-handle.active { background: var(--purple); }
@@ -286,6 +297,7 @@ body {
286
297
  <button class="btn btn-icon" onclick="selectAll()" data-tooltip="Show all sessions/agents">⊞</button>
287
298
  <button class="btn btn-icon accent" onclick="soloSelected()" data-tooltip="Solo selected">⊙</button>
288
299
  <button class="btn btn-icon danger" onclick="removeSelectedSession()" data-tooltip="Remove session">✕</button>
300
+ <button class="btn btn-icon on" id="btn-activity" onclick="toggleActivity()" data-tooltip="Toggle activity info">💬</button>
289
301
  <span style="flex:1"></span>
290
302
  <span id="tree-cursor-info" style="font-size:10px;color:var(--dim)"></span>
291
303
  </div>
@@ -355,6 +367,9 @@ class LRUCache {
355
367
  }
356
368
  const seenToolIDs = new LRUCache(5000);
357
369
  const toolNameMap = new LRUCache(2000);
370
+ const agentActivity = new Map(); // "sessionID:agentID" → { toolName, content }
371
+ const taskDescriptions = new Map(); // toolID → description string
372
+ const MAX_DESC_STORE = 200;
358
373
  let filters = new Map();
359
374
 
360
375
  let showThinking = true;
@@ -362,6 +377,7 @@ let showToolInput = true;
362
377
  let showToolOutput = true;
363
378
  let showText = true;
364
379
  let showHook = true;
380
+ let showActivity = true;
365
381
  let autoDiscovery = true;
366
382
 
367
383
  let renderPending = false;
@@ -644,6 +660,20 @@ function pushItem(item) {
644
660
  toolNameMap.set(item.toolID, item.toolName);
645
661
  }
646
662
 
663
+ if (item.type === 'tool_input') {
664
+ // Main 代理不追踪工具调用,只显示用户 prompt
665
+ if (item.agentID) {
666
+ agentActivity.set(item.sessionID + ':' + item.agentID, { toolName: item.toolName || '', content: (item.content || '').slice(0, MAX_DESC_STORE) });
667
+ }
668
+ if (item.toolID) {
669
+ taskDescriptions.set(item.toolID, (item.content || '').slice(0, MAX_DESC_STORE));
670
+ }
671
+ }
672
+
673
+ if (item.type === 'user_text') {
674
+ agentActivity.set(item.sessionID + ':' + (item.agentID || ''), { toolName: '', content: (item.content || '').slice(0, MAX_DESC_STORE) });
675
+ }
676
+
647
677
  if (item.toolID) {
648
678
  const key = `${item.toolID}:${item.type}`;
649
679
  if (seenToolIDs.has(key)) return;
@@ -669,6 +699,7 @@ function isItemVisible(item) {
669
699
  case 'tool_output': return showToolOutput;
670
700
  case 'text': return showText;
671
701
  case 'hook_output': return showHook;
702
+ case 'user_text': return false;
672
703
  default: return true;
673
704
  }
674
705
  }
@@ -692,14 +723,23 @@ function rebuildNodes() {
692
723
  );
693
724
  const lastTaskIdx = tasks.length - 1;
694
725
  const hasTasks = tasks.length > 0;
695
- treeNodes.push({ type: a.type, id: a.id, name: a.name, sessionID: s.id, level: 1, isLast: isLastAgent && !hasTasks });
726
+ const actKey = s.id + ':' + a.id;
727
+ const act = agentActivity.get(actKey);
728
+ treeNodes.push({
729
+ type: a.type, id: a.id, name: a.name, sessionID: s.id,
730
+ level: 1, isLast: isLastAgent && !hasTasks,
731
+ activityTool: act ? act.toolName : '',
732
+ activityDesc: act ? act.content : '',
733
+ });
696
734
  for (let ti = 0; ti < tasks.length; ti++) {
697
735
  const t = tasks[ti];
736
+ const tDesc = taskDescriptions.get(t.id);
698
737
  treeNodes.push({
699
738
  type: 'task', id: t.id, name: t.toolName,
700
739
  sessionID: s.id, parentAgentID: t.parentAgentID,
701
740
  outputPath: t.outputPath, isComplete: t.isComplete,
702
741
  level: 2, isLast: isLastAgent && ti === lastTaskIdx,
742
+ description: tDesc || '',
703
743
  });
704
744
  }
705
745
  }
@@ -734,10 +774,12 @@ function getNodeHTML(node, idx) {
734
774
  const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
735
775
  const agentCount = node.agents ? node.agents.filter(a => a.type === 'agent').length : 0;
736
776
  return `<div class="tree-row${selClass ? ' selected' : ''}">
737
- <div class="tree-node" onclick="treeClick(${idx})" data-idx="${idx}">
738
- <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
739
- ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
740
- ${subInfo}
777
+ <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
778
+ <div class="tree-node">
779
+ <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
780
+ ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
781
+ ${subInfo}
782
+ </div>
741
783
  </div>
742
784
  <span class="tree-actions">
743
785
  <button class="btn btn-icon accent" onclick="event.stopPropagation();selectIndex(${idx});soloSelected()" data-tooltip="Solo">⊙</button>
@@ -758,9 +800,19 @@ function getNodeHTML(node, idx) {
758
800
  ctxPct = `<span class="ctx-pct ${cls}">${pct}%</span>`;
759
801
  }
760
802
  const activeDot = ctx && (Date.now() - ctx.lastActivity < 120000) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
803
+ const actIcon = node.type === 'main' ? '🗣' : '⚡';
804
+ const actText = showActivity && (node.activityTool || node.activityDesc)
805
+ ? (node.activityTool && node.activityDesc ? `${node.activityTool}: ${node.activityDesc}` : (node.activityTool || node.activityDesc))
806
+ : '';
807
+ const activityHTML = actText
808
+ ? `<div class="tree-activity">${actIcon} ${esc(actText)}</div>`
809
+ : '';
761
810
  return `<div class="tree-row${selClass ? ' selected' : ''}">
762
- <div class="tree-node${enabled ? '' : ' dim'}" onclick="treeClick(${idx})" data-idx="${idx}">
811
+ <div class="tree-content${enabled ? '' : ' dim'}" onclick="treeClick(${idx})" data-idx="${idx}">
812
+ <div class="tree-node">
763
813
  <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${icon} ${esc(node.name || '')}${ctxPct}
814
+ </div>
815
+ ${activityHTML}
764
816
  </div>
765
817
  <span class="tree-actions">
766
818
  <button class="btn btn-icon accent" onclick="event.stopPropagation();selectIndex(${idx});soloSelected()" data-tooltip="Solo">⊙</button>
@@ -771,9 +823,15 @@ function getNodeHTML(node, idx) {
771
823
 
772
824
  if (node.type === 'task') {
773
825
  const icon = node.isComplete ? '✓' : '⏳';
826
+ const descHTML = showActivity && node.description
827
+ ? `<div class="tree-activity">📋 ${esc(node.description)}</div>`
828
+ : '';
774
829
  return `<div class="tree-row${selClass ? ' selected' : ''}">
775
- <div class="tree-node dim" onclick="treeClick(${idx})" data-idx="${idx}">
830
+ <div class="tree-content dim" onclick="treeClick(${idx})" data-idx="${idx}">
831
+ <div class="tree-node">
776
832
  <span class="tree-prefix">${treePrefix(node)}</span>${icon} ${esc(node.name || 'bg-task')}
833
+ </div>
834
+ ${descHTML}
777
835
  </div>
778
836
  <span class="tree-actions">
779
837
  <button class="btn btn-icon" onclick="event.stopPropagation();selectIndex(${idx});loadBgTask(${idx})" data-tooltip="Load output">▶</button>
@@ -797,7 +855,7 @@ function renderTree() {
797
855
  treeEl.innerHTML = html;
798
856
 
799
857
  // Scroll selected into view
800
- const sel = treeEl.querySelector('.tree-node.selected');
858
+ const sel = treeEl.querySelector('.tree-row.selected');
801
859
  if (sel) sel.scrollIntoView({ block: 'nearest' });
802
860
 
803
861
  treeCursorInfo.textContent = `${treeCursor + 1}/${treeNodes.length}`;
@@ -894,14 +952,15 @@ function renderItem(item) {
894
952
  }
895
953
 
896
954
  const agentName = item.agentName || 'Main';
955
+ const tsHtml = item.timestamp ? `<span class="timestamp">${fmtTimestamp(item.timestamp)}</span>` : '';
897
956
 
898
957
  switch (item.type) {
899
958
  case 'thinking':
900
- lines.push({ cls: agentTagCls, text: agentName + sep + '🧠 Thinking' });
959
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + '🧠 Thinking')}</span>${tsHtml}`, html: true });
901
960
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line thinking', text: l });
902
961
  break;
903
962
  case 'tool_input':
904
- lines.push({ cls: agentTagCls, text: agentName + sep + `🔧 ${item.toolName || ''}` });
963
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + `🔧 ${item.toolName || ''}`)}</span>${tsHtml}`, html: true });
905
964
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-input', text: l });
906
965
  break;
907
966
  case 'tool_output': {
@@ -911,33 +970,33 @@ function renderItem(item) {
911
970
  }
912
971
  let label = tn ? `📤 ${tn} result` : '📤 Output';
913
972
  if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
914
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
973
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
915
974
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-output', text: l });
916
975
  break;
917
976
  }
918
977
  case 'text':
919
- lines.push({ cls: agentTagCls, text: agentName + sep + '💬 Response' });
978
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + '💬 Response')}</span>${tsHtml}`, html: true });
920
979
  lines.push({ cls: 'stream-line text md-content', text: mdRender(item.content), html: true });
921
980
  break;
922
981
  case 'hook_output': {
923
982
  let label = '🪝 Hook';
924
983
  if (item.toolName) label += ' ' + item.toolName;
925
984
  if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
926
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
985
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
927
986
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line hook', text: l });
928
987
  break;
929
988
  }
930
989
  case 'diagnostics': {
931
990
  let label = '⚠ Diagnostics';
932
991
  if (item.toolName) label += ' ' + item.toolName;
933
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
992
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
934
993
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line diag', text: l });
935
994
  break;
936
995
  }
937
996
  case 'debug': {
938
997
  let label = '🔍 Debug';
939
998
  if (item.toolName) label += ' ' + item.toolName;
940
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
999
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
941
1000
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l });
942
1001
  break;
943
1002
  }
@@ -962,6 +1021,7 @@ function refreshButtons() {
962
1021
  document.getElementById('btn-tool-output').classList.toggle('on', showToolOutput);
963
1022
  document.getElementById('btn-text').classList.toggle('on', showText);
964
1023
  document.getElementById('btn-hook').classList.toggle('on', showHook);
1024
+ document.getElementById('btn-activity').classList.toggle('on', showActivity);
965
1025
  document.getElementById('btn-autoscroll').classList.toggle('on', autoScroll);
966
1026
  document.getElementById('btn-tree-toggle').classList.toggle('on', showTree);
967
1027
  document.getElementById('btn-autodisco').classList.toggle('on', autoDiscovery);
@@ -1141,6 +1201,7 @@ function toggleText() { showText = !showText; needsFullRender = true;
1141
1201
  visibleDirty = true; renderStream(); refreshButtons(); }
1142
1202
  function toggleHook() { showHook = !showHook; needsFullRender = true;
1143
1203
  visibleDirty = true; renderStream(); refreshButtons(); }
1204
+ function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
1144
1205
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1145
1206
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1146
1207
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
@@ -1272,6 +1333,15 @@ function fmtDur(ms) {
1272
1333
  return `(${(ms / 60000).toFixed(1)}m)`;
1273
1334
  }
1274
1335
 
1336
+ function fmtTimestamp(ts) {
1337
+ if (!ts) return '';
1338
+ const d = ts instanceof Date ? ts : new Date(ts);
1339
+ if (isNaN(d.getTime())) return '';
1340
+ const pad = (n, len) => String(n).padStart(len, '0');
1341
+ const ms = pad(d.getMilliseconds(), 3);
1342
+ return `${pad(d.getFullYear(),4)}-${pad(d.getMonth()+1,2)}-${pad(d.getDate(),2)} ${pad(d.getHours(),2)}:${pad(d.getMinutes(),2)}:${pad(d.getSeconds(),2)}.${ms}`;
1343
+ }
1344
+
1275
1345
  function fmtTok(n) {
1276
1346
  if (!n) return '0';
1277
1347
  if (n < 1000) return String(n);
@@ -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',
@@ -368,7 +369,7 @@ function parseAssistantMessage(raw, timestamp) {
368
369
 
369
370
  function parseUserMessage(raw, timestamp) {
370
371
  const msg = raw.message;
371
- if (!msg || !Array.isArray(msg.content)) return [];
372
+ if (!msg) return [];
372
373
 
373
374
  // Parse toolUseResult for duration
374
375
  let durationMs = 0;
@@ -379,20 +380,54 @@ function parseUserMessage(raw, timestamp) {
379
380
  const items = [];
380
381
  const name = agentDisplayName(raw.agentId);
381
382
 
382
- for (const result of msg.content) {
383
- if (result.type === 'tool_result') {
383
+ // String content user prompt
384
+ if (typeof msg.content === 'string' && msg.content) {
385
+ const text = stripNonUserContent(msg.content);
386
+ if (text) {
384
387
  items.push(makeItem({
385
- type: StreamItemType.TOOL_OUTPUT,
388
+ type: StreamItemType.USER_TEXT,
386
389
  agentID: raw.agentId || '',
387
390
  agentName: name,
388
- content: extractToolResultContent(result.content),
389
- toolID: result.tool_use_id || '',
390
- durationMs,
391
+ content: text,
391
392
  timestamp,
392
393
  }));
393
394
  }
394
395
  }
395
396
 
397
+ // Array content — mixed text blocks and tool_result blocks
398
+ if (Array.isArray(msg.content)) {
399
+ const textParts = [];
400
+ for (const block of msg.content) {
401
+ if (block.type === 'text' && block.text) {
402
+ const text = stripNonUserContent(block.text);
403
+ if (text) textParts.push(text);
404
+ }
405
+ }
406
+ if (textParts.length > 0) {
407
+ items.push(makeItem({
408
+ type: StreamItemType.USER_TEXT,
409
+ agentID: raw.agentId || '',
410
+ agentName: name,
411
+ content: textParts.join('\n'),
412
+ timestamp,
413
+ }));
414
+ }
415
+
416
+ for (const result of msg.content) {
417
+ if (result.type === 'tool_result') {
418
+ items.push(makeItem({
419
+ type: StreamItemType.TOOL_OUTPUT,
420
+ agentID: raw.agentId || '',
421
+ agentName: name,
422
+ content: extractToolResultContent(result.content),
423
+ toolID: result.tool_use_id || '',
424
+ durationMs,
425
+ timestamp,
426
+ }));
427
+ }
428
+ }
429
+ }
430
+
396
431
  return items;
397
432
  }
398
433
 
@@ -413,6 +448,19 @@ function extractToolResultContent(content) {
413
448
  }
414
449
  }
415
450
 
451
+ function stripNonUserContent(text) {
452
+ if (!text) return '';
453
+ // Remove tags that wrap non-user content
454
+ let s = text;
455
+ s = s.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '');
456
+ s = s.replace(/<command-name>[\s\S]*?<\/command-name>/g, '');
457
+ s = s.replace(/<command-message>[\s\S]*?<\/command-message>/g, '');
458
+ s = s.replace(/<command-args>[\s\S]*?<\/command-args>/g, '');
459
+ s = s.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '');
460
+ // Trim and return; empty string means no real user content
461
+ return s.trim();
462
+ }
463
+
416
464
  // ============================================================================
417
465
  // Tool Input Formatting
418
466
  // ============================================================================
@@ -497,5 +545,6 @@ module.exports = {
497
545
  formatToolInput,
498
546
  prettyToolName,
499
547
  agentDisplayName,
548
+ stripNonUserContent,
500
549
  MAX_TOOL_INPUT_LENGTH,
501
550
  };