claude-code-watch 0.0.19 → 0.0.20
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 +8 -4
- package/README.zh-CN.md +5 -1
- package/bin/claude-watch.js +9 -5
- package/package.json +1 -1
- package/public/index.html +167 -83
- package/src/parser/parser.js +2 -10
- package/src/server/server.js +29 -3
- package/src/watcher/watcher.js +24 -30
package/README.md
CHANGED
|
@@ -9,8 +9,12 @@ Claude Code writes detailed JSONL logs under `~/.claude/projects/` as it works
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Real-time streaming** — thinking, tool calls, tool results, and text responses appear as they happen
|
|
12
|
-
- **Multi-session** — watch all Claude Code sessions
|
|
12
|
+
- **Multi-session** — watch all Claude Code sessions in a tree view, grouped by date; active sessions stay flat regardless of age
|
|
13
13
|
- **Subagent tracking** — see subagent activity nested under their parent session
|
|
14
|
+
- **Color-coded session tags** — each session gets a unique colored hash prefix for easy visual distinction
|
|
15
|
+
- **Agent-level activity** — active dots on agent/main nodes (not just sessions) with configurable thresholds
|
|
16
|
+
- **Session hiding** — remove unwanted sessions; hidden state persists for 24h via localStorage
|
|
17
|
+
- **Code block copy** — one-click copy button on every markdown code block
|
|
14
18
|
- **Token & cost visibility** — tracks input/output/cache tokens per agent, with context window utilization
|
|
15
19
|
- **Filter controls** — toggle thinking, tool input, tool output, hook output, and text visibility independently
|
|
16
20
|
- **Auto-discovery** — automatically picks up new sessions as they start (toggleable)
|
|
@@ -46,7 +50,7 @@ Shorter alias: `cc-watch` (equivalent to `claude-code-watch`).
|
|
|
46
50
|
|
|
47
51
|
OPTIONS:
|
|
48
52
|
-p, --port <port> HTTP port (default: 23000)
|
|
49
|
-
|
|
53
|
+
--host <host> Bind host (default: 127.0.0.1)
|
|
50
54
|
-s <ID> Watch a specific session by ID
|
|
51
55
|
-n Start from newest (skip history, live only)
|
|
52
56
|
-l [N] List recent sessions (default 10) and exit
|
|
@@ -57,7 +61,7 @@ OPTIONS:
|
|
|
57
61
|
-D Debug: show raw type:subtype for every JSONL line we'd drop
|
|
58
62
|
--poll <ms> Polling interval in milliseconds (default: 500)
|
|
59
63
|
-v Show version
|
|
60
|
-
--help
|
|
64
|
+
-h, --help Show this help
|
|
61
65
|
```
|
|
62
66
|
|
|
63
67
|
### Examples
|
|
@@ -76,7 +80,7 @@ claude-code-watch -s abc123-def456
|
|
|
76
80
|
claude-code-watch -n
|
|
77
81
|
|
|
78
82
|
# Custom port and host
|
|
79
|
-
claude-code-watch -p 8080
|
|
83
|
+
claude-code-watch -p 8080 --host 0.0.0.0
|
|
80
84
|
|
|
81
85
|
# Limit tree to 5 most recent sessions, auto-collapse after 2m of inactivity
|
|
82
86
|
claude-code-watch -m 5 -c 2m
|
package/README.zh-CN.md
CHANGED
|
@@ -19,8 +19,12 @@ Claude Code 在运行时会将详细的 JSONL 日志写入 `~/.claude/projects/`
|
|
|
19
19
|
## 主要功能
|
|
20
20
|
|
|
21
21
|
- **实时流式传输** — 思考过程、工具调用/结果、文本响应实时呈现
|
|
22
|
-
- **多会话监视** — 同时查看所有活跃的 Claude Code
|
|
22
|
+
- **多会话监视** — 同时查看所有活跃的 Claude Code 会话,活跃会话不进历史分组
|
|
23
23
|
- **子代理追踪** — 在父会话下嵌套显示子代理活动
|
|
24
|
+
- **会话彩色标识** — 每个会话显示独特的彩色 hash 前缀,多会话一目了然
|
|
25
|
+
- **代理级活跃指示** — 绿点细化到 agent/main 级别,不只看会话整体
|
|
26
|
+
- **会话隐藏** — 移除不关心的会话,隐藏状态 24h 内持久化
|
|
27
|
+
- **代码块一键复制** — 代码块 header 右侧复制按钮,点击即复制
|
|
24
28
|
- **Token/成本追踪** — 每个代理的输入/输出/缓存 token 及上下文窗口利用率
|
|
25
29
|
- **过滤控制** — 独立切换 thinking、工具输入/输出、hook 输出、文本的可见性
|
|
26
30
|
- **自动发现** — 新会话启动时自动纳入监控
|
package/bin/claude-watch.js
CHANGED
|
@@ -23,19 +23,19 @@ USAGE:
|
|
|
23
23
|
|
|
24
24
|
OPTIONS:
|
|
25
25
|
-p, --port <port> HTTP port (default: 23000)
|
|
26
|
-
|
|
26
|
+
--host <host> Bind host (default: 127.0.0.1)
|
|
27
27
|
-s <ID> Watch a specific session by ID
|
|
28
28
|
-n Start from newest (skip history, live only)
|
|
29
29
|
-l [N] List recent sessions (default 10) and exit
|
|
30
30
|
-a [N] List active sessions (default all) and exit
|
|
31
|
-
-w <dur> Active window duration (default
|
|
31
|
+
-w <dur> Active window duration (default 24h, e.g. 30s, 2m, 10m)
|
|
32
32
|
-m <N> Max sessions to show in tree (default 0=unlimited)
|
|
33
33
|
-c <dur> Auto-collapse sessions inactive for this duration (e.g. 2m)
|
|
34
34
|
-D Debug: show raw type:subtype for every JSONL line we'd drop
|
|
35
35
|
--poll <ms> Polling interval in milliseconds (default: 500)
|
|
36
36
|
--no-open Do not auto-open browser on start
|
|
37
37
|
-v Show version
|
|
38
|
-
--help Show this help
|
|
38
|
+
-h, --help Show this help
|
|
39
39
|
|
|
40
40
|
ENVIRONMENT:
|
|
41
41
|
CLAUDE_HOME Override Claude config directory (default: ~/.claude)
|
|
@@ -80,13 +80,17 @@ function checkForUpdate() {
|
|
|
80
80
|
console.log(`\n New version available: v${latest} (current: v${VERSION})`);
|
|
81
81
|
console.log(' Updating in background...\n');
|
|
82
82
|
const child = cp.spawn('npm', ['install', '-g', 'claude-code-watch@latest'], {
|
|
83
|
-
stdio: 'ignore',
|
|
83
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
84
84
|
detached: true,
|
|
85
85
|
});
|
|
86
|
+
let stderr = '';
|
|
87
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
86
88
|
child.unref();
|
|
87
89
|
child.on('exit', (code) => {
|
|
88
90
|
if (code === 0) {
|
|
89
91
|
console.log(` Updated to v${latest}. Changes take effect on next start.\n`);
|
|
92
|
+
} else {
|
|
93
|
+
console.error(` Update failed (exit code ${code}): ${stderr.trim()}\n`);
|
|
90
94
|
}
|
|
91
95
|
});
|
|
92
96
|
}
|
|
@@ -168,7 +172,6 @@ async function main() {
|
|
|
168
172
|
options.port = pv;
|
|
169
173
|
break;
|
|
170
174
|
}
|
|
171
|
-
case '-h':
|
|
172
175
|
case '--host':
|
|
173
176
|
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
174
177
|
console.error(`Error: ${arg} requires a host address`);
|
|
@@ -219,6 +222,7 @@ async function main() {
|
|
|
219
222
|
case '-v':
|
|
220
223
|
showVersion = true;
|
|
221
224
|
break;
|
|
225
|
+
case '-h':
|
|
222
226
|
case '--help':
|
|
223
227
|
showHelp = true;
|
|
224
228
|
break;
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -170,12 +170,14 @@ body {
|
|
|
170
170
|
cursor: pointer; white-space: nowrap; gap: 4px;
|
|
171
171
|
overflow: hidden;
|
|
172
172
|
}
|
|
173
|
-
.tree-prefix { color: var(--dim); font-size: 12px; flex-shrink: 0; letter-spacing: 0; font-family: monospace; }
|
|
173
|
+
.tree-prefix { color: var(--dim); font-size: 12px; flex-shrink: 0; letter-spacing: 0; font-family: monospace; white-space: pre; }
|
|
174
174
|
.tree-activity {
|
|
175
|
-
font-size: 10px; color: var(--dim); white-space:
|
|
176
|
-
overflow: hidden;
|
|
177
|
-
padding: 0 2px 2px;
|
|
178
|
-
|
|
175
|
+
font-size: 10px; color: var(--dim); white-space: pre;
|
|
176
|
+
overflow: hidden; line-height: 1.2;
|
|
177
|
+
padding: 0 2px 2px; cursor: pointer;
|
|
178
|
+
}
|
|
179
|
+
.tree-activity .act-text {
|
|
180
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
179
181
|
}
|
|
180
182
|
.tree-node .ctx-pct { font-size: 10px; margin-left: 4px; flex-shrink: 0; }
|
|
181
183
|
.tree-node .ctx-pct.warn { color: var(--yellow); }
|
|
@@ -184,6 +186,11 @@ body {
|
|
|
184
186
|
.tree-node .active-dot.on { color: var(--green); text-shadow: 0 0 6px var(--green); }
|
|
185
187
|
.tree-node .active-dot.off { color: #555; opacity: 1; }
|
|
186
188
|
|
|
189
|
+
.tree-node .session-prefix {
|
|
190
|
+
background: rgba(255,255,255,0.08);
|
|
191
|
+
padding: 0 3px; border-radius: 3px; flex-shrink: 0; font-family: monospace;
|
|
192
|
+
letter-spacing: 0.5px; vertical-align: middle; font-weight: 600;
|
|
193
|
+
}
|
|
187
194
|
.tree-actions { display: none; gap: 2px; padding-right: 4px; }
|
|
188
195
|
.tree-row:hover .tree-actions { display: flex; }
|
|
189
196
|
.tree-row.selected>.tree-actions { display: flex; }
|
|
@@ -257,6 +264,8 @@ body {
|
|
|
257
264
|
.code-block-wrapper { margin: 8px 0; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
|
|
258
265
|
.code-block-header { display: flex; align-items: center; justify-content: space-between; padding: 4px 12px; background: var(--bg3); font-size: 11px; color: var(--dim); }
|
|
259
266
|
.code-block-header .lang-tag { color: var(--blue); font-weight: bold; }
|
|
267
|
+
.code-block-header .copy-btn { cursor: pointer; opacity: 0.5; transition: opacity 0.2s; font-size: 11px; }
|
|
268
|
+
.code-block-header .copy-btn:hover { opacity: 1; }
|
|
260
269
|
.code-block-wrapper pre { margin: 0; padding: 12px; overflow-x: auto; font-size: 12px; line-height: 1.5; }
|
|
261
270
|
.code-block-wrapper pre code { font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace; font-size: 12px; }
|
|
262
271
|
|
|
@@ -377,12 +386,13 @@ class LRUCache {
|
|
|
377
386
|
get(key) { if (!this.map.has(key)) return undefined; const v = this.map.get(key); this.map.delete(key); this.map.set(key, v); return v; }
|
|
378
387
|
set(key, val) { if (this.map.has(key)) this.map.delete(key); this.map.set(key, val); if (this.map.size > this.max) { const oldest = this.map.keys().next().value; this.map.delete(oldest); } }
|
|
379
388
|
}
|
|
380
|
-
const seenToolIDs = new LRUCache(
|
|
389
|
+
const seenToolIDs = new LRUCache(20000);
|
|
381
390
|
const toolNameMap = new LRUCache(2000);
|
|
382
391
|
const agentActivity = new Map(); // "sessionID:agentID" → { toolName, content }
|
|
383
392
|
const taskDescriptions = new Map(); // toolID → description string
|
|
384
393
|
const MAX_DESC_STORE = 200;
|
|
385
394
|
let filters = new Map();
|
|
395
|
+
let visibleFilterCount = 0;
|
|
386
396
|
|
|
387
397
|
let showThinking = true;
|
|
388
398
|
let showToolInput = true;
|
|
@@ -393,6 +403,25 @@ let showActivity = true;
|
|
|
393
403
|
let autoDiscovery = true;
|
|
394
404
|
let appVersion = '';
|
|
395
405
|
|
|
406
|
+
const HIDDEN_KEY = 'claude-watch-hidden';
|
|
407
|
+
function loadHiddenSessions() {
|
|
408
|
+
try {
|
|
409
|
+
const data = JSON.parse(localStorage.getItem(HIDDEN_KEY) || '{}');
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
for (const [id, ts] of Object.entries(data)) {
|
|
412
|
+
if (now - ts < 24 * 60 * 60 * 1000) hiddenSessionIDs.add(id);
|
|
413
|
+
}
|
|
414
|
+
_saveHiddenSessions();
|
|
415
|
+
} catch {}
|
|
416
|
+
}
|
|
417
|
+
function _saveHiddenSessions() {
|
|
418
|
+
const data = {};
|
|
419
|
+
for (const id of hiddenSessionIDs) data[id] = Date.now();
|
|
420
|
+
localStorage.setItem(HIDDEN_KEY, JSON.stringify(data));
|
|
421
|
+
}
|
|
422
|
+
const hiddenSessionIDs = new Set();
|
|
423
|
+
loadHiddenSessions();
|
|
424
|
+
|
|
396
425
|
let renderPending = false;
|
|
397
426
|
|
|
398
427
|
let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0;
|
|
@@ -416,6 +445,8 @@ const MAX_ITEMS = 9999;
|
|
|
416
445
|
const MAX_LINES = 50;
|
|
417
446
|
let renderedItemCount = 0;
|
|
418
447
|
let needsFullRender = true;
|
|
448
|
+
let treeDirty = true;
|
|
449
|
+
let lastTreeCursor = -1;
|
|
419
450
|
|
|
420
451
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
421
452
|
// Markdown renderer (marked + highlight.js)
|
|
@@ -438,12 +469,22 @@ mdRenderer.code = function (codeOrObj, langOrEsc) {
|
|
|
438
469
|
}
|
|
439
470
|
const langTag = lang ? `<span class="lang-tag">${esc(lang)}</span>` : '';
|
|
440
471
|
return `<div class="code-block-wrapper">
|
|
441
|
-
<div class="code-block-header">${langTag}<span
|
|
472
|
+
<div class="code-block-header">${langTag}<span class="copy-btn" onclick="copyCode(this)">⎘</span></div>
|
|
442
473
|
<pre><code>${highlighted}</code></pre>
|
|
443
474
|
</div>`;
|
|
444
475
|
};
|
|
445
476
|
marked.setOptions({ renderer: mdRenderer, breaks: true, gfm: true });
|
|
446
477
|
|
|
478
|
+
function copyCode(btn) {
|
|
479
|
+
const wrapper = btn.closest('.code-block-wrapper');
|
|
480
|
+
const code = wrapper ? wrapper.querySelector('code') : null;
|
|
481
|
+
if (!code) return;
|
|
482
|
+
navigator.clipboard.writeText(code.textContent).then(() => {
|
|
483
|
+
btn.innerHTML = '✓';
|
|
484
|
+
setTimeout(() => { btn.innerHTML = '⎘'; }, 1500);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
447
488
|
function mdRender(text) {
|
|
448
489
|
try {
|
|
449
490
|
return DOMPurify.sanitize(marked.parse(text));
|
|
@@ -516,7 +557,7 @@ function handleMessage(msg) {
|
|
|
516
557
|
case 'newBackgroundTask': handleNewBgTask(msg.payload); break;
|
|
517
558
|
case 'sessionRemoved': handleSessionRemoved(msg.payload); break;
|
|
518
559
|
case 'autoDiscoveryChanged': autoDiscovery = msg.payload.enabled; scheduleRender(); break;
|
|
519
|
-
case 'context': contextData = msg.payload; updateTreeDots(); break;
|
|
560
|
+
case 'context': contextData = msg.payload; updateTreeDots(); refreshButtons(); break;
|
|
520
561
|
case 'config':
|
|
521
562
|
if (msg.payload.version) appVersion = msg.payload.version;
|
|
522
563
|
if (msg.payload.collapseAfter > 0 && !collapseTimer) {
|
|
@@ -538,6 +579,7 @@ function sendCmd(action, extra = {}) {
|
|
|
538
579
|
function handleSnapshot(payload) {
|
|
539
580
|
autoDiscovery = payload.autoDiscovery;
|
|
540
581
|
for (const s of (payload.sessions || [])) {
|
|
582
|
+
if (hiddenSessionIDs.has(s.id)) continue;
|
|
541
583
|
let session = sessionsMap.get(s.id);
|
|
542
584
|
if (!session) {
|
|
543
585
|
session = {
|
|
@@ -566,21 +608,26 @@ function handleSnapshot(payload) {
|
|
|
566
608
|
}
|
|
567
609
|
}
|
|
568
610
|
}
|
|
611
|
+
// Initialize agentActivity from server-computed lastActivities (survives history skip)
|
|
612
|
+
for (const [key, val] of Object.entries(payload.lastActivities || {})) {
|
|
613
|
+
agentActivity.set(key, val);
|
|
614
|
+
}
|
|
569
615
|
updateFilters();
|
|
570
616
|
rebuildNodes();
|
|
571
617
|
needsFullRender = true;
|
|
572
618
|
visibleDirty = true;
|
|
573
|
-
|
|
619
|
+
// Don't render tree yet — wait for itemBatch to fill title/model/taskDescriptions
|
|
574
620
|
}
|
|
575
621
|
|
|
576
622
|
function handleNewSession(payload) {
|
|
623
|
+
if (hiddenSessionIDs.has(payload.sessionID)) return;
|
|
577
624
|
if (sessionsMap.has(payload.sessionID)) return;
|
|
578
625
|
const session = {
|
|
579
626
|
id: payload.sessionID, projectPath: payload.projectPath,
|
|
580
627
|
title: '', folder: folderName(payload.projectPath), model: '',
|
|
581
628
|
agents: [{ id: '', name: 'Main', type: 'main' }],
|
|
582
629
|
tasks: [], collapsed: false, pinned: false,
|
|
583
|
-
lastActivity: Date.now(),
|
|
630
|
+
lastActivity: payload.birthtimeMs || Date.now(),
|
|
584
631
|
birthtimeMs: payload.birthtimeMs || 0,
|
|
585
632
|
};
|
|
586
633
|
sessions.push(session);
|
|
@@ -589,7 +636,7 @@ function handleNewSession(payload) {
|
|
|
589
636
|
rebuildNodes();
|
|
590
637
|
needsFullRender = true;
|
|
591
638
|
visibleDirty = true;
|
|
592
|
-
|
|
639
|
+
scheduleRender();
|
|
593
640
|
}
|
|
594
641
|
|
|
595
642
|
function handleNewAgent(payload) {
|
|
@@ -604,7 +651,7 @@ function handleNewAgent(payload) {
|
|
|
604
651
|
rebuildNodes();
|
|
605
652
|
needsFullRender = true;
|
|
606
653
|
visibleDirty = true;
|
|
607
|
-
|
|
654
|
+
scheduleRender();
|
|
608
655
|
}
|
|
609
656
|
|
|
610
657
|
function handleNewBgTask(payload) {
|
|
@@ -616,7 +663,7 @@ function handleNewBgTask(payload) {
|
|
|
616
663
|
isComplete: payload.isComplete,
|
|
617
664
|
});
|
|
618
665
|
rebuildNodes();
|
|
619
|
-
|
|
666
|
+
scheduleRender();
|
|
620
667
|
}
|
|
621
668
|
|
|
622
669
|
function handleSessionRemoved(payload) {
|
|
@@ -629,7 +676,7 @@ function handleSessionRemoved(payload) {
|
|
|
629
676
|
rebuildNodes();
|
|
630
677
|
needsFullRender = true;
|
|
631
678
|
visibleDirty = true;
|
|
632
|
-
|
|
679
|
+
scheduleRender();
|
|
633
680
|
}
|
|
634
681
|
|
|
635
682
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -645,7 +692,7 @@ function handleItem(item) {
|
|
|
645
692
|
}
|
|
646
693
|
// Update activity
|
|
647
694
|
const s = sessionsMap.get(item.sessionID);
|
|
648
|
-
if (s) s.lastActivity =
|
|
695
|
+
if (s) s.lastActivity = itemTime(item);
|
|
649
696
|
pushItem(item);
|
|
650
697
|
scheduleRender();
|
|
651
698
|
}
|
|
@@ -658,13 +705,15 @@ function handleItemBatch(items) {
|
|
|
658
705
|
continue;
|
|
659
706
|
}
|
|
660
707
|
const s = sessionsMap.get(item.sessionID);
|
|
661
|
-
if (s) s.lastActivity =
|
|
708
|
+
if (s) s.lastActivity = itemTime(item);
|
|
662
709
|
pushItem(item);
|
|
663
710
|
}
|
|
711
|
+
rebuildNodes();
|
|
664
712
|
scheduleRender();
|
|
665
713
|
}
|
|
666
714
|
|
|
667
715
|
function pushItem(item) {
|
|
716
|
+
if (hiddenSessionIDs.has(item.sessionID)) return;
|
|
668
717
|
// Token counts are sourced exclusively from server context messages
|
|
669
718
|
// to avoid divergence between frontend accumulation and server tracking
|
|
670
719
|
|
|
@@ -678,7 +727,6 @@ function pushItem(item) {
|
|
|
678
727
|
}
|
|
679
728
|
|
|
680
729
|
if (item.type === 'tool_input') {
|
|
681
|
-
// Main 代理不追踪工具调用,只显示用户 prompt
|
|
682
730
|
if (item.agentID) {
|
|
683
731
|
agentActivity.set(item.sessionID + ':' + item.agentID, { toolName: item.toolName || '', content: (item.content || '').slice(0, MAX_DESC_STORE) });
|
|
684
732
|
}
|
|
@@ -725,20 +773,26 @@ function isItemVisible(item) {
|
|
|
725
773
|
// Tree
|
|
726
774
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
727
775
|
|
|
776
|
+
function idColor(rank) {
|
|
777
|
+
const hue = (rank * 137.508) % 360;
|
|
778
|
+
return `hsl(${hue}, 75%, 60%)`;
|
|
779
|
+
}
|
|
780
|
+
|
|
728
781
|
function rebuildNodes() {
|
|
729
782
|
// Sort sessions by creation time, newest first
|
|
730
783
|
sessions.sort((a, b) => (b.birthtimeMs || 0) - (a.birthtimeMs || 0));
|
|
784
|
+
for (let i = 0; i < sessions.length; i++) sessions[i].colorRank = i;
|
|
731
785
|
|
|
732
786
|
const today = new Date();
|
|
733
787
|
const todayStr = `${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
734
788
|
|
|
735
|
-
const
|
|
789
|
+
const flatSessions = [];
|
|
736
790
|
const olderByDate = new Map(); // dateStr -> [sessions]
|
|
737
791
|
|
|
738
792
|
for (const s of sessions) {
|
|
739
793
|
const dateStr = s.birthtimeMs ? formatTime(s.birthtimeMs).split(' ')[0] : null;
|
|
740
|
-
if (!dateStr || dateStr === todayStr) {
|
|
741
|
-
|
|
794
|
+
if (!dateStr || dateStr === todayStr || isSessionActive(s)) {
|
|
795
|
+
flatSessions.push(s);
|
|
742
796
|
} else {
|
|
743
797
|
if (!olderByDate.has(dateStr)) olderByDate.set(dateStr, []);
|
|
744
798
|
olderByDate.get(dateStr).push(s);
|
|
@@ -764,7 +818,7 @@ function rebuildNodes() {
|
|
|
764
818
|
const act = agentActivity.get(actKey);
|
|
765
819
|
treeNodes.push({
|
|
766
820
|
type: a.type, id: a.id, name: a.name, sessionID: s.id,
|
|
767
|
-
level: 1, isLast: isLastAgent
|
|
821
|
+
level: 1, isLast: isLastAgent,
|
|
768
822
|
activityTool: act ? act.toolName : '',
|
|
769
823
|
activityDesc: act ? act.content : '',
|
|
770
824
|
});
|
|
@@ -775,15 +829,16 @@ function rebuildNodes() {
|
|
|
775
829
|
type: 'task', id: t.id, name: t.toolName,
|
|
776
830
|
sessionID: s.id, parentAgentID: t.parentAgentID,
|
|
777
831
|
outputPath: t.outputPath, isComplete: t.isComplete,
|
|
778
|
-
level: 2, isLast:
|
|
832
|
+
level: 2, isLast: ti === lastTaskIdx,
|
|
833
|
+
parentIsLast: isLastAgent,
|
|
779
834
|
description: tDesc || '',
|
|
780
835
|
});
|
|
781
836
|
}
|
|
782
837
|
}
|
|
783
838
|
}
|
|
784
839
|
|
|
785
|
-
// Today's sessions (expanded)
|
|
786
|
-
for (const s of
|
|
840
|
+
// Today's + active sessions (expanded, not in date folders)
|
|
841
|
+
for (const s of flatSessions) {
|
|
787
842
|
addSessionWithChildren(s, false);
|
|
788
843
|
}
|
|
789
844
|
|
|
@@ -807,14 +862,13 @@ function rebuildNodes() {
|
|
|
807
862
|
}
|
|
808
863
|
}
|
|
809
864
|
|
|
810
|
-
// Mark last session among
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
865
|
+
// Mark last session among flat sessions
|
|
866
|
+
const flatSessionNodes = treeNodes.filter(n => n.type === 'session' && !n.inFolder);
|
|
867
|
+
if (flatSessionNodes.length > 0) flatSessionNodes[flatSessionNodes.length - 1].isLast = true;
|
|
813
868
|
|
|
814
869
|
// Mark last session inside each folder
|
|
815
870
|
for (const dateStr of sortedDates) {
|
|
816
871
|
if (folderCollapsed[dateStr] !== false) continue;
|
|
817
|
-
const folderSessionNodes = treeNodes.filter(n => n.type === 'session' && n.inFolder);
|
|
818
872
|
// Find sessions belonging to this folder
|
|
819
873
|
const thisFolder = [];
|
|
820
874
|
let inThisFolder = false;
|
|
@@ -827,19 +881,19 @@ function rebuildNodes() {
|
|
|
827
881
|
}
|
|
828
882
|
|
|
829
883
|
if (treeCursor >= treeNodes.length) treeCursor = Math.max(0, treeNodes.length - 1);
|
|
884
|
+
treeDirty = true;
|
|
830
885
|
}
|
|
831
886
|
|
|
832
887
|
function treePrefix(node) {
|
|
833
888
|
if (node.level === 0) {
|
|
834
889
|
return node.inFolder ? ' ' : '';
|
|
835
890
|
}
|
|
836
|
-
const branch = node.isLast ? '└──
|
|
837
|
-
if (node.level === 1) return branch;
|
|
838
|
-
// Level 2:
|
|
839
|
-
const
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
return stem + branch;
|
|
891
|
+
const branch = node.isLast ? '└──' : '├──';
|
|
892
|
+
if (node.level === 1) return ' ' + branch;
|
|
893
|
+
// Level 2: use pre-computed parentIsLast from rebuildNodes
|
|
894
|
+
const parentIsLast = node.parentIsLast !== undefined ? node.parentIsLast : true;
|
|
895
|
+
const stem = parentIsLast ? ' ' : '│ ';
|
|
896
|
+
return ' ' + stem + branch;
|
|
843
897
|
}
|
|
844
898
|
|
|
845
899
|
function getNodeHTML(node, idx) {
|
|
@@ -869,7 +923,7 @@ function getNodeHTML(node, idx) {
|
|
|
869
923
|
return `<div class="tree-row tree-row-session${selClass ? ' selected' : ''}">
|
|
870
924
|
<div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
871
925
|
<div class="tree-node">
|
|
872
|
-
<span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
|
|
926
|
+
<span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} <span class="session-prefix" style="color:${idColor(node.colorRank)}">${esc(node.id.split('-')[0].toUpperCase())}</span> ${esc(displayName)}
|
|
873
927
|
${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
|
|
874
928
|
${subInfo}
|
|
875
929
|
${timeHtml}
|
|
@@ -898,8 +952,10 @@ function getNodeHTML(node, idx) {
|
|
|
898
952
|
const actText = showActivity && (node.activityTool || node.activityDesc)
|
|
899
953
|
? (node.activityTool && node.activityDesc ? `${node.activityTool}: ${node.activityDesc}` : (node.activityTool || node.activityDesc))
|
|
900
954
|
: '';
|
|
955
|
+
const indent = treePrefix(node).replace(/[├└]──/, ' ');
|
|
956
|
+
const actPrefix = `<span class="tree-prefix">${indent}</span>`;
|
|
901
957
|
const activityHTML = actText
|
|
902
|
-
? `<div class="tree-activity">${actIcon} ${esc(actText)}</div>`
|
|
958
|
+
? `<div class="tree-activity">${actPrefix}<span class="act-text">${actIcon} ${esc(actText)}</span></div>`
|
|
903
959
|
: '';
|
|
904
960
|
return `<div class="tree-row${selClass ? ' selected' : ''}">
|
|
905
961
|
<div class="tree-content${enabled ? '' : ' dim'}" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
@@ -917,8 +973,10 @@ function getNodeHTML(node, idx) {
|
|
|
917
973
|
|
|
918
974
|
if (node.type === 'task') {
|
|
919
975
|
const icon = node.isComplete ? '✓' : '⏳';
|
|
976
|
+
const taskIndent = treePrefix(node).replace(/[├└]──/, ' ');
|
|
977
|
+
const taskPrefix = `<span class="tree-prefix">${taskIndent}</span>`;
|
|
920
978
|
const descHTML = showActivity && node.description
|
|
921
|
-
? `<div class="tree-activity">📋 ${esc(node.description)}</div>`
|
|
979
|
+
? `<div class="tree-activity">${taskPrefix}<span class="act-text">📋 ${esc(node.description)}</span></div>`
|
|
922
980
|
: '';
|
|
923
981
|
return `<div class="tree-row${selClass ? ' selected' : ''}">
|
|
924
982
|
<div class="tree-content dim" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
@@ -942,11 +1000,25 @@ function renderTree() {
|
|
|
942
1000
|
treeCursorInfo.textContent = '';
|
|
943
1001
|
return;
|
|
944
1002
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1003
|
+
|
|
1004
|
+
const cursorChanged = treeCursor !== lastTreeCursor;
|
|
1005
|
+
if (treeDirty) {
|
|
1006
|
+
let html = '';
|
|
1007
|
+
for (let i = 0; i < treeNodes.length; i++) {
|
|
1008
|
+
html += getNodeHTML(treeNodes[i], i);
|
|
1009
|
+
}
|
|
1010
|
+
treeEl.innerHTML = html;
|
|
1011
|
+
treeDirty = false;
|
|
1012
|
+
} else if (cursorChanged) {
|
|
1013
|
+
const prevSel = treeEl.querySelector('.tree-row.selected');
|
|
1014
|
+
if (prevSel) prevSel.classList.remove('selected');
|
|
1015
|
+
const newContent = treeEl.querySelector('[data-idx="' + treeCursor + '"]');
|
|
1016
|
+
if (newContent) {
|
|
1017
|
+
const row = newContent.closest('.tree-row');
|
|
1018
|
+
if (row) row.classList.add('selected');
|
|
1019
|
+
}
|
|
948
1020
|
}
|
|
949
|
-
|
|
1021
|
+
lastTreeCursor = treeCursor;
|
|
950
1022
|
|
|
951
1023
|
// Scroll selected into view
|
|
952
1024
|
const sel = treeEl.querySelector('.tree-row.selected');
|
|
@@ -964,8 +1036,16 @@ function updateTreeDots() {
|
|
|
964
1036
|
const idx = parseInt(content.getAttribute('data-idx'));
|
|
965
1037
|
if (isNaN(idx)) continue;
|
|
966
1038
|
const node = treeNodes[idx];
|
|
967
|
-
if (!node
|
|
968
|
-
|
|
1039
|
+
if (!node) continue;
|
|
1040
|
+
let active = false;
|
|
1041
|
+
if (node.type === 'session') {
|
|
1042
|
+
active = isSessionActive(node);
|
|
1043
|
+
} else if (node.type === 'main' || node.type === 'agent') {
|
|
1044
|
+
const ctxKey = node.sessionID + ':' + node.id;
|
|
1045
|
+
const ctx = contextData[ctxKey];
|
|
1046
|
+
const threshold = node.type === 'main' ? 600000 : 180000;
|
|
1047
|
+
active = ctx && (now - ctx.lastActivity < threshold);
|
|
1048
|
+
}
|
|
969
1049
|
const newCls = active ? 'active-dot on' : 'active-dot off';
|
|
970
1050
|
const newHTML = active ? '🟢' : '⚪';
|
|
971
1051
|
if (dot.className !== newCls) {
|
|
@@ -977,6 +1057,14 @@ function updateTreeDots() {
|
|
|
977
1057
|
|
|
978
1058
|
const ACTIVE_THRESHOLD = 600000; // 10 minutes
|
|
979
1059
|
|
|
1060
|
+
function itemTime(item) {
|
|
1061
|
+
if (item && item.timestamp) {
|
|
1062
|
+
const ts = item.timestamp instanceof Date ? item.timestamp : new Date(item.timestamp);
|
|
1063
|
+
if (!isNaN(ts.getTime())) return ts.getTime();
|
|
1064
|
+
}
|
|
1065
|
+
return Date.now();
|
|
1066
|
+
}
|
|
1067
|
+
|
|
980
1068
|
function isSessionActive(session) {
|
|
981
1069
|
if (!session) return false;
|
|
982
1070
|
const now = Date.now();
|
|
@@ -1017,8 +1105,8 @@ function renderStream() {
|
|
|
1017
1105
|
let html;
|
|
1018
1106
|
if (lines.length > 0) {
|
|
1019
1107
|
html = lines.map(l => {
|
|
1020
|
-
if (l.html) return `<div class="${l.cls}">${l.text}</div>`;
|
|
1021
|
-
return `<div class="${l.cls}">${esc(l.text)}</div>`;
|
|
1108
|
+
if (l.html) return `<div class="${esc(l.cls)}">${l.text}</div>`;
|
|
1109
|
+
return `<div class="${esc(l.cls)}">${esc(l.text)}</div>`;
|
|
1022
1110
|
}).join('\n');
|
|
1023
1111
|
} else if (streamItems.length > 0) {
|
|
1024
1112
|
html = `<div style="color:#fbbf24;padding:20px;text-align:center">${streamItems.length} items buffered, 0 visible — check toggles or tree selection</div>`;
|
|
@@ -1068,15 +1156,18 @@ function renderItem(item) {
|
|
|
1068
1156
|
}
|
|
1069
1157
|
|
|
1070
1158
|
const agentName = item.agentName || 'Main';
|
|
1159
|
+
const sForColor = sessionsMap.get(item.sessionID);
|
|
1160
|
+
const prefixTag = `<span class="session-prefix" style="color:${idColor(sForColor ? sForColor.colorRank : 0)}">[${esc(item.sessionID.split('-')[0].toUpperCase())}]</span>`;
|
|
1161
|
+
const agentLabel = prefixTag + ' ' + esc(agentName);
|
|
1071
1162
|
const tsHtml = item.timestamp ? `<span class="timestamp">${fmtTimestamp(item.timestamp)}</span>` : '';
|
|
1072
1163
|
|
|
1073
1164
|
switch (item.type) {
|
|
1074
1165
|
case 'thinking':
|
|
1075
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${
|
|
1166
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🧠 Thinking</span>${tsHtml}`, html: true });
|
|
1076
1167
|
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line thinking', text: l });
|
|
1077
1168
|
break;
|
|
1078
1169
|
case 'tool_input':
|
|
1079
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${
|
|
1170
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🔧 ${esc(item.toolName || '')}</span>${tsHtml}`, html: true });
|
|
1080
1171
|
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-input', text: l });
|
|
1081
1172
|
break;
|
|
1082
1173
|
case 'tool_output': {
|
|
@@ -1086,19 +1177,19 @@ function renderItem(item) {
|
|
|
1086
1177
|
}
|
|
1087
1178
|
let label = tn ? `📤 ${tn} result` : '📤 Output';
|
|
1088
1179
|
if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
|
|
1089
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(
|
|
1180
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1090
1181
|
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-output', text: l });
|
|
1091
1182
|
break;
|
|
1092
1183
|
}
|
|
1093
1184
|
case 'text':
|
|
1094
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${
|
|
1185
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}💬 Response</span>${tsHtml}`, html: true });
|
|
1095
1186
|
lines.push({ cls: 'stream-line text md-content', text: mdRender(item.content), html: true });
|
|
1096
1187
|
break;
|
|
1097
1188
|
case 'hook_output': {
|
|
1098
1189
|
let label = '🪝 Hook';
|
|
1099
1190
|
if (item.toolName) label += ' ' + item.toolName;
|
|
1100
1191
|
if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
|
|
1101
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(
|
|
1192
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1102
1193
|
if (item.hookCommand) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">command:</span> ${esc(item.hookCommand)}`, html: true });
|
|
1103
1194
|
if (item.hookContent) {
|
|
1104
1195
|
for (const l of truncContent(item.hookContent)) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">content:</span> ${esc(l)}`, html: true });
|
|
@@ -1109,14 +1200,14 @@ function renderItem(item) {
|
|
|
1109
1200
|
case 'diagnostics': {
|
|
1110
1201
|
let label = '⚠ Diagnostics';
|
|
1111
1202
|
if (item.toolName) label += ' ' + item.toolName;
|
|
1112
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(
|
|
1203
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1113
1204
|
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line diag', text: l });
|
|
1114
1205
|
break;
|
|
1115
1206
|
}
|
|
1116
1207
|
case 'debug': {
|
|
1117
1208
|
let label = '🔍 Debug';
|
|
1118
1209
|
if (item.toolName) label += ' ' + item.toolName;
|
|
1119
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(
|
|
1210
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1120
1211
|
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l });
|
|
1121
1212
|
break;
|
|
1122
1213
|
}
|
|
@@ -1212,7 +1303,10 @@ function toggleNodeVisibility(idx) {
|
|
|
1212
1303
|
const node = treeNodes[idx];
|
|
1213
1304
|
if (!node) return;
|
|
1214
1305
|
const key = node.sessionID + ':' + node.id;
|
|
1215
|
-
|
|
1306
|
+
const wasEnabled = filters.get(key);
|
|
1307
|
+
filters.set(key, !wasEnabled);
|
|
1308
|
+
if (wasEnabled) visibleFilterCount--;
|
|
1309
|
+
else visibleFilterCount++;
|
|
1216
1310
|
renderAll();
|
|
1217
1311
|
}
|
|
1218
1312
|
|
|
@@ -1256,6 +1350,7 @@ function soloSelected() {
|
|
|
1256
1350
|
updateFilters();
|
|
1257
1351
|
} else {
|
|
1258
1352
|
filters.clear();
|
|
1353
|
+
visibleFilterCount = 0;
|
|
1259
1354
|
if (node.type === 'session') {
|
|
1260
1355
|
const session = sessions.find(s => s.id === node.id);
|
|
1261
1356
|
if (session && session.collapsed) {
|
|
@@ -1263,9 +1358,13 @@ function soloSelected() {
|
|
|
1263
1358
|
session.pinned = true;
|
|
1264
1359
|
rebuildNodes();
|
|
1265
1360
|
}
|
|
1266
|
-
for (const a of node.agents)
|
|
1361
|
+
for (const a of node.agents) {
|
|
1362
|
+
filters.set(node.id + ':' + a.id, true);
|
|
1363
|
+
visibleFilterCount++;
|
|
1364
|
+
}
|
|
1267
1365
|
} else if (node.type === 'main' || node.type === 'agent') {
|
|
1268
1366
|
filters.set(node.sessionID + ':' + node.id, true);
|
|
1367
|
+
visibleFilterCount = 1;
|
|
1269
1368
|
}
|
|
1270
1369
|
}
|
|
1271
1370
|
renderAll();
|
|
@@ -1273,23 +1372,17 @@ function soloSelected() {
|
|
|
1273
1372
|
|
|
1274
1373
|
function isSoloed(node) {
|
|
1275
1374
|
if (node.type === 'session') {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
if (filters.get(s.id + ':' + a.id)) return false;
|
|
1280
|
-
}
|
|
1375
|
+
if (visibleFilterCount !== node.agents.length) return false;
|
|
1376
|
+
for (const a of node.agents) {
|
|
1377
|
+
if (!filters.get(node.id + ':' + a.id)) return false;
|
|
1281
1378
|
}
|
|
1282
1379
|
return true;
|
|
1283
1380
|
}
|
|
1284
1381
|
if (node.type === 'main' || node.type === 'agent') {
|
|
1285
1382
|
const key = node.sessionID + ':' + node.id;
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
if ((s.id + ':' + a.id) !== key && filters.get(s.id + ':' + a.id)) return false;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
return true;
|
|
1383
|
+
return visibleFilterCount === 1 && filters.get(key);
|
|
1384
|
+
}
|
|
1385
|
+
return false;
|
|
1293
1386
|
}
|
|
1294
1387
|
return false;
|
|
1295
1388
|
}
|
|
@@ -1307,6 +1400,8 @@ function removeSelectedSession() {
|
|
|
1307
1400
|
else sid = node.sessionID;
|
|
1308
1401
|
if (!sid) return;
|
|
1309
1402
|
if (!confirm(`Remove session ${sid.slice(0, 12)}...?`)) return;
|
|
1403
|
+
hiddenSessionIDs.add(sid);
|
|
1404
|
+
_saveHiddenSessions();
|
|
1310
1405
|
const idx = sessions.findIndex(s => s.id === sid);
|
|
1311
1406
|
if (idx >= 0) {
|
|
1312
1407
|
sessions.splice(idx, 1);
|
|
@@ -1332,7 +1427,7 @@ function toggleText() { showText = !showText; needsFullRender = true;
|
|
|
1332
1427
|
visibleDirty = true; renderStream(); refreshButtons(); }
|
|
1333
1428
|
function toggleHook() { showHook = !showHook; needsFullRender = true;
|
|
1334
1429
|
visibleDirty = true; renderStream(); refreshButtons(); }
|
|
1335
|
-
function toggleActivity() { showActivity = !showActivity; rebuildNodes();
|
|
1430
|
+
function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
|
|
1336
1431
|
function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
|
|
1337
1432
|
function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
|
|
1338
1433
|
function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
|
|
@@ -1406,10 +1501,8 @@ function applyCollapsePolicy(duration) {
|
|
|
1406
1501
|
function startActiveRefresh() {
|
|
1407
1502
|
if (activeRefreshTimer) clearInterval(activeRefreshTimer);
|
|
1408
1503
|
activeRefreshTimer = setInterval(() => {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
renderTree();
|
|
1412
|
-
if (treeEl.innerHTML !== prevHTML) renderAll();
|
|
1504
|
+
updateTreeDots();
|
|
1505
|
+
refreshButtons();
|
|
1413
1506
|
}, 15000);
|
|
1414
1507
|
}
|
|
1415
1508
|
|
|
@@ -1430,9 +1523,11 @@ streamEl.addEventListener('scroll', () => {
|
|
|
1430
1523
|
|
|
1431
1524
|
function updateFilters() {
|
|
1432
1525
|
filters.clear();
|
|
1526
|
+
visibleFilterCount = 0;
|
|
1433
1527
|
for (const s of sessions) {
|
|
1434
1528
|
for (const a of s.agents) {
|
|
1435
1529
|
filters.set(s.id + ':' + a.id, true);
|
|
1530
|
+
visibleFilterCount++;
|
|
1436
1531
|
}
|
|
1437
1532
|
}
|
|
1438
1533
|
}
|
|
@@ -1496,17 +1591,6 @@ function renderAll() {
|
|
|
1496
1591
|
}
|
|
1497
1592
|
|
|
1498
1593
|
function scheduleRender() {
|
|
1499
|
-
if (!renderPending) {
|
|
1500
|
-
renderPending = true;
|
|
1501
|
-
requestAnimationFrame(() => {
|
|
1502
|
-
renderPending = false;
|
|
1503
|
-
renderStream();
|
|
1504
|
-
refreshButtons();
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
function scheduleTreeRender() {
|
|
1510
1594
|
if (!renderPending) {
|
|
1511
1595
|
renderPending = true;
|
|
1512
1596
|
requestAnimationFrame(() => {
|
package/src/parser/parser.js
CHANGED
|
@@ -455,15 +455,7 @@ function extractToolResultContent(content) {
|
|
|
455
455
|
|
|
456
456
|
function stripNonUserContent(text) {
|
|
457
457
|
if (!text) return '';
|
|
458
|
-
|
|
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();
|
|
458
|
+
return text.replace(/<(?:local-command-caveat|command-name|command-message|command-args|local-command-stdout)>[\s\S]*?<\/(?:local-command-caveat|command-name|command-message|command-args|local-command-stdout)>/g, '').trim();
|
|
467
459
|
}
|
|
468
460
|
|
|
469
461
|
// ============================================================================
|
|
@@ -483,7 +475,7 @@ function formatToolInput(toolName, input) {
|
|
|
483
475
|
|
|
484
476
|
switch (toolName) {
|
|
485
477
|
case 'Bash':
|
|
486
|
-
if (inp.description) return truncate(`${inp.command}\n # ${inp.description}`);
|
|
478
|
+
if (inp.description) return truncate(`${inp.command || ''}\n # ${inp.description}`);
|
|
487
479
|
return truncate(inp.command || '');
|
|
488
480
|
case 'Read':
|
|
489
481
|
return inp.file_path || '';
|
package/src/server/server.js
CHANGED
|
@@ -41,6 +41,7 @@ class DashboardServer {
|
|
|
41
41
|
this.server = null;
|
|
42
42
|
this.wss = null;
|
|
43
43
|
this._heartbeatTimer = null;
|
|
44
|
+
this._allowedPrefix = null;
|
|
44
45
|
|
|
45
46
|
setDebugAll(options.debugAll || false);
|
|
46
47
|
this.debugAll = options.debugAll || false;
|
|
@@ -50,6 +51,14 @@ class DashboardServer {
|
|
|
50
51
|
return sessionID + ':' + (agentID || '');
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
async _getAllowedPrefix() {
|
|
55
|
+
if (!this._allowedPrefix) {
|
|
56
|
+
const homeReal = await fs.promises.realpath(os.homedir());
|
|
57
|
+
this._allowedPrefix = path.join(homeReal, '.claude', 'projects');
|
|
58
|
+
}
|
|
59
|
+
return this._allowedPrefix;
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
itemTime(item) {
|
|
54
63
|
if (item.timestamp) {
|
|
55
64
|
const ts = item.timestamp instanceof Date ? item.timestamp : new Date(item.timestamp);
|
|
@@ -207,8 +216,7 @@ class DashboardServer {
|
|
|
207
216
|
let realPath;
|
|
208
217
|
let allowedPrefix;
|
|
209
218
|
try {
|
|
210
|
-
|
|
211
|
-
allowedPrefix = path.join(homeReal, '.claude', 'projects');
|
|
219
|
+
allowedPrefix = await this._getAllowedPrefix();
|
|
212
220
|
realPath = await fs.promises.realpath(resolved);
|
|
213
221
|
if (!realPath.startsWith(allowedPrefix)) {
|
|
214
222
|
this.sendJSON(res, { error: 'Access denied' }, 403);
|
|
@@ -302,9 +310,20 @@ class DashboardServer {
|
|
|
302
310
|
isComplete: t.isComplete,
|
|
303
311
|
})),
|
|
304
312
|
}));
|
|
313
|
+
// Compute last activity per agent from itemBuffer (handles skipped history)
|
|
314
|
+
const lastActivities = {};
|
|
315
|
+
for (const item of this.itemBuffer) {
|
|
316
|
+
const actKey = item.sessionID + ':' + (item.agentID || '');
|
|
317
|
+
if (item.type === 'user_text') {
|
|
318
|
+
lastActivities[actKey] = { toolName: '', content: (item.content || '').slice(0, 200) };
|
|
319
|
+
} else if (item.type === 'tool_input' && item.agentID) {
|
|
320
|
+
lastActivities[actKey] = { toolName: item.toolName || '', content: (item.content || '').slice(0, 200) };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
305
323
|
this.send(ws, 'snapshot', {
|
|
306
324
|
sessions,
|
|
307
325
|
autoDiscovery: this.watcher.isAutoDiscoveryEnabled(),
|
|
326
|
+
lastActivities,
|
|
308
327
|
});
|
|
309
328
|
}
|
|
310
329
|
|
|
@@ -330,6 +349,7 @@ class DashboardServer {
|
|
|
330
349
|
}
|
|
331
350
|
});
|
|
332
351
|
|
|
352
|
+
const FLUSH_BATCH_LIMIT = 50;
|
|
333
353
|
w.on('item', (item) => {
|
|
334
354
|
this.itemBuffer.push(item);
|
|
335
355
|
if (this.itemBuffer.length > MAX_ITEM_BUFFER) {
|
|
@@ -337,7 +357,13 @@ class DashboardServer {
|
|
|
337
357
|
}
|
|
338
358
|
this.updateContext(item);
|
|
339
359
|
this._pendingItems.push(item);
|
|
340
|
-
if (
|
|
360
|
+
if (this._pendingItems.length >= FLUSH_BATCH_LIMIT) {
|
|
361
|
+
// Batch size hit limit — flush immediately
|
|
362
|
+
if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
|
|
363
|
+
const batch = this._pendingItems;
|
|
364
|
+
this._pendingItems = [];
|
|
365
|
+
this.broadcast('itemBatch', batch);
|
|
366
|
+
} else if (!this._flushTimer) {
|
|
341
367
|
this._flushTimer = setTimeout(() => {
|
|
342
368
|
this._flushTimer = null;
|
|
343
369
|
const batch = this._pendingItems;
|
package/src/watcher/watcher.js
CHANGED
|
@@ -32,11 +32,13 @@ function getClaudeProjectsDir() {
|
|
|
32
32
|
return path.join(os.homedir(), '.claude', 'projects');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const _projectPathCache = new Map();
|
|
36
|
+
|
|
35
37
|
async function resolveProjectPath(encoded) {
|
|
38
|
+
if (_projectPathCache.has(encoded)) return _projectPathCache.get(encoded);
|
|
36
39
|
let s = encoded;
|
|
37
40
|
if (s.startsWith('-')) s = s.slice(1);
|
|
38
41
|
if (!s) return '';
|
|
39
|
-
|
|
40
42
|
const parts = s.split('-');
|
|
41
43
|
|
|
42
44
|
// Try progressively joining segments from the right with dashes
|
|
@@ -46,14 +48,18 @@ async function resolveProjectPath(encoded) {
|
|
|
46
48
|
const testPath = `/${pathPart}/${dirPart}`;
|
|
47
49
|
try {
|
|
48
50
|
await fsp.access(testPath);
|
|
49
|
-
|
|
51
|
+
const result = `${pathPart}/${dirPart}`;
|
|
52
|
+
_projectPathCache.set(encoded, result);
|
|
53
|
+
return result;
|
|
50
54
|
} catch {
|
|
51
55
|
// Path doesn't exist, try next combination
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// Fallback to naive conversion
|
|
56
|
-
|
|
60
|
+
const result = s.replace(/-/g, '/');
|
|
61
|
+
_projectPathCache.set(encoded, result);
|
|
62
|
+
return result;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
function isMainSessionFile(filePath, stats) {
|
|
@@ -339,6 +345,11 @@ class Watcher extends EventEmitter {
|
|
|
339
345
|
this.watcher.on('unlink', (p) => {
|
|
340
346
|
this.filePositions.delete(p);
|
|
341
347
|
this.fileContexts.delete(p);
|
|
348
|
+
const timer = this.debounceTimers.get(p);
|
|
349
|
+
if (timer) {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
this.debounceTimers.delete(p);
|
|
352
|
+
}
|
|
342
353
|
});
|
|
343
354
|
this.watcher.on('error', (err) => this.emit('error', err));
|
|
344
355
|
|
|
@@ -877,14 +888,14 @@ class Watcher extends EventEmitter {
|
|
|
877
888
|
async _initializeSessionReading(sessions) {
|
|
878
889
|
let shouldSkip = this.skipHistory;
|
|
879
890
|
if (!shouldSkip) {
|
|
880
|
-
let
|
|
891
|
+
let totalEstimate = 0;
|
|
881
892
|
for (const session of sessions) {
|
|
882
|
-
|
|
893
|
+
totalEstimate += await this._estimateFileLines(session.mainFile);
|
|
883
894
|
for (const agentPath of Object.values(session.subagents)) {
|
|
884
|
-
|
|
895
|
+
totalEstimate += await this._estimateFileLines(agentPath);
|
|
885
896
|
}
|
|
886
897
|
}
|
|
887
|
-
shouldSkip =
|
|
898
|
+
shouldSkip = totalEstimate > AutoSkipLineThreshold;
|
|
888
899
|
}
|
|
889
900
|
|
|
890
901
|
if (shouldSkip) {
|
|
@@ -982,17 +993,17 @@ class Watcher extends EventEmitter {
|
|
|
982
993
|
if (pos === stats.size) { await handle.close(); handle = null; return; }
|
|
983
994
|
|
|
984
995
|
newPos = pos;
|
|
996
|
+
const fileSize = stats.size;
|
|
985
997
|
// Read in chunks to avoid large buffer allocations for big file deltas
|
|
986
998
|
let carryOver = ''; // incomplete trailing line from previous chunk
|
|
987
999
|
let carryOverBytes = 0; // byte length of carryOver (to avoid re-reading it)
|
|
988
1000
|
const buf = Buffer.alloc(MaxReadChunk);
|
|
989
1001
|
|
|
990
1002
|
while (true) {
|
|
991
|
-
const currentStats = await handle.stat();
|
|
992
1003
|
const readFrom = newPos + carryOverBytes;
|
|
993
|
-
if (readFrom >=
|
|
1004
|
+
if (readFrom >= fileSize) break;
|
|
994
1005
|
|
|
995
|
-
const readLen = Math.min(MaxReadChunk,
|
|
1006
|
+
const readLen = Math.min(MaxReadChunk, fileSize - readFrom);
|
|
996
1007
|
const { bytesRead } = await handle.read(buf, 0, readLen, readFrom);
|
|
997
1008
|
if (bytesRead === 0) break;
|
|
998
1009
|
|
|
@@ -1076,7 +1087,7 @@ class Watcher extends EventEmitter {
|
|
|
1076
1087
|
}
|
|
1077
1088
|
|
|
1078
1089
|
newPos += chunkBytes;
|
|
1079
|
-
this.filePositions.set(filePath, Math.min(newPos,
|
|
1090
|
+
this.filePositions.set(filePath, Math.min(newPos, fileSize));
|
|
1080
1091
|
}
|
|
1081
1092
|
|
|
1082
1093
|
// Process any remaining carryOver as a final incomplete line (no trailing \n).
|
|
@@ -1103,27 +1114,10 @@ class Watcher extends EventEmitter {
|
|
|
1103
1114
|
}
|
|
1104
1115
|
}
|
|
1105
1116
|
|
|
1106
|
-
async
|
|
1117
|
+
async _estimateFileLines(filePath) {
|
|
1107
1118
|
try {
|
|
1108
1119
|
const stat = await fsp.stat(filePath);
|
|
1109
|
-
|
|
1110
|
-
const handle = await fsp.open(filePath, 'r');
|
|
1111
|
-
const buf = Buffer.alloc(8192);
|
|
1112
|
-
let count = 0;
|
|
1113
|
-
let pos = 0;
|
|
1114
|
-
try {
|
|
1115
|
-
while (pos < stat.size) {
|
|
1116
|
-
const readLen = Math.min(8192, stat.size - pos);
|
|
1117
|
-
const { bytesRead } = await handle.read(buf, 0, readLen, pos);
|
|
1118
|
-
for (let i = 0; i < bytesRead; i++) {
|
|
1119
|
-
if (buf[i] === 0x0A) count++;
|
|
1120
|
-
}
|
|
1121
|
-
pos += bytesRead;
|
|
1122
|
-
}
|
|
1123
|
-
} finally {
|
|
1124
|
-
await handle.close();
|
|
1125
|
-
}
|
|
1126
|
-
return count;
|
|
1120
|
+
return Math.ceil(stat.size / 500);
|
|
1127
1121
|
} catch {
|
|
1128
1122
|
return 0;
|
|
1129
1123
|
}
|