claude-code-watch 0.0.1
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/LICENSE +22 -0
- package/README.md +110 -0
- package/README.zh-CN.md +30 -0
- package/bin/claude-watch.js +187 -0
- package/package.json +38 -0
- package/public/index.html +1206 -0
- package/src/parser/parser.js +528 -0
- package/src/server/server.js +375 -0
- package/src/watcher/watcher.js +1130 -0
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>claude-watch</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
|
8
|
+
<style>
|
|
9
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #111827;
|
|
13
|
+
--bg2: #1f2937;
|
|
14
|
+
--bg3: #374151;
|
|
15
|
+
--border: #4b5563;
|
|
16
|
+
--text: #d1d5db;
|
|
17
|
+
--dim: #6b7280;
|
|
18
|
+
--white: #f9fafb;
|
|
19
|
+
--purple: #7c3aed;
|
|
20
|
+
--purple2: #5b21b6;
|
|
21
|
+
--blue: #3b82f6;
|
|
22
|
+
--magenta: #c084fc;
|
|
23
|
+
--yellow: #fbbf24;
|
|
24
|
+
--yellow2: #92400e;
|
|
25
|
+
--green: #34d399;
|
|
26
|
+
--cyan: #22d3ee;
|
|
27
|
+
--red: #f87171;
|
|
28
|
+
--red2: #dc2626;
|
|
29
|
+
--gray: #9ca3af;
|
|
30
|
+
--orange: #fb923c;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
background: var(--bg);
|
|
35
|
+
color: var(--text);
|
|
36
|
+
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
|
|
37
|
+
font-size: 13px;
|
|
38
|
+
line-height: 1.5;
|
|
39
|
+
height: 100vh;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── Header ── */
|
|
46
|
+
#header {
|
|
47
|
+
height: 32px;
|
|
48
|
+
background: var(--bg3);
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
padding: 0 12px;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
color: var(--white);
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
user-select: none;
|
|
57
|
+
}
|
|
58
|
+
#header .sep { color: var(--dim); margin: 0 4px; }
|
|
59
|
+
#header .auto { margin-left: auto; display: flex; gap: 4px; align-items: center; }
|
|
60
|
+
|
|
61
|
+
.btn {
|
|
62
|
+
position: relative;
|
|
63
|
+
display: inline-flex; align-items: center; gap: 3px;
|
|
64
|
+
padding: 2px 8px; border-radius: 4px; border: 1px solid var(--border);
|
|
65
|
+
background: var(--bg2); color: var(--text); cursor: pointer;
|
|
66
|
+
font-family: inherit; font-size: 11px; line-height: 1.5;
|
|
67
|
+
white-space: nowrap; user-select: none; transition: all 0.15s;
|
|
68
|
+
}
|
|
69
|
+
.btn[data-tooltip]:hover::after {
|
|
70
|
+
content: attr(data-tooltip);
|
|
71
|
+
position: absolute;
|
|
72
|
+
bottom: calc(100% + 5px);
|
|
73
|
+
left: 50%;
|
|
74
|
+
transform: translateX(-50%);
|
|
75
|
+
background: var(--bg2); color: var(--white);
|
|
76
|
+
padding: 2px 8px; border-radius: 4px;
|
|
77
|
+
font-size: 10px; white-space: nowrap; z-index: 9999;
|
|
78
|
+
pointer-events: none; border: 1px solid var(--border);
|
|
79
|
+
font-family: inherit; line-height: 1.4;
|
|
80
|
+
}
|
|
81
|
+
.btn:hover { background: var(--bg3); border-color: var(--dim); }
|
|
82
|
+
.btn.on { background: var(--purple); border-color: var(--purple); color: var(--white); }
|
|
83
|
+
.btn.on:hover { background: var(--purple2); }
|
|
84
|
+
.btn.on:hover::after { background: var(--purple2); }
|
|
85
|
+
.btn.danger { border-color: var(--red2); color: var(--red); }
|
|
86
|
+
.btn.danger:hover { background: var(--red2); color: var(--white); }
|
|
87
|
+
.btn.accent { border-color: var(--yellow2); color: var(--yellow); }
|
|
88
|
+
.btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
89
|
+
.btn-icon { padding: 2px 6px; min-width: 28px; justify-content: center; }
|
|
90
|
+
|
|
91
|
+
/* ── Main area ── */
|
|
92
|
+
#main {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex: 1;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ── Tree panel ── */
|
|
99
|
+
#tree-panel {
|
|
100
|
+
width: 30%; min-width: 180px; max-width: 60%;
|
|
101
|
+
border-right: 1px solid var(--border);
|
|
102
|
+
background: var(--bg2);
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
display: flex; flex-direction: column;
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
position: relative;
|
|
107
|
+
}
|
|
108
|
+
#tree-panel.hidden { display: none; }
|
|
109
|
+
|
|
110
|
+
#tree-resize-handle {
|
|
111
|
+
position: absolute; right: -3px; top: 0; bottom: 0;
|
|
112
|
+
width: 6px; cursor: col-resize; z-index: 10;
|
|
113
|
+
}
|
|
114
|
+
#tree-resize-handle:hover, #tree-resize-handle.active { background: var(--purple); }
|
|
115
|
+
|
|
116
|
+
#tree-toolbar {
|
|
117
|
+
display: flex; gap: 4px; padding: 4px 6px;
|
|
118
|
+
border-bottom: 1px solid var(--border);
|
|
119
|
+
flex-shrink: 0; flex-wrap: wrap;
|
|
120
|
+
}
|
|
121
|
+
#tree-content {
|
|
122
|
+
flex: 1; overflow-y: auto; overflow-x: hidden; padding: 4px 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── Tree node styles ── */
|
|
126
|
+
.tree-row {
|
|
127
|
+
display: flex; align-items: center;
|
|
128
|
+
}
|
|
129
|
+
.tree-node {
|
|
130
|
+
flex: 1; display: flex; align-items: center;
|
|
131
|
+
padding: 3px 2px 3px 0;
|
|
132
|
+
cursor: pointer; white-space: nowrap; gap: 4px;
|
|
133
|
+
min-width: 0; overflow: hidden;
|
|
134
|
+
}
|
|
135
|
+
.tree-node:hover { background: rgba(255,255,255,0.05); }
|
|
136
|
+
.tree-node.selected { background: rgba(124,58,237,0.3); }
|
|
137
|
+
.tree-node.dim { opacity: 0.4; }
|
|
138
|
+
.tree-prefix { color: var(--dim); font-size: 12px; flex-shrink: 0; letter-spacing: 0; font-family: monospace; }
|
|
139
|
+
.tree-node .ctx-pct { font-size: 10px; margin-left: 4px; flex-shrink: 0; }
|
|
140
|
+
.tree-node .ctx-pct.warn { color: var(--yellow); }
|
|
141
|
+
.tree-node .ctx-pct.danger { color: var(--red); }
|
|
142
|
+
.tree-node .active-dot { flex-shrink: 0; }
|
|
143
|
+
.tree-node .active-dot.on { color: var(--green); text-shadow: 0 0 6px var(--green); }
|
|
144
|
+
.tree-node .active-dot.off { color: #555; opacity: 1; }
|
|
145
|
+
|
|
146
|
+
.tree-actions { display: none; gap: 2px; padding-right: 4px; }
|
|
147
|
+
.tree-row:hover .tree-actions { display: flex; }
|
|
148
|
+
.tree-row.selected>.tree-actions { display: flex; }
|
|
149
|
+
|
|
150
|
+
/* ── Stream panel ── */
|
|
151
|
+
#stream-panel-wrap {
|
|
152
|
+
flex: 1; display: flex; flex-direction: column; overflow: hidden;
|
|
153
|
+
}
|
|
154
|
+
#stream-toolbar {
|
|
155
|
+
display: flex; gap: 4px; padding: 4px 8px;
|
|
156
|
+
border-bottom: 1px solid var(--border);
|
|
157
|
+
background: var(--bg); flex-shrink: 0;
|
|
158
|
+
}
|
|
159
|
+
#stream-panel {
|
|
160
|
+
flex: 1; overflow-y: auto; padding: 8px 12px;
|
|
161
|
+
font-size: 12px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ── Stream lines ── */
|
|
165
|
+
.stream-line { white-space: pre-wrap; word-break: break-all; }
|
|
166
|
+
.stream-line.thinking { color: var(--magenta); }
|
|
167
|
+
.stream-line.tool-input { color: var(--yellow); }
|
|
168
|
+
.stream-line.tool-output { color: var(--green); }
|
|
169
|
+
.stream-line.text { color: var(--text); }
|
|
170
|
+
.stream-line.hook { color: var(--cyan); }
|
|
171
|
+
.stream-line.diag { color: var(--red); }
|
|
172
|
+
.stream-line.debug { color: var(--gray); }
|
|
173
|
+
.stream-line.marker { color: var(--dim); }
|
|
174
|
+
.stream-line.agent-tag { font-weight: bold; }
|
|
175
|
+
.stream-line.agent-main { color: var(--blue); }
|
|
176
|
+
.stream-line.agent-sub { color: var(--magenta); }
|
|
177
|
+
.stream-line.separator { color: var(--dim); }
|
|
178
|
+
|
|
179
|
+
/* ── Footer ── */
|
|
180
|
+
#footer {
|
|
181
|
+
height: 28px; background: var(--bg2);
|
|
182
|
+
border-top: 1px solid var(--border);
|
|
183
|
+
display: flex; align-items: center;
|
|
184
|
+
padding: 0 8px; gap: 6px;
|
|
185
|
+
font-size: 11px; flex-shrink: 0; flex-wrap: wrap;
|
|
186
|
+
}
|
|
187
|
+
#footer .sep { color: var(--dim); margin: 0 2px; }
|
|
188
|
+
|
|
189
|
+
/* ── Scrollbar ── */
|
|
190
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
191
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
192
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
193
|
+
|
|
194
|
+
/* ── Focus ring ── */
|
|
195
|
+
#stream-panel:focus { outline: none; }
|
|
196
|
+
|
|
197
|
+
/* ── Markdown & code blocks ── */
|
|
198
|
+
.md-content { line-height: 1.6; color: var(--text); }
|
|
199
|
+
.md-content p { margin: 4px 0; }
|
|
200
|
+
.md-content ul, .md-content ol { padding-left: 20px; margin: 4px 0; }
|
|
201
|
+
.md-content li { margin: 2px 0; }
|
|
202
|
+
.md-content strong { color: var(--white); }
|
|
203
|
+
.md-content a { color: var(--blue); text-decoration: underline; }
|
|
204
|
+
.md-content h1, .md-content h2, .md-content h3, .md-content h4,
|
|
205
|
+
.md-content h5, .md-content h6 { color: var(--white); margin: 8px 0 4px; font-size: inherit; font-weight: bold; }
|
|
206
|
+
.md-content blockquote { border-left: 3px solid var(--purple); padding-left: 12px; color: var(--dim); margin: 4px 0; }
|
|
207
|
+
.md-content hr { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
|
|
208
|
+
.md-content table { border-collapse: collapse; margin: 4px 0; width: 100%; }
|
|
209
|
+
.md-content th, .md-content td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
|
|
210
|
+
.md-content th { background: var(--bg3); color: var(--white); }
|
|
211
|
+
.md-content img { max-width: 100%; }
|
|
212
|
+
|
|
213
|
+
.code-block-wrapper { margin: 8px 0; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
|
|
214
|
+
.code-block-header { display: flex; align-items: center; justify-content: space-between; padding: 4px 12px; background: var(--bg3); font-size: 11px; color: var(--dim); }
|
|
215
|
+
.code-block-header .lang-tag { color: var(--blue); font-weight: bold; }
|
|
216
|
+
.code-block-wrapper pre { margin: 0; padding: 12px; overflow-x: auto; font-size: 12px; line-height: 1.5; }
|
|
217
|
+
.code-block-wrapper pre code { font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace; font-size: 12px; }
|
|
218
|
+
|
|
219
|
+
/* Override highlight.js background to match our theme */
|
|
220
|
+
.hljs { background: #0d1117 !important; }
|
|
221
|
+
</style>
|
|
222
|
+
</head>
|
|
223
|
+
<body>
|
|
224
|
+
|
|
225
|
+
<div id="header">
|
|
226
|
+
<button class="btn on" id="btn-thinking" onclick="toggleThinking()" data-tooltip="Toggle thinking">🧠 Thinking</button>
|
|
227
|
+
<button class="btn on" id="btn-tool-input" onclick="toggleToolInput()" data-tooltip="Toggle tool input">🔧 Tools</button>
|
|
228
|
+
<button class="btn on" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
|
|
229
|
+
<button class="btn on" id="btn-text" onclick="toggleText()" data-tooltip="Toggle text responses">💬 Text</button>
|
|
230
|
+
<span class="sep">│</span>
|
|
231
|
+
<button class="btn on" id="btn-autoscroll" onclick="toggleAutoScroll()" data-tooltip="Auto-scroll">⇣ Auto</button>
|
|
232
|
+
<button class="btn" id="btn-tree-toggle" onclick="toggleTree()" data-tooltip="Toggle tree panel">📂 Tree</button>
|
|
233
|
+
<span class="sep">│</span>
|
|
234
|
+
<span id="session-info">Connecting...</span>
|
|
235
|
+
<div class="auto">
|
|
236
|
+
<button class="btn on" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
|
|
237
|
+
<span class="sep">│</span>
|
|
238
|
+
<span id="token-info"></span>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div id="main">
|
|
243
|
+
<div id="tree-panel">
|
|
244
|
+
<div id="tree-resize-handle"></div>
|
|
245
|
+
<div id="tree-toolbar">
|
|
246
|
+
<button class="btn btn-icon" onclick="selectAll()" data-tooltip="Show all sessions/agents">⊞</button>
|
|
247
|
+
<button class="btn btn-icon accent" onclick="soloSelected()" data-tooltip="Solo selected">⊙</button>
|
|
248
|
+
<button class="btn btn-icon danger" onclick="removeSelectedSession()" data-tooltip="Remove session">✕</button>
|
|
249
|
+
<span style="flex:1"></span>
|
|
250
|
+
<span id="tree-cursor-info" style="font-size:10px;color:var(--dim)"></span>
|
|
251
|
+
</div>
|
|
252
|
+
<div id="tree-content"></div>
|
|
253
|
+
</div>
|
|
254
|
+
<div id="stream-panel-wrap">
|
|
255
|
+
<div id="stream-toolbar">
|
|
256
|
+
<button class="btn btn-icon" onclick="scrollToTop()" data-tooltip="Top">⏫</button>
|
|
257
|
+
<button class="btn btn-icon" onclick="scrollUp()" data-tooltip="Up">↑</button>
|
|
258
|
+
<button class="btn btn-icon" onclick="scrollDown()" data-tooltip="Down">↓</button>
|
|
259
|
+
<button class="btn btn-icon" onclick="scrollToBottom()" data-tooltip="Bottom">⏬</button>
|
|
260
|
+
</div>
|
|
261
|
+
<div id="stream-panel" tabindex="0"></div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div id="footer">
|
|
266
|
+
<span id="scroll-pos">0%</span>
|
|
267
|
+
<span class="sep">│</span>
|
|
268
|
+
<span id="item-count">0 items</span>
|
|
269
|
+
<span class="sep">│</span>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
273
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script>
|
|
274
|
+
<script>
|
|
275
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
276
|
+
// DOM refs
|
|
277
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
278
|
+
|
|
279
|
+
const streamEl = document.getElementById('stream-panel');
|
|
280
|
+
const treeEl = document.getElementById('tree-content');
|
|
281
|
+
const sessionInfo = document.getElementById('session-info');
|
|
282
|
+
const tokenInfo = document.getElementById('token-info');
|
|
283
|
+
const treeCursorInfo = document.getElementById('tree-cursor-info');
|
|
284
|
+
|
|
285
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
286
|
+
// State
|
|
287
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
288
|
+
|
|
289
|
+
let ws = null;
|
|
290
|
+
let reconnectTimer = null;
|
|
291
|
+
let reconnectDelay = 1000;
|
|
292
|
+
const MaxReconnectDelay = 30000;
|
|
293
|
+
let showTree = true;
|
|
294
|
+
let autoScroll = true;
|
|
295
|
+
let lastMsgTime = 0;
|
|
296
|
+
let staleCheckTimer = null;
|
|
297
|
+
|
|
298
|
+
let sessions = [];
|
|
299
|
+
let treeNodes = [];
|
|
300
|
+
let treeCursor = 0;
|
|
301
|
+
let streamItems = [];
|
|
302
|
+
let seenToolIDs = new Set();
|
|
303
|
+
let toolNameMap = new Map(); // toolID -> toolName
|
|
304
|
+
let filters = new Map();
|
|
305
|
+
|
|
306
|
+
let showThinking = true;
|
|
307
|
+
let showToolInput = true;
|
|
308
|
+
let showToolOutput = true;
|
|
309
|
+
let showText = true;
|
|
310
|
+
let autoDiscovery = true;
|
|
311
|
+
|
|
312
|
+
let renderPending = false;
|
|
313
|
+
|
|
314
|
+
let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0;
|
|
315
|
+
let contextData = {};
|
|
316
|
+
|
|
317
|
+
let collapseAfter = 0;
|
|
318
|
+
let collapseTimer = null;
|
|
319
|
+
let activeRefreshTimer = null;
|
|
320
|
+
|
|
321
|
+
const MAX_ITEMS = 1000;
|
|
322
|
+
const MAX_LINES = 50;
|
|
323
|
+
let renderedItemCount = 0;
|
|
324
|
+
let needsFullRender = true;
|
|
325
|
+
|
|
326
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
327
|
+
// Markdown renderer (marked + highlight.js)
|
|
328
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
329
|
+
|
|
330
|
+
const mdRenderer = new marked.Renderer();
|
|
331
|
+
mdRenderer.code = function (code, lang) {
|
|
332
|
+
let highlighted;
|
|
333
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
334
|
+
try {
|
|
335
|
+
highlighted = hljs.highlight(code, { language: lang }).value;
|
|
336
|
+
} catch {
|
|
337
|
+
highlighted = hljs.highlightAuto(code).value;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
highlighted = hljs.highlightAuto(code).value;
|
|
341
|
+
}
|
|
342
|
+
const langTag = lang ? `<span class="lang-tag">${esc(lang)}</span>` : '';
|
|
343
|
+
return `<div class="code-block-wrapper">
|
|
344
|
+
<div class="code-block-header">${langTag}<span></span></div>
|
|
345
|
+
<pre><code>${highlighted}</code></pre>
|
|
346
|
+
</div>`;
|
|
347
|
+
};
|
|
348
|
+
marked.setOptions({ renderer: mdRenderer, breaks: true, gfm: true });
|
|
349
|
+
|
|
350
|
+
function mdRender(text) {
|
|
351
|
+
try {
|
|
352
|
+
return marked.parse(text);
|
|
353
|
+
} catch {
|
|
354
|
+
return esc(text);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
359
|
+
// WebSocket
|
|
360
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
361
|
+
|
|
362
|
+
function connect() {
|
|
363
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
364
|
+
ws = new WebSocket(`${proto}//${location.host}`);
|
|
365
|
+
|
|
366
|
+
ws.onopen = () => {
|
|
367
|
+
sessionInfo.textContent = 'Connected';
|
|
368
|
+
lastMsgTime = Date.now();
|
|
369
|
+
reconnectDelay = 1000;
|
|
370
|
+
startStaleCheck();
|
|
371
|
+
startActiveRefresh();
|
|
372
|
+
};
|
|
373
|
+
ws.onclose = () => {
|
|
374
|
+
sessionInfo.textContent = 'Disconnected, reconnecting...';
|
|
375
|
+
stopStaleCheck();
|
|
376
|
+
if (activeRefreshTimer) { clearInterval(activeRefreshTimer); activeRefreshTimer = null; }
|
|
377
|
+
reconnectTimer = setTimeout(connect, reconnectDelay);
|
|
378
|
+
reconnectDelay = Math.min(reconnectDelay * 2, MaxReconnectDelay);
|
|
379
|
+
};
|
|
380
|
+
ws.onerror = () => {};
|
|
381
|
+
|
|
382
|
+
ws.onmessage = (e) => {
|
|
383
|
+
lastMsgTime = Date.now();
|
|
384
|
+
let msg;
|
|
385
|
+
try { msg = JSON.parse(e.data); } catch { return; }
|
|
386
|
+
handleMessage(msg);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function startStaleCheck() {
|
|
391
|
+
if (staleCheckTimer) clearInterval(staleCheckTimer);
|
|
392
|
+
staleCheckTimer = setInterval(() => {
|
|
393
|
+
if (Date.now() - lastMsgTime > 45000) {
|
|
394
|
+
sessionInfo.textContent = 'Stale connection, reconnecting...';
|
|
395
|
+
stopStaleCheck();
|
|
396
|
+
try { ws.close(); } catch {}
|
|
397
|
+
// onclose handler will handle reconnect — no separate timer needed
|
|
398
|
+
}
|
|
399
|
+
}, 10000);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function stopStaleCheck() {
|
|
403
|
+
if (staleCheckTimer) { clearInterval(staleCheckTimer); staleCheckTimer = null; }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function handleMessage(msg) {
|
|
407
|
+
switch (msg.type) {
|
|
408
|
+
case 'snapshot': handleSnapshot(msg.payload); break;
|
|
409
|
+
case 'itemBatch': handleItemBatch(msg.payload); break;
|
|
410
|
+
case 'item': handleItem(msg.payload); break;
|
|
411
|
+
case 'newSession': handleNewSession(msg.payload); break;
|
|
412
|
+
case 'newAgent': handleNewAgent(msg.payload); break;
|
|
413
|
+
case 'newBackgroundTask': handleNewBgTask(msg.payload); break;
|
|
414
|
+
case 'sessionRemoved': handleSessionRemoved(msg.payload); break;
|
|
415
|
+
case 'autoDiscoveryChanged': autoDiscovery = msg.payload.enabled; scheduleRender(); break;
|
|
416
|
+
case 'context': contextData = msg.payload; scheduleRender(); break;
|
|
417
|
+
case 'config':
|
|
418
|
+
if (msg.payload.collapseAfter > 0 && !collapseTimer) {
|
|
419
|
+
applyCollapsePolicy(msg.payload.collapseAfter);
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
case 'heartbeat': break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function sendCmd(action, extra = {}) {
|
|
427
|
+
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ action, ...extra }));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
431
|
+
// Snapshot / Session management
|
|
432
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
433
|
+
|
|
434
|
+
function handleSnapshot(payload) {
|
|
435
|
+
autoDiscovery = payload.autoDiscovery;
|
|
436
|
+
for (const s of (payload.sessions || [])) {
|
|
437
|
+
let session = sessions.find(x => x.id === s.id);
|
|
438
|
+
if (!session) {
|
|
439
|
+
session = {
|
|
440
|
+
id: s.id, projectPath: s.projectPath, title: '',
|
|
441
|
+
folder: folderName(s.projectPath), model: '',
|
|
442
|
+
agents: [], tasks: [], collapsed: false, pinned: false,
|
|
443
|
+
lastActivity: Date.now(),
|
|
444
|
+
};
|
|
445
|
+
sessions.push(session);
|
|
446
|
+
session.agents.push({ id: '', name: 'Main', type: 'main' });
|
|
447
|
+
}
|
|
448
|
+
session.lastActivity = Date.now();
|
|
449
|
+
for (const [aid, atype] of Object.entries(s.subagents || {})) {
|
|
450
|
+
if (!session.agents.find(a => a.id === aid)) {
|
|
451
|
+
session.agents.push({ id: aid, name: agentDisplayName(aid, atype), type: 'agent' });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const t of (s.backgroundTasks || [])) {
|
|
455
|
+
if (!session.tasks.find(ta => ta.id === t.id)) {
|
|
456
|
+
session.tasks.push({
|
|
457
|
+
id: t.id, parentAgentID: t.parentAgentID,
|
|
458
|
+
toolName: t.toolName, outputPath: t.outputPath,
|
|
459
|
+
isComplete: t.isComplete,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
updateFilters();
|
|
465
|
+
rebuildNodes();
|
|
466
|
+
scheduleRender();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function handleNewSession(payload) {
|
|
470
|
+
if (sessions.find(s => s.id === payload.sessionID)) return;
|
|
471
|
+
sessions.push({
|
|
472
|
+
id: payload.sessionID, projectPath: payload.projectPath,
|
|
473
|
+
title: '', folder: folderName(payload.projectPath), model: '',
|
|
474
|
+
agents: [{ id: '', name: 'Main', type: 'main' }],
|
|
475
|
+
tasks: [], collapsed: false, pinned: false,
|
|
476
|
+
lastActivity: Date.now(),
|
|
477
|
+
});
|
|
478
|
+
updateFilters();
|
|
479
|
+
rebuildNodes();
|
|
480
|
+
scheduleRender();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function handleNewAgent(payload) {
|
|
484
|
+
const s = sessions.find(s => s.id === payload.sessionID);
|
|
485
|
+
if (!s || s.agents.find(a => a.id === payload.agentID)) return;
|
|
486
|
+
s.agents.push({
|
|
487
|
+
id: payload.agentID,
|
|
488
|
+
name: agentDisplayName(payload.agentID, payload.agentType),
|
|
489
|
+
type: 'agent',
|
|
490
|
+
});
|
|
491
|
+
updateFilters();
|
|
492
|
+
rebuildNodes();
|
|
493
|
+
scheduleRender();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function handleNewBgTask(payload) {
|
|
497
|
+
const s = sessions.find(s => s.id === payload.sessionID);
|
|
498
|
+
if (!s || s.tasks.find(t => t.id === payload.toolID)) return;
|
|
499
|
+
s.tasks.push({
|
|
500
|
+
id: payload.toolID, parentAgentID: payload.parentAgentID,
|
|
501
|
+
toolName: payload.toolName, outputPath: payload.outputPath,
|
|
502
|
+
isComplete: payload.isComplete,
|
|
503
|
+
});
|
|
504
|
+
rebuildNodes();
|
|
505
|
+
scheduleRender();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function handleSessionRemoved(payload) {
|
|
509
|
+
const idx = sessions.findIndex(s => s.id === payload.sessionID);
|
|
510
|
+
if (idx >= 0) sessions.splice(idx, 1);
|
|
511
|
+
updateFilters();
|
|
512
|
+
rebuildNodes();
|
|
513
|
+
scheduleRender();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
517
|
+
// Stream items
|
|
518
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
519
|
+
|
|
520
|
+
function handleItem(item) {
|
|
521
|
+
if (item.type === 'session_title') {
|
|
522
|
+
const s = sessions.find(s => s.id === item.sessionID);
|
|
523
|
+
if (s) { s.title = item.content.slice(0, 30); }
|
|
524
|
+
scheduleRender();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// Update activity
|
|
528
|
+
const s = sessions.find(s => s.id === item.sessionID);
|
|
529
|
+
if (s) s.lastActivity = Date.now();
|
|
530
|
+
pushItem(item);
|
|
531
|
+
scheduleRender();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function handleItemBatch(items) {
|
|
535
|
+
for (const item of items) {
|
|
536
|
+
if (item.type === 'session_title') {
|
|
537
|
+
const s = sessions.find(s => s.id === item.sessionID);
|
|
538
|
+
if (s) { s.title = item.content.slice(0, 30); }
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
pushItem(item);
|
|
542
|
+
}
|
|
543
|
+
scheduleRender();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function pushItem(item) {
|
|
547
|
+
if (item.inputTokens > 0) totalInput += item.inputTokens;
|
|
548
|
+
if (item.outputTokens > 0) totalOutput += item.outputTokens;
|
|
549
|
+
if (item.cacheCreationTokens > 0) totalCacheCreate += item.cacheCreationTokens;
|
|
550
|
+
if (item.cacheReadTokens > 0) totalCacheRead += item.cacheReadTokens;
|
|
551
|
+
|
|
552
|
+
if (item.model) {
|
|
553
|
+
const s = sessions.find(s => s.id === item.sessionID);
|
|
554
|
+
if (s) s.model = item.model;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (item.type === 'tool_input' && item.toolID && item.toolName) {
|
|
558
|
+
toolNameMap.set(item.toolID, item.toolName);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (item.toolID) {
|
|
562
|
+
const key = `${item.toolID}:${item.type}`;
|
|
563
|
+
if (seenToolIDs.has(key)) return;
|
|
564
|
+
seenToolIDs.add(key);
|
|
565
|
+
if (seenToolIDs.size > 5000) seenToolIDs.clear();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
streamItems.push(item);
|
|
569
|
+
if (streamItems.length > MAX_ITEMS) {
|
|
570
|
+
streamItems = streamItems.slice(-MAX_ITEMS);
|
|
571
|
+
needsFullRender = true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function isItemVisible(item) {
|
|
576
|
+
if (!filters.has(item.sessionID + ':' + (item.agentID || ''))) return false;
|
|
577
|
+
switch (item.type) {
|
|
578
|
+
case 'thinking': return showThinking;
|
|
579
|
+
case 'tool_input': return showToolInput;
|
|
580
|
+
case 'tool_output': return showToolOutput;
|
|
581
|
+
case 'text': return showText;
|
|
582
|
+
default: return true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
587
|
+
// Tree
|
|
588
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
589
|
+
|
|
590
|
+
function rebuildNodes() {
|
|
591
|
+
treeNodes = [];
|
|
592
|
+
for (const s of sessions) {
|
|
593
|
+
treeNodes.push({ type: 'session', level: 0, isLast: false, ...s });
|
|
594
|
+
if (s.collapsed) continue;
|
|
595
|
+
const agents = s.agents || [];
|
|
596
|
+
const lastAgentIdx = agents.length - 1;
|
|
597
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
598
|
+
const a = agents[ai];
|
|
599
|
+
const isLastAgent = ai === lastAgentIdx;
|
|
600
|
+
const tasks = s.tasks.filter(t =>
|
|
601
|
+
(a.id === '' && !t.parentAgentID) || t.parentAgentID === a.id
|
|
602
|
+
);
|
|
603
|
+
const lastTaskIdx = tasks.length - 1;
|
|
604
|
+
const hasTasks = tasks.length > 0;
|
|
605
|
+
treeNodes.push({ type: a.type, id: a.id, name: a.name, sessionID: s.id, level: 1, isLast: isLastAgent && !hasTasks });
|
|
606
|
+
for (let ti = 0; ti < tasks.length; ti++) {
|
|
607
|
+
const t = tasks[ti];
|
|
608
|
+
treeNodes.push({
|
|
609
|
+
type: 'task', id: t.id, name: t.toolName,
|
|
610
|
+
sessionID: s.id, parentAgentID: t.parentAgentID,
|
|
611
|
+
outputPath: t.outputPath, isComplete: t.isComplete,
|
|
612
|
+
level: 2, isLast: isLastAgent && ti === lastTaskIdx,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Mark last session
|
|
618
|
+
const sessionNodes = treeNodes.filter(n => n.type === 'session');
|
|
619
|
+
if (sessionNodes.length > 0) sessionNodes[sessionNodes.length - 1].isLast = true;
|
|
620
|
+
|
|
621
|
+
if (treeCursor >= treeNodes.length) treeCursor = Math.max(0, treeNodes.length - 1);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function treePrefix(node) {
|
|
625
|
+
if (node.level === 0) return '';
|
|
626
|
+
const branch = node.isLast ? '└── ' : '├── ';
|
|
627
|
+
if (node.level === 1) return branch;
|
|
628
|
+
// Level 2: need to check if parent agent is last
|
|
629
|
+
const agentNode = treeNodes.find(n => n.sessionID === node.sessionID && (n.type === 'main' || n.type === 'agent') && n.id === (node.parentAgentID || ''));
|
|
630
|
+
const parentIsLast = agentNode ? agentNode.isLast : true;
|
|
631
|
+
const stem = parentIsLast ? ' ' : '│ ';
|
|
632
|
+
return stem + branch;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function getNodeHTML(node, idx) {
|
|
636
|
+
const isSelected = idx === treeCursor;
|
|
637
|
+
const selClass = isSelected ? ' selected' : '';
|
|
638
|
+
|
|
639
|
+
if (node.type === 'session') {
|
|
640
|
+
const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
|
|
641
|
+
const parts = [];
|
|
642
|
+
if (node.folder) parts.push(`📂 ${esc(node.folder)}`);
|
|
643
|
+
if (node.model) parts.push(`🧠 ${esc(node.model)}`);
|
|
644
|
+
const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
|
|
645
|
+
const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
|
|
646
|
+
const agentCount = node.agents ? node.agents.filter(a => a.type === 'agent').length : 0;
|
|
647
|
+
return `<div class="tree-row${selClass ? ' selected' : ''}">
|
|
648
|
+
<div class="tree-node" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
649
|
+
<span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
|
|
650
|
+
${node.collapsed && agentCount > 0 ? `(${agentCount})` : ''}
|
|
651
|
+
${subInfo}
|
|
652
|
+
</div>
|
|
653
|
+
<span class="tree-actions">
|
|
654
|
+
<button class="btn btn-icon accent" onclick="event.stopPropagation();selectIndex(${idx});soloSelected()" data-tooltip="Solo">⊙</button>
|
|
655
|
+
<button class="btn btn-icon danger" onclick="event.stopPropagation();selectIndex(${idx});removeSelectedSession()" data-tooltip="Remove">✕</button>
|
|
656
|
+
</span>
|
|
657
|
+
</div>`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (node.type === 'main' || node.type === 'agent') {
|
|
661
|
+
const icon = node.type === 'main' ? '💬' : '🤖';
|
|
662
|
+
const enabled = filters.get(node.sessionID + ':' + node.id);
|
|
663
|
+
const ctxKey = node.sessionID + ':' + node.id;
|
|
664
|
+
const ctx = contextData[ctxKey];
|
|
665
|
+
let ctxPct = '';
|
|
666
|
+
if (ctx && ctx.contextWindow > 0 && ctx.inputTokens > 0) {
|
|
667
|
+
const pct = Math.round(ctx.inputTokens / ctx.contextWindow * 100);
|
|
668
|
+
const cls = pct > 80 ? 'danger' : pct > 50 ? 'warn' : '';
|
|
669
|
+
ctxPct = `<span class="ctx-pct ${cls}">${pct}%</span>`;
|
|
670
|
+
}
|
|
671
|
+
const activeDot = ctx && (Date.now() - ctx.lastActivity < 120000) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
|
|
672
|
+
return `<div class="tree-row${selClass ? ' selected' : ''}">
|
|
673
|
+
<div class="tree-node${enabled ? '' : ' dim'}" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
674
|
+
<span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${icon} ${esc(node.name || '')}${ctxPct}
|
|
675
|
+
</div>
|
|
676
|
+
<span class="tree-actions">
|
|
677
|
+
<button class="btn btn-icon accent" onclick="event.stopPropagation();selectIndex(${idx});soloSelected()" data-tooltip="Solo">⊙</button>
|
|
678
|
+
<button class="btn btn-icon" onclick="event.stopPropagation();selectIndex(${idx});toggleNodeVisibility(${idx})" data-tooltip="${enabled ? 'Hide' : 'Show'}">${enabled ? '👁' : '─'}</button>
|
|
679
|
+
</span>
|
|
680
|
+
</div>`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (node.type === 'task') {
|
|
684
|
+
const icon = node.isComplete ? '✓' : '⏳';
|
|
685
|
+
return `<div class="tree-row${selClass ? ' selected' : ''}">
|
|
686
|
+
<div class="tree-node dim" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
687
|
+
<span class="tree-prefix">${treePrefix(node)}</span>${icon} ${esc(node.name || 'bg-task')}
|
|
688
|
+
</div>
|
|
689
|
+
<span class="tree-actions">
|
|
690
|
+
<button class="btn btn-icon" onclick="event.stopPropagation();selectIndex(${idx});loadBgTask(${idx})" data-tooltip="Load output">▶</button>
|
|
691
|
+
</span>
|
|
692
|
+
</div>`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return '';
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function renderTree() {
|
|
699
|
+
if (treeNodes.length === 0) {
|
|
700
|
+
treeEl.innerHTML = '<div class="tree-node" style="padding:8px;color:var(--dim)">Waiting for sessions...</div>';
|
|
701
|
+
treeCursorInfo.textContent = '';
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
let html = '';
|
|
705
|
+
for (let i = 0; i < treeNodes.length; i++) {
|
|
706
|
+
html += getNodeHTML(treeNodes[i], i);
|
|
707
|
+
}
|
|
708
|
+
treeEl.innerHTML = html;
|
|
709
|
+
|
|
710
|
+
// Scroll selected into view
|
|
711
|
+
const sel = treeEl.querySelector('.tree-node.selected');
|
|
712
|
+
if (sel) sel.scrollIntoView({ block: 'nearest' });
|
|
713
|
+
|
|
714
|
+
treeCursorInfo.textContent = `${treeCursor + 1}/${treeNodes.length}`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isSessionActive(session) {
|
|
718
|
+
if (!session) return false;
|
|
719
|
+
const now = Date.now();
|
|
720
|
+
// Check main agent
|
|
721
|
+
const mainCtx = contextData[session.id + ':'];
|
|
722
|
+
if (mainCtx && (now - mainCtx.lastActivity) < 120000) return true;
|
|
723
|
+
// Check subagents
|
|
724
|
+
for (const a of session.agents) {
|
|
725
|
+
if (a.id === '') continue;
|
|
726
|
+
const ctx = contextData[session.id + ':' + a.id];
|
|
727
|
+
if (ctx && (now - ctx.lastActivity) < 120000) return true;
|
|
728
|
+
}
|
|
729
|
+
// Fallback: check own lastActivity
|
|
730
|
+
return (now - session.lastActivity) < 120000;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
734
|
+
// Stream rendering
|
|
735
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
736
|
+
|
|
737
|
+
function renderStream() {
|
|
738
|
+
const visible = streamItems.filter(isItemVisible);
|
|
739
|
+
|
|
740
|
+
if (needsFullRender || renderedItemCount > visible.length) {
|
|
741
|
+
// Full rebuild: filter changed, items trimmed, or initial render
|
|
742
|
+
const lines = [];
|
|
743
|
+
for (const item of visible) {
|
|
744
|
+
for (const l of renderItem(item)) lines.push(l);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let html;
|
|
748
|
+
if (lines.length > 0) {
|
|
749
|
+
html = lines.map(l => {
|
|
750
|
+
if (l.html) return `<div class="${l.cls}">${l.text}</div>`;
|
|
751
|
+
return `<div class="${l.cls}">${esc(l.text)}</div>`;
|
|
752
|
+
}).join('\n');
|
|
753
|
+
} else if (streamItems.length > 0) {
|
|
754
|
+
html = `<div style="color:#fbbf24;padding:20px;text-align:center">${streamItems.length} items buffered, 0 visible — check toggles or tree selection</div>`;
|
|
755
|
+
} else {
|
|
756
|
+
html = '<div style="color:#6b7280;padding:20px;text-align:center">Waiting for output...</div>';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
streamEl.innerHTML = html;
|
|
760
|
+
renderedItemCount = visible.length;
|
|
761
|
+
needsFullRender = false;
|
|
762
|
+
if (autoScroll) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
|
|
763
|
+
} else {
|
|
764
|
+
// Incremental append: only add new items since last render
|
|
765
|
+
const wasAtBottom = streamEl.scrollHeight - streamEl.scrollTop - streamEl.clientHeight < 50;
|
|
766
|
+
for (let i = renderedItemCount; i < visible.length; i++) {
|
|
767
|
+
for (const l of renderItem(visible[i])) {
|
|
768
|
+
const div = document.createElement('div');
|
|
769
|
+
div.className = l.cls;
|
|
770
|
+
div.innerHTML = l.html ? l.text : esc(l.text);
|
|
771
|
+
streamEl.appendChild(div);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
renderedItemCount = visible.length;
|
|
775
|
+
if (autoScroll && wasAtBottom) requestAnimationFrame(() => { streamEl.scrollTop = streamEl.scrollHeight; });
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const maxScroll = streamEl.scrollHeight - streamEl.clientHeight;
|
|
779
|
+
const pct = maxScroll > 0 ? Math.round(streamEl.scrollTop / maxScroll * 100) : 0;
|
|
780
|
+
document.getElementById('scroll-pos').textContent = Math.min(100, pct) + '%';
|
|
781
|
+
document.getElementById('item-count').textContent = streamItems.length + ' items';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function renderItem(item) {
|
|
785
|
+
const lines = [];
|
|
786
|
+
const isSub = !!item.agentID;
|
|
787
|
+
const agentTagCls = 'stream-line ' + (isSub ? 'agent-sub agent-tag' : 'agent-main agent-tag');
|
|
788
|
+
const sep = ' » ';
|
|
789
|
+
|
|
790
|
+
if (item.type === 'turn_marker') {
|
|
791
|
+
return [{ cls: 'stream-line marker', text: `── turn ended ${fmtDur(item.durationMs)} ──` }];
|
|
792
|
+
}
|
|
793
|
+
if (item.type === 'compact_marker') {
|
|
794
|
+
const label = item.content ? `compacted (${item.content})` : 'compacted';
|
|
795
|
+
return [{ cls: 'stream-line marker', text: `── ${label} ──` }];
|
|
796
|
+
}
|
|
797
|
+
if (item.type === 'pr_link') {
|
|
798
|
+
return [{ cls: 'stream-line marker', text: `── ${item.content} ──` }];
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const agentName = item.agentName || 'Main';
|
|
802
|
+
|
|
803
|
+
switch (item.type) {
|
|
804
|
+
case 'thinking':
|
|
805
|
+
lines.push({ cls: agentTagCls, text: agentName + sep + '🧠 Thinking' });
|
|
806
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line thinking', text: l });
|
|
807
|
+
break;
|
|
808
|
+
case 'tool_input':
|
|
809
|
+
lines.push({ cls: agentTagCls, text: agentName + sep + `🔧 ${item.toolName || ''}` });
|
|
810
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-input', text: l });
|
|
811
|
+
break;
|
|
812
|
+
case 'tool_output': {
|
|
813
|
+
let tn = '';
|
|
814
|
+
if (item.toolID) {
|
|
815
|
+
tn = toolNameMap.get(item.toolID) || '';
|
|
816
|
+
}
|
|
817
|
+
let label = tn ? `📤 ${tn} result` : '📤 Output';
|
|
818
|
+
if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
|
|
819
|
+
lines.push({ cls: agentTagCls, text: agentName + sep + label });
|
|
820
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-output', text: l });
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case 'text':
|
|
824
|
+
lines.push({ cls: agentTagCls, text: agentName + sep + '💬 Response' });
|
|
825
|
+
lines.push({ cls: 'stream-line text md-content', text: mdRender(item.content), html: true });
|
|
826
|
+
break;
|
|
827
|
+
case 'hook_output': {
|
|
828
|
+
let label = '🪝 Hook';
|
|
829
|
+
if (item.toolName) label += ' ' + item.toolName;
|
|
830
|
+
if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
|
|
831
|
+
lines.push({ cls: agentTagCls, text: agentName + sep + label });
|
|
832
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line hook', text: l });
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
case 'diagnostics': {
|
|
836
|
+
let label = '⚠ Diagnostics';
|
|
837
|
+
if (item.toolName) label += ' ' + item.toolName;
|
|
838
|
+
lines.push({ cls: agentTagCls, text: agentName + sep + label });
|
|
839
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line diag', text: l });
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
case 'debug': {
|
|
843
|
+
let label = '🔍 Debug';
|
|
844
|
+
if (item.toolName) label += ' ' + item.toolName;
|
|
845
|
+
lines.push({ cls: agentTagCls, text: agentName + sep + label });
|
|
846
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l });
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
lines.push({ cls: 'stream-line separator', text: '─'.repeat(60) });
|
|
852
|
+
return lines;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function truncContent(content) {
|
|
856
|
+
const raw = content.split('\n');
|
|
857
|
+
return raw.length > MAX_LINES ? raw.slice(0, MAX_LINES).concat([`... (${raw.length - MAX_LINES} more lines)`]) : raw;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
861
|
+
// Button / header refresh
|
|
862
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
863
|
+
|
|
864
|
+
function refreshButtons() {
|
|
865
|
+
document.getElementById('btn-thinking').classList.toggle('on', showThinking);
|
|
866
|
+
document.getElementById('btn-tool-input').classList.toggle('on', showToolInput);
|
|
867
|
+
document.getElementById('btn-tool-output').classList.toggle('on', showToolOutput);
|
|
868
|
+
document.getElementById('btn-text').classList.toggle('on', showText);
|
|
869
|
+
document.getElementById('btn-autoscroll').classList.toggle('on', autoScroll);
|
|
870
|
+
document.getElementById('btn-tree-toggle').classList.toggle('on', showTree);
|
|
871
|
+
document.getElementById('btn-autodisco').classList.toggle('on', autoDiscovery);
|
|
872
|
+
|
|
873
|
+
// Session info
|
|
874
|
+
let info = '';
|
|
875
|
+
if (sessions.length === 0) info = 'Waiting...';
|
|
876
|
+
else if (sessions.length === 1) {
|
|
877
|
+
const s = sessions[0];
|
|
878
|
+
info = (s.title || folderName(s.projectPath) || s.id.slice(0, 14));
|
|
879
|
+
} else info = sessions.length + ' sessions';
|
|
880
|
+
if (!autoDiscovery) info += ' [paused]';
|
|
881
|
+
sessionInfo.textContent = info;
|
|
882
|
+
|
|
883
|
+
// Token info
|
|
884
|
+
let tokStr = '';
|
|
885
|
+
if (totalInput > 0 || totalOutput > 0) {
|
|
886
|
+
tokStr = `${fmtTok(totalInput)} in / ${fmtTok(totalOutput)} out`;
|
|
887
|
+
if (totalCacheCreate > 0 || totalCacheRead > 0) {
|
|
888
|
+
tokStr += ` · cache ${fmtTok(totalCacheCreate)}+${fmtTok(totalCacheRead)}`;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
tokenInfo.textContent = tokStr;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
895
|
+
// Actions
|
|
896
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
897
|
+
|
|
898
|
+
function selectIndex(idx) {
|
|
899
|
+
if (idx >= 0 && idx < treeNodes.length) treeCursor = idx;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function treeClick(idx) {
|
|
903
|
+
selectIndex(idx);
|
|
904
|
+
const node = treeNodes[idx];
|
|
905
|
+
if (!node) return;
|
|
906
|
+
if (node.type === 'session') {
|
|
907
|
+
const session = sessions.find(s => s.id === node.id);
|
|
908
|
+
if (session) {
|
|
909
|
+
session.collapsed = !session.collapsed;
|
|
910
|
+
if (!session.collapsed) session.pinned = true;
|
|
911
|
+
}
|
|
912
|
+
rebuildNodes();
|
|
913
|
+
} else if (node.type === 'main' || node.type === 'agent') {
|
|
914
|
+
toggleNodeVisibility(idx);
|
|
915
|
+
return;
|
|
916
|
+
} else if (node.type === 'task') {
|
|
917
|
+
loadBgTask(idx);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
renderAll();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function toggleNodeVisibility(idx) {
|
|
924
|
+
const node = treeNodes[idx];
|
|
925
|
+
if (!node) return;
|
|
926
|
+
const key = node.sessionID + ':' + node.id;
|
|
927
|
+
filters.set(key, !filters.get(key));
|
|
928
|
+
renderAll();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function loadBgTask(idx) {
|
|
932
|
+
const node = treeNodes[idx];
|
|
933
|
+
if (!node || node.type !== 'task') return;
|
|
934
|
+
if (!node.outputPath) return;
|
|
935
|
+
|
|
936
|
+
// Fetch the actual output file
|
|
937
|
+
fetch(`/api/task-output?path=${encodeURIComponent(node.outputPath)}`)
|
|
938
|
+
.then(r => r.json())
|
|
939
|
+
.then(data => {
|
|
940
|
+
const content = data.content || `[Error: ${data.error || 'unknown'}]`;
|
|
941
|
+
const statusIcon = node.isComplete ? '✓' : '⏳';
|
|
942
|
+
streamItems.push({
|
|
943
|
+
type: 'tool_output', sessionID: node.sessionID, agentID: node.parentAgentID || '',
|
|
944
|
+
agentName: '', toolName: `${statusIcon} ${node.name || 'bg-task'}`,
|
|
945
|
+
content: content,
|
|
946
|
+
timestamp: new Date(), toolID: '', durationMs: 0,
|
|
947
|
+
inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, model: '',
|
|
948
|
+
});
|
|
949
|
+
renderAll();
|
|
950
|
+
})
|
|
951
|
+
.catch(err => {
|
|
952
|
+
streamItems.push({
|
|
953
|
+
type: 'tool_output', sessionID: node.sessionID, agentID: node.parentAgentID || '',
|
|
954
|
+
agentName: '', toolName: `⏳ ${node.name || 'bg-task'}`,
|
|
955
|
+
content: `[Failed to load: ${err.message}]`,
|
|
956
|
+
timestamp: new Date(), toolID: '', durationMs: 0,
|
|
957
|
+
inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, model: '',
|
|
958
|
+
});
|
|
959
|
+
renderAll();
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function soloSelected() {
|
|
964
|
+
const node = treeNodes[treeCursor];
|
|
965
|
+
if (!node || node.type === 'task') return;
|
|
966
|
+
|
|
967
|
+
if (isSoloed(node)) {
|
|
968
|
+
updateFilters();
|
|
969
|
+
} else {
|
|
970
|
+
filters.clear();
|
|
971
|
+
if (node.type === 'session') {
|
|
972
|
+
const session = sessions.find(s => s.id === node.id);
|
|
973
|
+
if (session && session.collapsed) {
|
|
974
|
+
session.collapsed = false;
|
|
975
|
+
session.pinned = true;
|
|
976
|
+
rebuildNodes();
|
|
977
|
+
}
|
|
978
|
+
for (const a of node.agents) filters.set(node.id + ':' + a.id, true);
|
|
979
|
+
} else if (node.type === 'main' || node.type === 'agent') {
|
|
980
|
+
filters.set(node.sessionID + ':' + node.id, true);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
renderAll();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function isSoloed(node) {
|
|
987
|
+
if (node.type === 'session') {
|
|
988
|
+
for (const s of sessions) {
|
|
989
|
+
if (s.id === node.id) continue;
|
|
990
|
+
for (const a of s.agents) {
|
|
991
|
+
if (filters.get(s.id + ':' + a.id)) return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
if (node.type === 'main' || node.type === 'agent') {
|
|
997
|
+
const key = node.sessionID + ':' + node.id;
|
|
998
|
+
if (!filters.get(key)) return false;
|
|
999
|
+
for (const s of sessions) {
|
|
1000
|
+
for (const a of s.agents) {
|
|
1001
|
+
if ((s.id + ':' + a.id) !== key && filters.get(s.id + ':' + a.id)) return false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function selectAll() {
|
|
1010
|
+
updateFilters();
|
|
1011
|
+
renderAll();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function removeSelectedSession() {
|
|
1015
|
+
const node = treeNodes[treeCursor];
|
|
1016
|
+
if (!node) return;
|
|
1017
|
+
let sid;
|
|
1018
|
+
if (node.type === 'session') sid = node.id;
|
|
1019
|
+
else sid = node.sessionID;
|
|
1020
|
+
if (!sid) return;
|
|
1021
|
+
if (!confirm(`Remove session ${sid.slice(0, 12)}...?`)) return;
|
|
1022
|
+
sessions = sessions.filter(s => s.id !== sid);
|
|
1023
|
+
sendCmd('removeSession', { sessionID: sid });
|
|
1024
|
+
updateFilters();
|
|
1025
|
+
rebuildNodes();
|
|
1026
|
+
renderAll();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1030
|
+
// Toggles
|
|
1031
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1032
|
+
|
|
1033
|
+
function toggleThinking() { showThinking = !showThinking; needsFullRender = true; renderStream(); refreshButtons(); }
|
|
1034
|
+
function toggleToolInput() { showToolInput = !showToolInput; needsFullRender = true; renderStream(); refreshButtons(); }
|
|
1035
|
+
function toggleToolOutput() { showToolOutput = !showToolOutput; needsFullRender = true; renderStream(); refreshButtons(); }
|
|
1036
|
+
function toggleText() { showText = !showText; needsFullRender = true; renderStream(); refreshButtons(); }
|
|
1037
|
+
function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
|
|
1038
|
+
function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
|
|
1039
|
+
function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
|
|
1040
|
+
|
|
1041
|
+
function scrollToTop() { streamEl.scrollTop = 0; autoScroll = false; renderAll(); }
|
|
1042
|
+
function scrollUp() { streamEl.scrollTop -= 80; autoScroll = false; renderAll(); }
|
|
1043
|
+
function scrollDown() { streamEl.scrollTop += 80; renderAll(); }
|
|
1044
|
+
function scrollToBottom() { streamEl.scrollTop = streamEl.scrollHeight; autoScroll = true; renderAll(); }
|
|
1045
|
+
|
|
1046
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1047
|
+
// Tree panel resize
|
|
1048
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1049
|
+
|
|
1050
|
+
(function setupResize() {
|
|
1051
|
+
const panel = document.getElementById('tree-panel');
|
|
1052
|
+
const handle = document.getElementById('tree-resize-handle');
|
|
1053
|
+
let startX, startWidth;
|
|
1054
|
+
|
|
1055
|
+
handle.addEventListener('mousedown', (e) => {
|
|
1056
|
+
e.preventDefault();
|
|
1057
|
+
startX = e.clientX;
|
|
1058
|
+
startWidth = panel.offsetWidth;
|
|
1059
|
+
handle.classList.add('active');
|
|
1060
|
+
document.body.style.cursor = 'col-resize';
|
|
1061
|
+
document.body.style.userSelect = 'none';
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
document.addEventListener('mousemove', (e) => {
|
|
1065
|
+
if (!handle.classList.contains('active')) return;
|
|
1066
|
+
const dx = e.clientX - startX;
|
|
1067
|
+
const newWidth = startWidth + dx;
|
|
1068
|
+
if (newWidth >= 180 && newWidth <= window.innerWidth * 0.6) {
|
|
1069
|
+
panel.style.width = newWidth + 'px';
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
document.addEventListener('mouseup', () => {
|
|
1074
|
+
handle.classList.remove('active');
|
|
1075
|
+
document.body.style.cursor = '';
|
|
1076
|
+
document.body.style.userSelect = '';
|
|
1077
|
+
});
|
|
1078
|
+
})();
|
|
1079
|
+
|
|
1080
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1081
|
+
// Auto-collapse
|
|
1082
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1083
|
+
|
|
1084
|
+
function applyCollapsePolicy(duration) {
|
|
1085
|
+
collapseAfter = duration;
|
|
1086
|
+
if (collapseTimer) clearInterval(collapseTimer);
|
|
1087
|
+
if (duration <= 0) return;
|
|
1088
|
+
|
|
1089
|
+
collapseTimer = setInterval(() => {
|
|
1090
|
+
if (!collapseAfter) return;
|
|
1091
|
+
const now = Date.now();
|
|
1092
|
+
let changed = false;
|
|
1093
|
+
for (const s of sessions) {
|
|
1094
|
+
if (s.pinned || s.collapsed) continue;
|
|
1095
|
+
if ((now - s.lastActivity) > collapseAfter) {
|
|
1096
|
+
s.collapsed = true;
|
|
1097
|
+
changed = true;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (changed) {
|
|
1101
|
+
rebuildNodes();
|
|
1102
|
+
renderAll();
|
|
1103
|
+
}
|
|
1104
|
+
}, 5000);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function startActiveRefresh() {
|
|
1108
|
+
if (activeRefreshTimer) clearInterval(activeRefreshTimer);
|
|
1109
|
+
activeRefreshTimer = setInterval(() => {
|
|
1110
|
+
const prevHTML = treeEl.innerHTML;
|
|
1111
|
+
rebuildNodes();
|
|
1112
|
+
renderTree();
|
|
1113
|
+
if (treeEl.innerHTML !== prevHTML) renderAll();
|
|
1114
|
+
}, 15000);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1118
|
+
// Scroll detection
|
|
1119
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1120
|
+
|
|
1121
|
+
streamEl.addEventListener('scroll', () => {
|
|
1122
|
+
const atBottom = streamEl.scrollHeight - streamEl.scrollTop - streamEl.clientHeight < 50;
|
|
1123
|
+
if (atBottom && !autoScroll) autoScroll = true;
|
|
1124
|
+
if (!atBottom && autoScroll) autoScroll = false;
|
|
1125
|
+
refreshButtons();
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1129
|
+
// Helpers
|
|
1130
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1131
|
+
|
|
1132
|
+
function updateFilters() {
|
|
1133
|
+
filters.clear();
|
|
1134
|
+
for (const s of sessions) {
|
|
1135
|
+
for (const a of s.agents) {
|
|
1136
|
+
filters.set(s.id + ':' + a.id, true);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function agentDisplayName(id, type) {
|
|
1142
|
+
if (type) {
|
|
1143
|
+
const idx = type.lastIndexOf(':');
|
|
1144
|
+
if (idx >= 0 && idx < type.length - 1) return type.slice(idx + 1);
|
|
1145
|
+
return type;
|
|
1146
|
+
}
|
|
1147
|
+
if (!id) return 'Main';
|
|
1148
|
+
return 'Agent-' + id.slice(0, 7);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function folderName(projectPath) {
|
|
1152
|
+
if (!projectPath) return '';
|
|
1153
|
+
const parts = projectPath.split('/');
|
|
1154
|
+
return parts[parts.length - 1] || projectPath;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function esc(s) {
|
|
1158
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function fmtDur(ms) {
|
|
1162
|
+
if (!ms || ms <= 0) return '';
|
|
1163
|
+
if (ms < 1000) return `(${ms}ms)`;
|
|
1164
|
+
if (ms < 60000) return `(${(ms / 1000).toFixed(1)}s)`;
|
|
1165
|
+
return `(${(ms / 60000).toFixed(1)}m)`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function fmtTok(n) {
|
|
1169
|
+
if (!n) return '0';
|
|
1170
|
+
if (n < 1000) return String(n);
|
|
1171
|
+
if (n < 1000000) return (n / 1000).toFixed(1) + 'k';
|
|
1172
|
+
return (n / 1000000).toFixed(1) + 'm';
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function renderAll() {
|
|
1176
|
+
needsFullRender = true;
|
|
1177
|
+
renderTree();
|
|
1178
|
+
renderStream();
|
|
1179
|
+
refreshButtons();
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function scheduleRender() {
|
|
1183
|
+
if (!renderPending) {
|
|
1184
|
+
renderPending = true;
|
|
1185
|
+
requestAnimationFrame(() => {
|
|
1186
|
+
renderPending = false;
|
|
1187
|
+
renderAll();
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1193
|
+
// Init
|
|
1194
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1195
|
+
|
|
1196
|
+
// Apply collapse-after from URL param
|
|
1197
|
+
const urlParams = new URLSearchParams(location.search);
|
|
1198
|
+
const ca = urlParams.get('collapseAfter');
|
|
1199
|
+
if (ca) {
|
|
1200
|
+
applyCollapsePolicy(parseInt(ca) || 0);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
connect();
|
|
1204
|
+
</script>
|
|
1205
|
+
</body>
|
|
1206
|
+
</html>
|