agent-tracer 0.2.0
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 +21 -0
- package/README.md +100 -0
- package/bin/agent-trace-daemon.js +482 -0
- package/lib/db.js +173 -0
- package/lib/parser.js +279 -0
- package/lib/session-store.js +631 -0
- package/package.json +40 -0
- package/public/index.html +2812 -0
|
@@ -0,0 +1,2812 @@
|
|
|
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>Agent Trace</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117;
|
|
10
|
+
--surface: #161b22;
|
|
11
|
+
--surface2: #1c2128;
|
|
12
|
+
--hover: #21262d;
|
|
13
|
+
--selected: #1a2d45;
|
|
14
|
+
--border: #30363d;
|
|
15
|
+
--border-dim: #21262d;
|
|
16
|
+
--text: #e6edf3;
|
|
17
|
+
--muted: #8b949e;
|
|
18
|
+
--dim: #6e7681;
|
|
19
|
+
--blue: #58a6ff;
|
|
20
|
+
--blue-soft: #79c0ff;
|
|
21
|
+
--green: #3fb950;
|
|
22
|
+
--yellow: #d29922;
|
|
23
|
+
--red: #f85149;
|
|
24
|
+
--purple: #c084fc;
|
|
25
|
+
--orange: #fb923c;
|
|
26
|
+
--teal: #2dd4bf;
|
|
27
|
+
--sky: #38bdf8;
|
|
28
|
+
--gold: #fbbf24;
|
|
29
|
+
--pink: #f472b6;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
33
|
+
|
|
34
|
+
body {
|
|
35
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
36
|
+
background: var(--bg);
|
|
37
|
+
color: var(--text);
|
|
38
|
+
font-size: 13px;
|
|
39
|
+
line-height: 1.5;
|
|
40
|
+
-webkit-font-smoothing: antialiased;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
code, pre, .mono { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace; }
|
|
44
|
+
|
|
45
|
+
/* ── Header ── */
|
|
46
|
+
header {
|
|
47
|
+
position: sticky; top: 0; z-index: 10;
|
|
48
|
+
background: var(--surface);
|
|
49
|
+
border-bottom: 1px solid var(--border);
|
|
50
|
+
padding: 0 16px;
|
|
51
|
+
display: flex; align-items: center; gap: 8px;
|
|
52
|
+
height: 48px;
|
|
53
|
+
}
|
|
54
|
+
.brand {
|
|
55
|
+
display: flex; align-items: center; gap: 8px;
|
|
56
|
+
padding-right: 16px;
|
|
57
|
+
border-right: 1px solid var(--border);
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
}
|
|
60
|
+
.brand-icon { font-size: 15px; }
|
|
61
|
+
.brand-name { font-size: 13px; font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
|
|
62
|
+
|
|
63
|
+
.stats { display: flex; align-items: center; gap: 2px; flex: 1; }
|
|
64
|
+
.stat-pill {
|
|
65
|
+
display: flex; align-items: baseline; gap: 4px;
|
|
66
|
+
padding: 4px 10px; border-radius: 6px;
|
|
67
|
+
background: transparent;
|
|
68
|
+
transition: background 0.1s;
|
|
69
|
+
}
|
|
70
|
+
.stat-pill:hover { background: var(--hover); }
|
|
71
|
+
.stat-key { font-size: 11px; color: var(--dim); font-weight: 500; }
|
|
72
|
+
.stat-val { font-size: 12px; color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
73
|
+
.stat-val.cost { color: var(--green); }
|
|
74
|
+
|
|
75
|
+
.header-right { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-shrink: 0; }
|
|
76
|
+
.status-text { font-size: 11px; color: var(--muted); }
|
|
77
|
+
.pulse {
|
|
78
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
79
|
+
background: var(--dim); flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
.pulse.live {
|
|
82
|
+
background: var(--green);
|
|
83
|
+
box-shadow: 0 0 0 0 rgba(63,185,80,0.5);
|
|
84
|
+
animation: ripple 1.8s ease-out infinite;
|
|
85
|
+
}
|
|
86
|
+
@keyframes ripple {
|
|
87
|
+
0% { box-shadow: 0 0 0 0 rgba(63,185,80,0.45); }
|
|
88
|
+
70% { box-shadow: 0 0 0 6px rgba(63,185,80,0); }
|
|
89
|
+
100% { box-shadow: 0 0 0 0 rgba(63,185,80,0); }
|
|
90
|
+
}
|
|
91
|
+
@keyframes ripple-yellow {
|
|
92
|
+
0% { box-shadow: 0 0 0 0 rgba(210,153,34,0.6); }
|
|
93
|
+
70% { box-shadow: 0 0 0 7px rgba(210,153,34,0); }
|
|
94
|
+
100% { box-shadow: 0 0 0 0 rgba(210,153,34,0); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── Layout ── */
|
|
98
|
+
.layout { display: flex; height: calc(100vh - 48px); overflow: hidden; }
|
|
99
|
+
|
|
100
|
+
/* ── Projects panel ── */
|
|
101
|
+
#projects-panel {
|
|
102
|
+
border-bottom: 1px solid var(--border);
|
|
103
|
+
background: var(--surface);
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
}
|
|
106
|
+
.proj-header {
|
|
107
|
+
display: flex; align-items: center; gap: 6px;
|
|
108
|
+
padding: 6px 12px; cursor: pointer; user-select: none;
|
|
109
|
+
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
|
110
|
+
letter-spacing: 0.08em; color: var(--dim);
|
|
111
|
+
}
|
|
112
|
+
.proj-header:hover { color: var(--muted); }
|
|
113
|
+
.proj-chevron { font-size: 14px; transition: transform 0.15s; line-height: 1; }
|
|
114
|
+
.proj-header.collapsed .proj-chevron { transform: rotate(-90deg); }
|
|
115
|
+
#projects-list { overflow-y: auto; max-height: 280px; }
|
|
116
|
+
.proj-group { border-bottom: 1px solid var(--border-dim); }
|
|
117
|
+
.proj-folder-row {
|
|
118
|
+
display: flex; align-items: center; gap: 8px;
|
|
119
|
+
padding: 5px 12px; cursor: pointer; user-select: none;
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
}
|
|
122
|
+
.proj-folder-row:hover { background: var(--hover); }
|
|
123
|
+
.proj-folder-name { font-weight: 600; color: var(--text); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
124
|
+
.proj-folder-count { font-size: 9px; color: var(--dim); flex-shrink: 0; }
|
|
125
|
+
.proj-session-row {
|
|
126
|
+
display: flex; align-items: center; gap: 6px;
|
|
127
|
+
padding: 3px 12px 3px 28px; cursor: pointer; user-select: none;
|
|
128
|
+
font-size: 11px; color: var(--muted);
|
|
129
|
+
border-left: 2px solid transparent;
|
|
130
|
+
}
|
|
131
|
+
.proj-session-row:hover { background: var(--hover); color: var(--text); }
|
|
132
|
+
.proj-session-row.active { background: var(--selected); border-left-color: var(--blue); color: var(--text); }
|
|
133
|
+
.proj-session-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
134
|
+
.proj-session-cost { font-size: 9px; color: var(--green); flex-shrink: 0; font-variant-numeric: tabular-nums; }
|
|
135
|
+
.proj-session-age { font-size: 9px; color: var(--dim); flex-shrink: 0; font-variant-numeric: tabular-nums; }
|
|
136
|
+
.proj-status-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
|
137
|
+
|
|
138
|
+
/* ── Tree panel ── */
|
|
139
|
+
#tree-panel {
|
|
140
|
+
width: 440px; min-width: 260px; max-width: 600px;
|
|
141
|
+
border-right: 1px solid var(--border);
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
display: flex; flex-direction: column;
|
|
144
|
+
flex-shrink: 0;
|
|
145
|
+
}
|
|
146
|
+
#tree-scroll {
|
|
147
|
+
flex: 1; overflow-y: auto; overflow-x: hidden;
|
|
148
|
+
padding: 6px 0 24px;
|
|
149
|
+
}
|
|
150
|
+
.tree-placeholder {
|
|
151
|
+
padding: 48px 20px; text-align: center;
|
|
152
|
+
color: var(--muted); font-size: 12px; line-height: 2.2;
|
|
153
|
+
}
|
|
154
|
+
.tree-placeholder-icon { font-size: 26px; display: block; margin-bottom: 8px; opacity: 0.3; }
|
|
155
|
+
|
|
156
|
+
/* ── Agent group ── */
|
|
157
|
+
.agent-group { }
|
|
158
|
+
.agent-group.collapsed .tool-rows,
|
|
159
|
+
.agent-group.collapsed .agent-thinking,
|
|
160
|
+
.agent-group.collapsed .children-wrap,
|
|
161
|
+
.agent-group.collapsed .session-info { display: none !important; }
|
|
162
|
+
|
|
163
|
+
.chevron {
|
|
164
|
+
font-size: 13px; color: var(--muted); flex-shrink: 0;
|
|
165
|
+
margin-right: 5px; transition: transform 0.15s; display: inline-block;
|
|
166
|
+
width: 14px; text-align: center; line-height: 1;
|
|
167
|
+
}
|
|
168
|
+
.agent-group.collapsed .chevron { transform: rotate(-90deg); }
|
|
169
|
+
|
|
170
|
+
.session-info {
|
|
171
|
+
padding: 6px 12px 8px;
|
|
172
|
+
background: var(--surface2);
|
|
173
|
+
border-bottom: 1px solid var(--border-dim);
|
|
174
|
+
display: none;
|
|
175
|
+
}
|
|
176
|
+
.session-info.open { display: block; }
|
|
177
|
+
.session-info-grid {
|
|
178
|
+
display: grid; grid-template-columns: 80px 1fr; gap: 3px 10px;
|
|
179
|
+
font-size: 11px;
|
|
180
|
+
}
|
|
181
|
+
.si-key { color: var(--dim); }
|
|
182
|
+
.si-val { color: var(--muted); font-family: 'SF Mono','Fira Code',ui-monospace,monospace; word-break: break-all; }
|
|
183
|
+
|
|
184
|
+
.agent-row {
|
|
185
|
+
display: flex; align-items: center; gap: 0;
|
|
186
|
+
padding: 4px 10px 4px 0;
|
|
187
|
+
cursor: pointer; user-select: none;
|
|
188
|
+
border-left: 2px solid transparent;
|
|
189
|
+
min-height: 30px;
|
|
190
|
+
transition: background 0.1s;
|
|
191
|
+
position: relative;
|
|
192
|
+
}
|
|
193
|
+
.agent-row:hover { background: var(--hover); }
|
|
194
|
+
.agent-row.selected { background: var(--selected); border-left-color: var(--blue); }
|
|
195
|
+
|
|
196
|
+
/* indent spacer with guide lines */
|
|
197
|
+
.indent { display: flex; align-items: stretch; flex-shrink: 0; }
|
|
198
|
+
.guide-line {
|
|
199
|
+
width: 16px; height: 100%;
|
|
200
|
+
border-left: 1px solid var(--border-dim);
|
|
201
|
+
margin-left: 8px;
|
|
202
|
+
flex-shrink: 0;
|
|
203
|
+
}
|
|
204
|
+
.guide-stub { width: 16px; flex-shrink: 0; }
|
|
205
|
+
|
|
206
|
+
.agent-status { width: 16px; flex-shrink: 0; text-align: center; font-size: 11px; margin-right: 6px; }
|
|
207
|
+
.agent-status.running { color: var(--yellow); animation: spin 1.4s linear infinite; display: inline-block; }
|
|
208
|
+
.agent-status.done { color: var(--green); }
|
|
209
|
+
.agent-status.error { color: var(--red); }
|
|
210
|
+
.agent-status.cancelled { color: var(--dim); }
|
|
211
|
+
@keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
|
|
212
|
+
|
|
213
|
+
.agent-label { font-size: 12px; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
|
|
214
|
+
|
|
215
|
+
.agent-meta { display: flex; align-items: center; gap: 5px; flex-shrink: 0; margin-left: 6px; }
|
|
216
|
+
.tok-badge {
|
|
217
|
+
font-size: 10px; color: var(--dim);
|
|
218
|
+
display: flex; align-items: center; gap: 2px;
|
|
219
|
+
font-variant-numeric: tabular-nums;
|
|
220
|
+
}
|
|
221
|
+
.tok-icon { font-size: 9px; }
|
|
222
|
+
.dur-pill {
|
|
223
|
+
font-size: 10px; color: var(--muted);
|
|
224
|
+
background: var(--surface2); border: 1px solid var(--border-dim);
|
|
225
|
+
padding: 1px 5px; border-radius: 4px;
|
|
226
|
+
font-variant-numeric: tabular-nums;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ── Tool rows ── */
|
|
230
|
+
.tool-rows { }
|
|
231
|
+
|
|
232
|
+
.tool-row {
|
|
233
|
+
display: flex; align-items: center; gap: 0;
|
|
234
|
+
padding: 2px 8px 2px 0;
|
|
235
|
+
cursor: pointer; user-select: none;
|
|
236
|
+
border-left: 2px solid transparent;
|
|
237
|
+
min-height: 22px;
|
|
238
|
+
transition: background 0.1s;
|
|
239
|
+
}
|
|
240
|
+
.tool-row:hover { background: var(--hover); }
|
|
241
|
+
.tool-row.selected { background: var(--selected); border-left-color: var(--blue-soft); }
|
|
242
|
+
|
|
243
|
+
.tool-status { width: 14px; flex-shrink: 0; text-align: center; font-size: 10px; margin-right: 6px; }
|
|
244
|
+
.tool-status.done { color: var(--green); }
|
|
245
|
+
.tool-status.running { color: var(--yellow); animation: spin 1.4s linear infinite; display: inline-block; }
|
|
246
|
+
|
|
247
|
+
/* Type badge */
|
|
248
|
+
.type-badge {
|
|
249
|
+
font-size: 9px; font-weight: 700; letter-spacing: 0.05em;
|
|
250
|
+
padding: 1px 5px; border-radius: 4px;
|
|
251
|
+
flex-shrink: 0; margin-right: 7px;
|
|
252
|
+
text-transform: uppercase;
|
|
253
|
+
border: 1px solid transparent;
|
|
254
|
+
}
|
|
255
|
+
.tb-agent { background: #2d1b4e; color: var(--purple); border-color: #5a3a80; }
|
|
256
|
+
.tb-bash { background: #0d2918; color: var(--green); border-color: #1a5c30; }
|
|
257
|
+
.tb-read { background: #0a2236; color: var(--sky); border-color: #1a4a6e; }
|
|
258
|
+
.tb-write { background: #2d1a0a; color: var(--orange); border-color: #6b3a18; }
|
|
259
|
+
.tb-edit { background: #2a1f0a; color: var(--gold); border-color: #5a4210; }
|
|
260
|
+
.tb-search { background: #1a1a2e; color: var(--blue-soft); border-color: #2a3a6e; }
|
|
261
|
+
.tb-web { background: #0a2030; color: var(--teal); border-color: #1a4a50; }
|
|
262
|
+
.tb-compact { background: #0a2a28; color: #5eead4; border-color: #1a5050; }
|
|
263
|
+
.tb-default { background: var(--surface2); color: var(--muted); border-color: var(--border-dim); }
|
|
264
|
+
|
|
265
|
+
.tool-summary-text { font-size: 11px; color: var(--muted); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
266
|
+
.tool-dur-text { font-size: 10px; color: var(--dim); flex-shrink: 0; margin-left: 4px; font-variant-numeric: tabular-nums; }
|
|
267
|
+
|
|
268
|
+
/* thinking */
|
|
269
|
+
.agent-thinking {
|
|
270
|
+
font-size: 11px; color: var(--dim); font-style: italic;
|
|
271
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
272
|
+
padding: 2px 10px 5px 0;
|
|
273
|
+
animation: breathe 2.4s ease-in-out infinite;
|
|
274
|
+
}
|
|
275
|
+
@keyframes breathe { 0%,100%{opacity:0.4} 50%{opacity:0.85} }
|
|
276
|
+
|
|
277
|
+
/* ── Detail panel ── */
|
|
278
|
+
#detail-panel {
|
|
279
|
+
flex: 1; overflow-y: auto; overflow-x: hidden;
|
|
280
|
+
padding: 0;
|
|
281
|
+
background: var(--bg);
|
|
282
|
+
}
|
|
283
|
+
.detail-empty {
|
|
284
|
+
height: 100%; display: flex; flex-direction: column;
|
|
285
|
+
align-items: center; justify-content: center;
|
|
286
|
+
gap: 10px; color: var(--muted); font-size: 12px;
|
|
287
|
+
}
|
|
288
|
+
.detail-empty-icon { font-size: 28px; opacity: 0.2; }
|
|
289
|
+
|
|
290
|
+
.detail-body { padding: 24px 28px; }
|
|
291
|
+
|
|
292
|
+
.detail-title {
|
|
293
|
+
font-size: 15px; font-weight: 600; color: var(--text);
|
|
294
|
+
margin-bottom: 20px; display: flex; align-items: center; gap: 10px;
|
|
295
|
+
padding-bottom: 14px; border-bottom: 1px solid var(--border-dim);
|
|
296
|
+
}
|
|
297
|
+
.detail-title .type-badge { font-size: 10px; padding: 2px 7px; }
|
|
298
|
+
|
|
299
|
+
.section { margin-bottom: 24px; }
|
|
300
|
+
.section-label {
|
|
301
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em;
|
|
302
|
+
color: var(--dim); font-weight: 600; margin-bottom: 10px;
|
|
303
|
+
display: flex; align-items: center; gap: 8px;
|
|
304
|
+
}
|
|
305
|
+
.section-label::after { content: ''; flex: 1; height: 1px; background: var(--border-dim); }
|
|
306
|
+
|
|
307
|
+
.prop-grid { display: grid; grid-template-columns: 100px 1fr; row-gap: 8px; column-gap: 16px; }
|
|
308
|
+
.prop-key { font-size: 12px; color: var(--muted); }
|
|
309
|
+
.prop-val { font-size: 12px; color: var(--text); word-break: break-all; }
|
|
310
|
+
.prop-val.green { color: var(--green); }
|
|
311
|
+
.prop-val.yellow { color: var(--yellow); }
|
|
312
|
+
.prop-val.red { color: var(--red); }
|
|
313
|
+
.prop-val.blue { color: var(--blue-soft); }
|
|
314
|
+
.prop-val.mono { font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace; }
|
|
315
|
+
|
|
316
|
+
.status-chip {
|
|
317
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
318
|
+
padding: 2px 8px; border-radius: 5px; font-size: 11px; font-weight: 600;
|
|
319
|
+
border: 1px solid transparent;
|
|
320
|
+
}
|
|
321
|
+
.status-chip.running { background: #2d2006; color: var(--yellow); border-color: #5a4010; }
|
|
322
|
+
.status-chip.done { background: #0d2118; color: var(--green); border-color: #1a4a2e; }
|
|
323
|
+
.status-chip.error { background: #2d0f0f; color: var(--red); border-color: #5a1a1a; }
|
|
324
|
+
.status-chip.cancelled { background: var(--surface2); color: var(--muted); border-color: var(--border-dim); }
|
|
325
|
+
|
|
326
|
+
.code-block {
|
|
327
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
328
|
+
border-radius: 8px; padding: 14px;
|
|
329
|
+
overflow-x: auto; overflow-y: auto;
|
|
330
|
+
white-space: pre; /* preserve formatting, don't wrap */
|
|
331
|
+
font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
|
|
332
|
+
font-size: 11.5px; color: var(--text);
|
|
333
|
+
max-height: 380px; line-height: 1.6;
|
|
334
|
+
tab-size: 2;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* JSON syntax highlighting */
|
|
338
|
+
.jk { color: #79c0ff; } /* key */
|
|
339
|
+
.js { color: #a5d6ff; } /* string value */
|
|
340
|
+
.jn { color: #ffa657; } /* number */
|
|
341
|
+
.jb { color: #56d364; } /* boolean */
|
|
342
|
+
.jz { color: var(--muted); } /* null */
|
|
343
|
+
|
|
344
|
+
/* Payload field layout */
|
|
345
|
+
.payload-field { margin-bottom: 14px; }
|
|
346
|
+
.payload-field:last-child { margin-bottom: 0; }
|
|
347
|
+
.payload-field-label {
|
|
348
|
+
font-size: 10px; color: var(--dim); text-transform: uppercase;
|
|
349
|
+
letter-spacing: 0.08em; font-weight: 600; margin-bottom: 5px;
|
|
350
|
+
}
|
|
351
|
+
.payload-text {
|
|
352
|
+
white-space: pre-wrap; word-break: break-word;
|
|
353
|
+
font-size: 12px; color: var(--text); line-height: 1.65;
|
|
354
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
355
|
+
border-radius: 8px; padding: 12px 14px;
|
|
356
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
357
|
+
max-height: 320px; overflow-y: auto;
|
|
358
|
+
}
|
|
359
|
+
.payload-path {
|
|
360
|
+
font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
|
|
361
|
+
font-size: 11.5px; color: var(--teal);
|
|
362
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
363
|
+
border-radius: 6px; padding: 7px 12px;
|
|
364
|
+
word-break: break-all; white-space: pre-wrap;
|
|
365
|
+
}
|
|
366
|
+
.payload-diff-old {
|
|
367
|
+
background: #3f0d0d; border: 1px solid #7f1d1d;
|
|
368
|
+
border-radius: 8px; padding: 12px 14px;
|
|
369
|
+
white-space: pre-wrap; word-break: break-word;
|
|
370
|
+
font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
|
|
371
|
+
font-size: 11.5px; color: #fca5a5; line-height: 1.6;
|
|
372
|
+
max-height: 260px; overflow-y: auto;
|
|
373
|
+
}
|
|
374
|
+
.payload-diff-new {
|
|
375
|
+
background: #0d2a12; border: 1px solid #14532d;
|
|
376
|
+
border-radius: 8px; padding: 12px 14px;
|
|
377
|
+
white-space: pre-wrap; word-break: break-word;
|
|
378
|
+
font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
|
|
379
|
+
font-size: 11.5px; color: #86efac; line-height: 1.6;
|
|
380
|
+
max-height: 260px; overflow-y: auto;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* Copy button */
|
|
384
|
+
.code-wrap { position: relative; }
|
|
385
|
+
.code-copy {
|
|
386
|
+
position: absolute; top: 8px; right: 8px;
|
|
387
|
+
background: var(--surface2); border: 1px solid var(--border-dim);
|
|
388
|
+
color: var(--muted); font-size: 10px; padding: 3px 9px;
|
|
389
|
+
border-radius: 4px; cursor: pointer; opacity: 0;
|
|
390
|
+
transition: opacity 0.15s, color 0.15s; user-select: none;
|
|
391
|
+
}
|
|
392
|
+
.code-wrap:hover .code-copy { opacity: 1; }
|
|
393
|
+
.code-copy.copied { color: var(--teal); border-color: var(--teal); }
|
|
394
|
+
|
|
395
|
+
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border-dim); margin-bottom: 12px; }
|
|
396
|
+
.tab {
|
|
397
|
+
padding: 6px 14px; font-size: 12px; font-weight: 500;
|
|
398
|
+
color: var(--muted); cursor: pointer; border-bottom: 2px solid transparent;
|
|
399
|
+
margin-bottom: -1px; transition: color 0.1s;
|
|
400
|
+
}
|
|
401
|
+
.tab:hover { color: var(--text); }
|
|
402
|
+
.tab.active { color: var(--blue); border-bottom-color: var(--blue); }
|
|
403
|
+
.tab-body { display: none; }
|
|
404
|
+
.tab-body.active { display: block; }
|
|
405
|
+
|
|
406
|
+
.token-bar {
|
|
407
|
+
display: flex; gap: 8px; flex-wrap: wrap; margin-top: 2px;
|
|
408
|
+
}
|
|
409
|
+
.token-item {
|
|
410
|
+
display: flex; align-items: center; gap: 5px;
|
|
411
|
+
padding: 4px 10px; border-radius: 6px;
|
|
412
|
+
background: var(--surface2); border: 1px solid var(--border-dim);
|
|
413
|
+
}
|
|
414
|
+
.token-item-icon { font-size: 11px; color: var(--blue-soft); }
|
|
415
|
+
.token-item-label { font-size: 10px; color: var(--dim); }
|
|
416
|
+
.token-item-val { font-size: 12px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
|
|
417
|
+
|
|
418
|
+
/* ── Summary popover ── */
|
|
419
|
+
#summary-popover {
|
|
420
|
+
display: none;
|
|
421
|
+
position: fixed; top: 48px; left: 0; right: 0; bottom: 0;
|
|
422
|
+
z-index: 50;
|
|
423
|
+
}
|
|
424
|
+
#summary-popover.open { display: flex; }
|
|
425
|
+
.summary-backdrop {
|
|
426
|
+
position: absolute; inset: 0;
|
|
427
|
+
background: rgba(0,0,0,0.4);
|
|
428
|
+
}
|
|
429
|
+
.summary-panel {
|
|
430
|
+
position: relative; z-index: 1;
|
|
431
|
+
width: 420px; background: var(--surface);
|
|
432
|
+
border-right: 1px solid var(--border);
|
|
433
|
+
overflow-y: auto; padding: 20px;
|
|
434
|
+
animation: slideIn 0.15s ease;
|
|
435
|
+
}
|
|
436
|
+
@keyframes slideIn { from{transform:translateX(-12px);opacity:0} to{transform:none;opacity:1} }
|
|
437
|
+
.summary-title {
|
|
438
|
+
font-size: 13px; font-weight: 600; color: var(--text);
|
|
439
|
+
margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center;
|
|
440
|
+
}
|
|
441
|
+
.summary-close { cursor: pointer; color: var(--muted); font-size: 16px; line-height: 1; }
|
|
442
|
+
.summary-close:hover { color: var(--text); }
|
|
443
|
+
.summary-row {
|
|
444
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
445
|
+
padding: 7px 0; border-bottom: 1px solid var(--border-dim);
|
|
446
|
+
font-size: 12px;
|
|
447
|
+
}
|
|
448
|
+
.summary-row:last-child { border-bottom: none; }
|
|
449
|
+
.summary-row-label { color: var(--muted); }
|
|
450
|
+
.summary-row-val { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
|
|
451
|
+
.summary-row-val.green { color: var(--green); }
|
|
452
|
+
.summary-row-val.blue { color: var(--blue-soft); }
|
|
453
|
+
|
|
454
|
+
/* ── Nav tabs (below header) ── */
|
|
455
|
+
.nav-tabs {
|
|
456
|
+
display: flex; gap: 0;
|
|
457
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
458
|
+
padding: 0 16px;
|
|
459
|
+
}
|
|
460
|
+
.nav-tab {
|
|
461
|
+
padding: 8px 14px; font-size: 12px; font-weight: 500;
|
|
462
|
+
color: var(--muted); cursor: pointer;
|
|
463
|
+
border-bottom: 2px solid transparent; margin-bottom: -1px;
|
|
464
|
+
transition: color 0.1s;
|
|
465
|
+
}
|
|
466
|
+
.nav-tab:hover { color: var(--text); }
|
|
467
|
+
.nav-tab.active { color: var(--blue); border-bottom-color: var(--blue); }
|
|
468
|
+
|
|
469
|
+
/* ── History sidebar ── */
|
|
470
|
+
#history-panel {
|
|
471
|
+
width: 260px; flex-shrink: 0;
|
|
472
|
+
border-right: 1px solid var(--border);
|
|
473
|
+
overflow-y: auto; padding: 6px 0 24px;
|
|
474
|
+
}
|
|
475
|
+
.history-session {
|
|
476
|
+
padding: 8px 14px; cursor: pointer;
|
|
477
|
+
border-left: 2px solid transparent;
|
|
478
|
+
transition: background 0.1s;
|
|
479
|
+
}
|
|
480
|
+
.history-session:hover { background: var(--hover); }
|
|
481
|
+
.history-session.active { background: var(--selected); border-left-color: var(--blue); }
|
|
482
|
+
.history-session-label { font-size: 12px; font-weight: 500; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
483
|
+
.history-session-meta { display: flex; gap: 8px; margin-top: 2px; }
|
|
484
|
+
.history-session-date { font-size: 10px; color: var(--dim); }
|
|
485
|
+
.history-session-cost { font-size: 10px; color: var(--green); }
|
|
486
|
+
.history-status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; margin-top: 3px; }
|
|
487
|
+
.history-status-dot.running { background: var(--yellow); }
|
|
488
|
+
.history-status-dot.done { background: var(--green); }
|
|
489
|
+
.history-status-dot.error { background: var(--red); }
|
|
490
|
+
.history-section-header { padding: 10px 14px 4px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--dim); font-weight: 600; }
|
|
491
|
+
|
|
492
|
+
/* ── Permissions panel ── */
|
|
493
|
+
#permissions-view {
|
|
494
|
+
flex: 1; overflow-y: auto; padding: 24px 28px;
|
|
495
|
+
display: none;
|
|
496
|
+
}
|
|
497
|
+
#permissions-view.active { display: block; }
|
|
498
|
+
.perm-section { margin-bottom: 28px; }
|
|
499
|
+
.perm-group { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
500
|
+
.perm-chip {
|
|
501
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
502
|
+
padding: 3px 10px; border-radius: 5px;
|
|
503
|
+
font-size: 11px; font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
|
|
504
|
+
border: 1px solid transparent;
|
|
505
|
+
}
|
|
506
|
+
.perm-chip.allow { background: #0d2118; color: var(--green); border-color: #1a4a2e; }
|
|
507
|
+
.perm-chip.deny { background: #2d0f0f; color: var(--red); border-color: #5a1a1a; }
|
|
508
|
+
.perm-chip.ask { background: #2d2006; color: var(--yellow); border-color: #5a4010; }
|
|
509
|
+
.perm-chip.mode { background: var(--surface2); color: var(--blue-soft); border-color: var(--border); }
|
|
510
|
+
.perm-empty { color: var(--dim); font-size: 12px; font-style: italic; margin-top: 6px; }
|
|
511
|
+
.perm-tool-row {
|
|
512
|
+
display: flex; align-items: baseline; gap: 10px;
|
|
513
|
+
padding: 6px 0; border-bottom: 1px solid var(--border-dim);
|
|
514
|
+
}
|
|
515
|
+
.perm-tool-name { font-size: 12px; font-weight: 600; color: var(--text); min-width: 80px; }
|
|
516
|
+
.perm-tool-count { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
517
|
+
|
|
518
|
+
/* ── Page views ── */
|
|
519
|
+
#trace-view { display: flex; flex: 1; overflow: hidden; }
|
|
520
|
+
#trace-view.hidden { display: none; }
|
|
521
|
+
|
|
522
|
+
/* scrollbar */
|
|
523
|
+
::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
524
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
525
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
526
|
+
::-webkit-scrollbar-thumb:hover { background: #484f58; }
|
|
527
|
+
|
|
528
|
+
/* ── Search bar ── */
|
|
529
|
+
.tree-search-wrap {
|
|
530
|
+
padding: 8px 10px 4px;
|
|
531
|
+
border-bottom: 1px solid var(--border-dim);
|
|
532
|
+
position: sticky; top: 0; z-index: 2;
|
|
533
|
+
background: var(--bg);
|
|
534
|
+
}
|
|
535
|
+
.tree-search {
|
|
536
|
+
width: 100%; background: var(--surface2); border: 1px solid var(--border-dim);
|
|
537
|
+
border-radius: 6px; padding: 5px 10px 5px 28px;
|
|
538
|
+
color: var(--text); font-size: 12px; outline: none;
|
|
539
|
+
transition: border-color 0.15s;
|
|
540
|
+
font-family: inherit;
|
|
541
|
+
}
|
|
542
|
+
.tree-search:focus { border-color: var(--blue); }
|
|
543
|
+
.tree-search-icon {
|
|
544
|
+
position: absolute; left: 18px; top: 50%; transform: translateY(-50%);
|
|
545
|
+
color: var(--dim); font-size: 12px; pointer-events: none;
|
|
546
|
+
}
|
|
547
|
+
.tree-search-wrap { position: relative; }
|
|
548
|
+
|
|
549
|
+
/* ── Graph overlay (slides in from right) ── */
|
|
550
|
+
#graph-overlay {
|
|
551
|
+
display: none; width: 0; flex-shrink: 0; overflow: hidden;
|
|
552
|
+
transition: width 0.2s ease;
|
|
553
|
+
flex-direction: row;
|
|
554
|
+
}
|
|
555
|
+
#graph-overlay.open {
|
|
556
|
+
display: flex; width: 420px;
|
|
557
|
+
}
|
|
558
|
+
#graph-resize-handle {
|
|
559
|
+
width: 5px; flex-shrink: 0; cursor: col-resize;
|
|
560
|
+
background: transparent;
|
|
561
|
+
border-left: 1px solid var(--border-dim);
|
|
562
|
+
transition: background 0.1s;
|
|
563
|
+
}
|
|
564
|
+
#graph-resize-handle:hover { background: var(--border); }
|
|
565
|
+
.graph-backdrop { display: none; }
|
|
566
|
+
.graph-panel {
|
|
567
|
+
display: flex; flex-direction: column;
|
|
568
|
+
flex: 1; min-width: 0;
|
|
569
|
+
height: 100%; background: var(--surface);
|
|
570
|
+
border-left: 1px solid var(--border);
|
|
571
|
+
}
|
|
572
|
+
.graph-header {
|
|
573
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
574
|
+
padding: 12px 16px; border-bottom: 1px solid var(--border-dim);
|
|
575
|
+
flex-shrink: 0;
|
|
576
|
+
}
|
|
577
|
+
.graph-title { font-size: 13px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px; }
|
|
578
|
+
.graph-close { color: var(--muted); cursor: pointer; font-size: 16px; line-height: 1; padding: 2px 6px; border-radius: 4px; }
|
|
579
|
+
.graph-close:hover { background: var(--hover); color: var(--text); }
|
|
580
|
+
#graph-svg-wrap {
|
|
581
|
+
flex: 1; overflow: auto; padding: 24px;
|
|
582
|
+
}
|
|
583
|
+
#graph-svg-wrap svg {
|
|
584
|
+
display: block; min-width: 100%;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* graph node styles */
|
|
588
|
+
.gnode { cursor: pointer; }
|
|
589
|
+
.gnode rect {
|
|
590
|
+
fill: var(--surface2); stroke: var(--border); stroke-width: 1;
|
|
591
|
+
rx: 6;
|
|
592
|
+
}
|
|
593
|
+
.gnode.running rect { stroke: var(--yellow); }
|
|
594
|
+
.gnode.done rect { stroke: var(--green); }
|
|
595
|
+
.gnode.error rect { stroke: var(--red); }
|
|
596
|
+
.gnode.selected rect { fill: var(--selected); stroke: var(--blue); stroke-width: 2; }
|
|
597
|
+
.gnode.worktree rect { stroke: var(--teal); stroke-dasharray: 5 3; fill: #0d1f1f; }
|
|
598
|
+
.gnode.sidechain rect { stroke: var(--purple); stroke-dasharray: 4 3; fill: #140d1f; }
|
|
599
|
+
.gedge { fill: none; stroke: var(--border); stroke-width: 1.5; }
|
|
600
|
+
.gedge.worktree { stroke: var(--teal); stroke-dasharray: 5 3; }
|
|
601
|
+
.gedge.sidechain { stroke: var(--purple); stroke-dasharray: 4 3; }
|
|
602
|
+
|
|
603
|
+
/* ── Thread view ── */
|
|
604
|
+
.thread-msg { margin-bottom: 12px; }
|
|
605
|
+
.thread-msg.assistant { padding-left: 12px; border-left: 2px solid var(--border-dim); }
|
|
606
|
+
.thread-role { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
|
|
607
|
+
.thread-role.user { color: var(--blue-soft); }
|
|
608
|
+
.thread-role.assistant { color: var(--purple); }
|
|
609
|
+
.thread-text { font-size: 12px; color: var(--muted); white-space: pre-wrap; word-break: break-word; line-height: 1.6; }
|
|
610
|
+
|
|
611
|
+
/* graph zoom buttons */
|
|
612
|
+
.graph-zoom-btn {
|
|
613
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
614
|
+
width: 22px; height: 22px; border-radius: 4px; cursor: pointer;
|
|
615
|
+
font-size: 14px; color: var(--muted); line-height: 1;
|
|
616
|
+
transition: background 0.1s, color 0.1s;
|
|
617
|
+
}
|
|
618
|
+
.graph-zoom-btn:hover { background: var(--hover); color: var(--text); }
|
|
619
|
+
|
|
620
|
+
/* graph tooltip */
|
|
621
|
+
#graph-tooltip {
|
|
622
|
+
position: fixed; z-index: 200; pointer-events: none;
|
|
623
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
624
|
+
border-radius: 8px; padding: 10px 12px;
|
|
625
|
+
font-size: 11px; color: var(--text);
|
|
626
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
|
|
627
|
+
max-width: 240px; display: none;
|
|
628
|
+
line-height: 1.6;
|
|
629
|
+
}
|
|
630
|
+
#graph-tooltip.visible { display: block; }
|
|
631
|
+
|
|
632
|
+
/* graph btn in header */
|
|
633
|
+
.graph-btn {
|
|
634
|
+
display: flex; align-items: center; gap: 5px;
|
|
635
|
+
padding: 4px 10px; border-radius: 6px; cursor: pointer;
|
|
636
|
+
font-size: 11px; font-weight: 600; color: var(--muted);
|
|
637
|
+
background: var(--surface2); border: 1px solid var(--border-dim);
|
|
638
|
+
transition: all 0.1s; white-space: nowrap;
|
|
639
|
+
}
|
|
640
|
+
.graph-btn:hover { color: var(--text); border-color: var(--border); }
|
|
641
|
+
.graph-btn.active { color: var(--blue); border-color: var(--blue); background: #0a1e36; }
|
|
642
|
+
</style>
|
|
643
|
+
</head>
|
|
644
|
+
<body>
|
|
645
|
+
|
|
646
|
+
<header>
|
|
647
|
+
<div class="brand">
|
|
648
|
+
<span class="brand-icon">◈</span>
|
|
649
|
+
<span class="brand-name">Agent Trace</span>
|
|
650
|
+
</div>
|
|
651
|
+
<div class="stats">
|
|
652
|
+
<div class="stat-pill" onclick="openSummary('agents')" style="cursor:pointer"><span class="stat-key">Agents</span><span class="stat-val" id="s-agents">0</span></div>
|
|
653
|
+
<div class="stat-pill" onclick="openSummary('tools')" style="cursor:pointer"><span class="stat-key">Tool Calls</span><span class="stat-val" id="s-tools">0</span></div>
|
|
654
|
+
<div class="stat-pill" onclick="openSummary('tokens')" style="cursor:pointer"><span class="stat-key">In</span><span class="stat-val" id="s-in">0</span></div>
|
|
655
|
+
<div class="stat-pill" onclick="openSummary('tokens')" style="cursor:pointer"><span class="stat-key">Out</span><span class="stat-val" id="s-out">0</span></div>
|
|
656
|
+
<div class="stat-pill" onclick="openSummary('tokens')" style="cursor:pointer"><span class="stat-key">Cache</span><span class="stat-val" id="s-cache">0</span></div>
|
|
657
|
+
<div class="stat-pill" onclick="openSummary('cost')" style="cursor:pointer"><span class="stat-key">Cost</span><span class="stat-val cost" id="s-cost">$0.0000</span></div>
|
|
658
|
+
</div>
|
|
659
|
+
<div class="header-right">
|
|
660
|
+
<div class="graph-btn" id="graph-btn" onclick="toggleGraph()">⬡ Graph</div>
|
|
661
|
+
<div class="pulse" id="pulse"></div>
|
|
662
|
+
</div>
|
|
663
|
+
</header>
|
|
664
|
+
|
|
665
|
+
<!-- Graph overlay -->
|
|
666
|
+
<!-- Summary popover -->
|
|
667
|
+
<div id="summary-popover">
|
|
668
|
+
<div class="summary-backdrop" onclick="closeSummary()"></div>
|
|
669
|
+
<div class="summary-panel" id="summary-panel-content"></div>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<div class="nav-tabs">
|
|
673
|
+
<div class="nav-tab active" onclick="showView('trace')">Trace</div>
|
|
674
|
+
<div class="nav-tab" onclick="showView('history')">History</div>
|
|
675
|
+
<div class="nav-tab" onclick="showView('permissions')">Permissions</div>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<div class="layout" style="height:calc(100vh - 84px)">
|
|
679
|
+
<!-- Trace view -->
|
|
680
|
+
<div id="trace-view" style="display:flex;flex:1;overflow:hidden">
|
|
681
|
+
<div id="tree-panel" style="display:flex;flex-direction:column;overflow:hidden">
|
|
682
|
+
<!-- Projects panel -->
|
|
683
|
+
<div id="projects-panel">
|
|
684
|
+
<div class="proj-header" id="proj-header" onclick="toggleProjects()">
|
|
685
|
+
<span class="proj-chevron" id="proj-chevron">▾</span>
|
|
686
|
+
<span>Projects</span>
|
|
687
|
+
<span id="proj-count" style="margin-left:4px"></span>
|
|
688
|
+
</div>
|
|
689
|
+
<div id="projects-list" style="display:none"></div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<!-- Search + tree -->
|
|
693
|
+
<div style="flex:1;overflow-y:auto;overflow-x:hidden;padding:6px 0 24px" id="tree-scroll">
|
|
694
|
+
<div class="tree-search-wrap">
|
|
695
|
+
<span class="tree-search-icon">⌕</span>
|
|
696
|
+
<input class="tree-search" id="tree-search" type="text" placeholder="Filter agents and tools…" oninput="applySearch(this.value)">
|
|
697
|
+
</div>
|
|
698
|
+
<div class="tree-placeholder" id="tree-placeholder">
|
|
699
|
+
<span class="tree-placeholder-icon">◌</span>
|
|
700
|
+
Waiting for session…
|
|
701
|
+
<br><span style="color:var(--dim);font-size:11px">Run Claude Code to start</span>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
<div id="panel-resize-handle" style="width:5px;cursor:col-resize;flex-shrink:0;background:transparent;border-right:1px solid var(--border-dim);transition:background 0.1s" onmouseenter="this.style.background='var(--border)'" onmouseleave="this.style.background='transparent'"></div>
|
|
706
|
+
<div id="detail-panel">
|
|
707
|
+
<div class="detail-empty">
|
|
708
|
+
<div class="detail-empty-icon">⊡</div>
|
|
709
|
+
<div>Select an agent or tool call</div>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<!-- History view -->
|
|
715
|
+
<div id="history-view" style="display:none;flex:1;overflow:hidden">
|
|
716
|
+
<div id="history-panel">
|
|
717
|
+
<div class="history-section-header">Past Sessions</div>
|
|
718
|
+
<div id="history-list"><div style="padding:20px 14px;color:var(--dim);font-size:12px">Loading…</div></div>
|
|
719
|
+
</div>
|
|
720
|
+
<div id="history-detail" style="flex:1;overflow-y:auto;padding:24px 28px">
|
|
721
|
+
<div class="detail-empty"><div class="detail-empty-icon">⊡</div><div>Select a session</div></div>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
<!-- Permissions view -->
|
|
726
|
+
<div id="permissions-view" style="display:none;flex:1;overflow-y:auto;padding:24px 28px;flex-direction:column">
|
|
727
|
+
<div style="font-size:15px;font-weight:600;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid var(--border-dim)">Permissions Overview</div>
|
|
728
|
+
<div id="permissions-content"><div style="color:var(--dim);font-size:12px">Loading…</div></div>
|
|
729
|
+
</div>
|
|
730
|
+
|
|
731
|
+
<!-- Graph side panel — sits inside layout as a flex sibling, pushes content left -->
|
|
732
|
+
<div id="graph-overlay">
|
|
733
|
+
<div id="graph-resize-handle"></div>
|
|
734
|
+
<div class="graph-panel">
|
|
735
|
+
<div class="graph-header">
|
|
736
|
+
<span class="graph-title">⬡ Agent Graph</span>
|
|
737
|
+
<div style="display:flex;align-items:center;gap:4px">
|
|
738
|
+
<span class="graph-zoom-btn" onclick="graphZoom(0.2)" title="Zoom in">+</span>
|
|
739
|
+
<span class="graph-zoom-btn" onclick="graphZoom(-0.2)" title="Zoom out">−</span>
|
|
740
|
+
<span class="graph-zoom-btn" onclick="graphFit()" title="Fit to view">⊡</span>
|
|
741
|
+
<span class="graph-close" onclick="closeGraph()">✕</span>
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
<div id="graph-svg-wrap" style="position:relative"></div>
|
|
745
|
+
<div id="graph-tooltip"></div>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
|
|
750
|
+
<script>
|
|
751
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
752
|
+
let selected = null; // { type: 'agent'|'tool', sessionId, toolId? }
|
|
753
|
+
let lastRoot = null;
|
|
754
|
+
let lastRoots = []; // all roots from last SSE — needed to find nodes in non-primary roots
|
|
755
|
+
let raf = null;
|
|
756
|
+
|
|
757
|
+
// DOM element maps for flicker-free diffing
|
|
758
|
+
const agentEls = new Map(); // sessionId → { group, row, toolRows, thinking, childrenWrap }
|
|
759
|
+
const toolEls = new Map(); // `${sessionId}:${toolId}` → rowEl
|
|
760
|
+
|
|
761
|
+
// ── SSE ──────────────────────────────────────────────────────────────────
|
|
762
|
+
let es;
|
|
763
|
+
function connect() {
|
|
764
|
+
if (es) { es.close(); es = null; }
|
|
765
|
+
es = new EventSource('/events');
|
|
766
|
+
es.onmessage = e => {
|
|
767
|
+
const msg = JSON.parse(e.data);
|
|
768
|
+
if (msg.disconnected) { markCancelled(); setStatus('done'); return; }
|
|
769
|
+
const { root, roots, totals } = msg;
|
|
770
|
+
lastRoots = roots || (root ? [root] : []);
|
|
771
|
+
lastRoot = lastRoots[0] || null;
|
|
772
|
+
if (raf) cancelAnimationFrame(raf);
|
|
773
|
+
raf = requestAnimationFrame(() => {
|
|
774
|
+
updateHeader(totals);
|
|
775
|
+
patchTree(lastRoot);
|
|
776
|
+
if (selected) renderDetail();
|
|
777
|
+
setStatus(lastRoot?.status === 'running' ? 'live' : 'done');
|
|
778
|
+
maybeAutoOpenGraph(lastRoot);
|
|
779
|
+
if (searchQuery) applySearchToTree(lastRoot);
|
|
780
|
+
scheduleProjectRefresh();
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
es.onerror = () => {
|
|
784
|
+
setStatus('done');
|
|
785
|
+
markCancelled();
|
|
786
|
+
es.close();
|
|
787
|
+
setTimeout(connect, 3000);
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function setStatus(state) {
|
|
792
|
+
const pulse = document.getElementById('pulse');
|
|
793
|
+
if (pulse) pulse.className = state === 'live' ? 'pulse live' : 'pulse';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function markCancelled() {
|
|
797
|
+
if (!lastRoot) return;
|
|
798
|
+
function walk(n) {
|
|
799
|
+
if (!n) return;
|
|
800
|
+
if (n.status === 'running') { n.status = 'cancelled'; n.endedAt = Date.now(); }
|
|
801
|
+
for (const tc of n.toolCalls) { if (!tc.done) tc.done = true; }
|
|
802
|
+
for (const c of (n.children||[])) walk(c);
|
|
803
|
+
}
|
|
804
|
+
walk(lastRoot);
|
|
805
|
+
patchTree(lastRoot);
|
|
806
|
+
if (selected) renderDetail();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
connect();
|
|
810
|
+
loadProjects(); // load project list on startup
|
|
811
|
+
|
|
812
|
+
// ── Event delegation (replaces unsafe inline onclick string interpolation) ─
|
|
813
|
+
document.addEventListener('click', e => {
|
|
814
|
+
// Summary rows → navigate to agent
|
|
815
|
+
const gotoEl = e.target.closest('[data-goto-sid]');
|
|
816
|
+
if (gotoEl) { closeSummary(); showView('trace'); selectAgent(gotoEl.dataset.gotoSid); return; }
|
|
817
|
+
// Graph nodes → select agent
|
|
818
|
+
const graphEl = e.target.closest('[data-graph-sid]');
|
|
819
|
+
if (graphEl) { selectAgent(graphEl.dataset.graphSid); renderGraph(); return; }
|
|
820
|
+
// Thread reload button
|
|
821
|
+
const reloadEl = e.target.closest('[data-reload-thread]');
|
|
822
|
+
if (reloadEl) { threadCache.delete(reloadEl.dataset.reloadThread); loadThread(reloadEl.dataset.reloadThread, reloadEl.dataset.threadContainer); return; }
|
|
823
|
+
// History session rows
|
|
824
|
+
const histEl = e.target.closest('[data-history-sid]');
|
|
825
|
+
if (histEl) { loadHistorySession(histEl.dataset.historySid); return; }
|
|
826
|
+
// Log filter chips
|
|
827
|
+
const chipEl = e.target.closest('.log-filter-chip[data-filter-name]');
|
|
828
|
+
if (chipEl) {
|
|
829
|
+
applyLogFilter(chipEl.dataset.filterName, chipEl.dataset.logList, chipEl);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// ── Panel resize handle ───────────────────────────────────────────────────
|
|
835
|
+
(function() {
|
|
836
|
+
const handle = document.getElementById('panel-resize-handle');
|
|
837
|
+
const treePanel = document.getElementById('tree-panel');
|
|
838
|
+
if (!handle || !treePanel) return;
|
|
839
|
+
let dragging = false, startX = 0, startW = 0;
|
|
840
|
+
handle.addEventListener('mousedown', e => {
|
|
841
|
+
dragging = true;
|
|
842
|
+
startX = e.clientX;
|
|
843
|
+
startW = treePanel.offsetWidth;
|
|
844
|
+
document.body.style.cursor = 'col-resize';
|
|
845
|
+
document.body.style.userSelect = 'none';
|
|
846
|
+
});
|
|
847
|
+
document.addEventListener('mousemove', e => {
|
|
848
|
+
if (!dragging) return;
|
|
849
|
+
const w = Math.max(180, Math.min(700, startW + (e.clientX - startX)));
|
|
850
|
+
treePanel.style.width = w + 'px';
|
|
851
|
+
treePanel.style.minWidth = w + 'px';
|
|
852
|
+
treePanel.style.maxWidth = w + 'px';
|
|
853
|
+
});
|
|
854
|
+
document.addEventListener('mouseup', () => {
|
|
855
|
+
if (!dragging) return;
|
|
856
|
+
dragging = false;
|
|
857
|
+
document.body.style.cursor = '';
|
|
858
|
+
document.body.style.userSelect = '';
|
|
859
|
+
});
|
|
860
|
+
})();
|
|
861
|
+
|
|
862
|
+
// ── Graph resize handle ───────────────────────────────────────────────────
|
|
863
|
+
(function() {
|
|
864
|
+
const handle = document.getElementById('graph-resize-handle');
|
|
865
|
+
const overlay = document.getElementById('graph-overlay');
|
|
866
|
+
if (!handle || !overlay) return;
|
|
867
|
+
let dragging = false, startX = 0, startW = 0;
|
|
868
|
+
handle.addEventListener('mousedown', e => {
|
|
869
|
+
dragging = true;
|
|
870
|
+
startX = e.clientX;
|
|
871
|
+
startW = overlay.offsetWidth;
|
|
872
|
+
document.body.style.cursor = 'col-resize';
|
|
873
|
+
document.body.style.userSelect = 'none';
|
|
874
|
+
e.preventDefault();
|
|
875
|
+
});
|
|
876
|
+
document.addEventListener('mousemove', e => {
|
|
877
|
+
if (!dragging) return;
|
|
878
|
+
// Dragging left = larger graph (startX - e.clientX increases width)
|
|
879
|
+
const w = Math.max(260, Math.min(900, startW + (startX - e.clientX)));
|
|
880
|
+
overlay.style.width = w + 'px';
|
|
881
|
+
overlay.style.transition = 'none';
|
|
882
|
+
});
|
|
883
|
+
document.addEventListener('mouseup', () => {
|
|
884
|
+
if (!dragging) return;
|
|
885
|
+
dragging = false;
|
|
886
|
+
document.body.style.cursor = '';
|
|
887
|
+
document.body.style.userSelect = '';
|
|
888
|
+
overlay.style.transition = '';
|
|
889
|
+
});
|
|
890
|
+
})();
|
|
891
|
+
|
|
892
|
+
// ── Header ────────────────────────────────────────────────────────────────
|
|
893
|
+
function updateHeader(t) {
|
|
894
|
+
setText('s-agents', t.agents);
|
|
895
|
+
setText('s-tools', t.tools);
|
|
896
|
+
setText('s-in', fmt(t.tokensIn));
|
|
897
|
+
setText('s-out', fmt(t.tokensOut));
|
|
898
|
+
setText('s-cache', fmt(t.cacheRead));
|
|
899
|
+
setText('s-cost', '$' + t.cost.toFixed(4));
|
|
900
|
+
}
|
|
901
|
+
function setText(id, v) {
|
|
902
|
+
const el = document.getElementById(id);
|
|
903
|
+
if (el && el.textContent !== String(v)) el.textContent = v;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ── Formatters ────────────────────────────────────────────────────────────
|
|
907
|
+
function fmt(n) { return n >= 1000 ? (n/1000).toFixed(1)+'k' : String(n); }
|
|
908
|
+
function dur(ms) { return ms >= 1000 ? (ms/1000).toFixed(1)+'s' : ms+'ms'; }
|
|
909
|
+
function esc(s) {
|
|
910
|
+
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
911
|
+
}
|
|
912
|
+
function toolBadgeClass(name) {
|
|
913
|
+
if (name === '__compact__') return 'tb-compact';
|
|
914
|
+
if (name === 'Agent' || name === 'Task') return 'tb-agent';
|
|
915
|
+
if (name === 'Bash') return 'tb-bash';
|
|
916
|
+
if (name === 'Read') return 'tb-read';
|
|
917
|
+
if (name === 'Write') return 'tb-write';
|
|
918
|
+
if (name === 'Edit' || name === 'NotebookEdit') return 'tb-edit';
|
|
919
|
+
if (name === 'Grep' || name === 'Glob') return 'tb-search';
|
|
920
|
+
if (name === 'WebSearch' || name === 'WebFetch') return 'tb-web';
|
|
921
|
+
return 'tb-default';
|
|
922
|
+
}
|
|
923
|
+
function toolBadgeLabel(name, summary) {
|
|
924
|
+
if (name === '__compact__') return 'COMPACT';
|
|
925
|
+
if (name === 'Bash' && summary) {
|
|
926
|
+
// Extract the actual CLI tool from the command
|
|
927
|
+
const cmd = summary.trim().split(/\s+/)[0].replace(/^[!$]/, '');
|
|
928
|
+
const known = { npm:'NPM', npx:'NPX', node:'NODE', git:'GIT', curl:'CURL',
|
|
929
|
+
python:'PY', python3:'PY', pip:'PIP', pip3:'PIP',
|
|
930
|
+
brew:'BREW', ls:'LS', cat:'CAT', echo:'ECHO', cp:'CP',
|
|
931
|
+
mv:'MV', rm:'RM', mkdir:'MKDIR', find:'FIND', grep:'GREP',
|
|
932
|
+
sqlite3:'SQL', jq:'JQ', launchctl:'LCTL', open:'OPEN',
|
|
933
|
+
bash:'BASH', sh:'SH', zsh:'ZSH', code:'CODE' };
|
|
934
|
+
const base = cmd.split('/').pop();
|
|
935
|
+
return known[base] || known[cmd] || cmd.slice(0,5).toUpperCase() || 'BASH';
|
|
936
|
+
}
|
|
937
|
+
const map = { Agent:'AGENT', Task:'TASK', Bash:'BASH', Read:'READ', Write:'WRITE',
|
|
938
|
+
Edit:'EDIT', NotebookEdit:'NB', Grep:'GREP', Glob:'GLOB',
|
|
939
|
+
WebSearch:'WEB', WebFetch:'FETCH' };
|
|
940
|
+
return map[name] || name.slice(0,4).toUpperCase();
|
|
941
|
+
}
|
|
942
|
+
function statusIcon(s) {
|
|
943
|
+
return s === 'running' ? '◌' : s === 'done' ? '✓' : s === 'error' ? '✗' : '○';
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ── Tree: flicker-free patch ──────────────────────────────────────────────
|
|
947
|
+
function patchTree(root) {
|
|
948
|
+
const panel = document.getElementById('tree-scroll');
|
|
949
|
+
const placeholder = document.getElementById('tree-placeholder');
|
|
950
|
+
|
|
951
|
+
if (!root) {
|
|
952
|
+
if (placeholder) placeholder.style.display = '';
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (placeholder) placeholder.style.display = 'none';
|
|
956
|
+
|
|
957
|
+
// Walk the tree and upsert every node
|
|
958
|
+
const seen = new Set();
|
|
959
|
+
walkNode(root, panel, 0, seen);
|
|
960
|
+
|
|
961
|
+
// Remove stale agents no longer in tree
|
|
962
|
+
for (const [sid, els] of agentEls) {
|
|
963
|
+
if (!seen.has(sid)) {
|
|
964
|
+
els.group.remove();
|
|
965
|
+
agentEls.delete(sid);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function walkNode(node, container, depth, seen) {
|
|
971
|
+
seen.add(node.sessionId);
|
|
972
|
+
const key = node.sessionId;
|
|
973
|
+
|
|
974
|
+
let els = agentEls.get(key);
|
|
975
|
+
if (!els) {
|
|
976
|
+
els = createAgentEls(node, depth);
|
|
977
|
+
container.appendChild(els.group);
|
|
978
|
+
agentEls.set(key, els);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Patch agent row in-place
|
|
982
|
+
patchAgentRow(els, node, depth);
|
|
983
|
+
|
|
984
|
+
// Patch tool rows
|
|
985
|
+
patchToolRows(els, node, depth);
|
|
986
|
+
|
|
987
|
+
// Patch thinking line
|
|
988
|
+
patchThinking(els, node, depth);
|
|
989
|
+
|
|
990
|
+
// Recurse into children — they live inside els.childrenWrap
|
|
991
|
+
for (const child of (node.children || [])) {
|
|
992
|
+
walkNode(child, els.childrenWrap, depth + 1, seen);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Track collapsed state per session (persists across re-renders)
|
|
997
|
+
const collapsedSessions = new Set();
|
|
998
|
+
|
|
999
|
+
function createAgentEls(node, depth) {
|
|
1000
|
+
const group = document.createElement('div');
|
|
1001
|
+
group.className = 'agent-group';
|
|
1002
|
+
group.dataset.sid = node.sessionId;
|
|
1003
|
+
|
|
1004
|
+
const row = document.createElement('div');
|
|
1005
|
+
row.className = 'agent-row';
|
|
1006
|
+
row.innerHTML = agentRowHTML(node, depth);
|
|
1007
|
+
// Left-click selects; chevron click (first 20px) toggles collapse
|
|
1008
|
+
row.onclick = (e) => {
|
|
1009
|
+
const x = e.clientX - row.getBoundingClientRect().left;
|
|
1010
|
+
const chevronEnd = 12 + depth * 16 + 28; // indent + chevron width
|
|
1011
|
+
if (x < chevronEnd) {
|
|
1012
|
+
toggleCollapse(node.sessionId, group);
|
|
1013
|
+
} else {
|
|
1014
|
+
selectAgent(node.sessionId);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
group.appendChild(row);
|
|
1018
|
+
|
|
1019
|
+
// Session info strip (hidden by default)
|
|
1020
|
+
const sessionInfo = document.createElement('div');
|
|
1021
|
+
sessionInfo.className = 'session-info';
|
|
1022
|
+
sessionInfo.innerHTML = sessionInfoHTML(node);
|
|
1023
|
+
group.appendChild(sessionInfo);
|
|
1024
|
+
|
|
1025
|
+
const toolRows = document.createElement('div');
|
|
1026
|
+
toolRows.className = 'tool-rows';
|
|
1027
|
+
group.appendChild(toolRows);
|
|
1028
|
+
|
|
1029
|
+
const thinking = document.createElement('div');
|
|
1030
|
+
thinking.className = 'agent-thinking';
|
|
1031
|
+
thinking.style.display = 'none';
|
|
1032
|
+
group.appendChild(thinking);
|
|
1033
|
+
|
|
1034
|
+
const childrenWrap = document.createElement('div');
|
|
1035
|
+
childrenWrap.className = 'children-wrap';
|
|
1036
|
+
group.appendChild(childrenWrap);
|
|
1037
|
+
|
|
1038
|
+
return { group, row, toolRows, thinking, childrenWrap, sessionInfo };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function toggleCollapse(sessionId, group) {
|
|
1042
|
+
if (collapsedSessions.has(sessionId)) {
|
|
1043
|
+
collapsedSessions.delete(sessionId);
|
|
1044
|
+
group.classList.remove('collapsed');
|
|
1045
|
+
// Show session info when expanding
|
|
1046
|
+
group.querySelector('.session-info')?.classList.add('open');
|
|
1047
|
+
} else {
|
|
1048
|
+
collapsedSessions.add(sessionId);
|
|
1049
|
+
group.classList.add('collapsed');
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function sessionInfoHTML(node) {
|
|
1054
|
+
const started = new Date(node.startedAt).toLocaleString();
|
|
1055
|
+
const ended = node.endedAt ? new Date(node.endedAt).toLocaleTimeString() : '—';
|
|
1056
|
+
const cost = node.costUsd > 0 ? `$${node.costUsd.toFixed(4)}` : '—';
|
|
1057
|
+
const elapsed = node.endedAt ? dur(node.endedAt - node.startedAt) : dur(Date.now() - node.startedAt) + ' (running)';
|
|
1058
|
+
const toks = node.tokens.input + node.tokens.output;
|
|
1059
|
+
const rows = [
|
|
1060
|
+
['Session ID', `<span style="word-break:break-all">${esc(node.sessionId)}</span>`],
|
|
1061
|
+
['Status', node.status],
|
|
1062
|
+
['Started', started],
|
|
1063
|
+
['Duration', elapsed],
|
|
1064
|
+
['Cost', `<span style="color:var(--green)">${cost}</span>`],
|
|
1065
|
+
toks > 0 ? ['Tokens', `↑${fmt(node.tokens.input)} ↓${fmt(node.tokens.output)} ◎${fmt(node.tokens.cacheRead)}`] : null,
|
|
1066
|
+
node.permissionMode ? ['Mode', esc(node.permissionMode)] : null,
|
|
1067
|
+
node.entrypoint ? ['Entrypoint', esc(node.entrypoint)] : null,
|
|
1068
|
+
node.version ? ['Version', `v${esc(node.version)}`] : null,
|
|
1069
|
+
node.gitBranch ? ['Branch', esc(node.gitBranch)] : null,
|
|
1070
|
+
node.cwd ? ['CWD', `<span style="font-size:10px;word-break:break-all">${esc(node.cwd)}</span>`] : null,
|
|
1071
|
+
].filter(Boolean);
|
|
1072
|
+
return `<div class="session-info-grid">${rows.map(([k,v]) =>
|
|
1073
|
+
`<span class="si-key">${k}</span><span class="si-val">${v}</span>`
|
|
1074
|
+
).join('')}</div>`;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const MODE_BADGE = {
|
|
1078
|
+
default: { label: 'default', color: 'var(--dim)', bg: 'var(--surface2)', border: 'var(--border-dim)' },
|
|
1079
|
+
acceptEdits: { label: 'auto-edit', color: 'var(--blue-soft)', bg: '#0a1e36', border: '#1a3a60' },
|
|
1080
|
+
auto: { label: 'auto', color: 'var(--yellow)', bg: '#2d2006', border: '#5a4010' },
|
|
1081
|
+
bypassPermissions: { label: '⚡ bypass', color: 'var(--red)', bg: '#2d0f0f', border: '#5a1a1a' },
|
|
1082
|
+
plan: { label: 'plan', color: 'var(--purple)', bg: '#2d1b4e', border: '#5a3a80' },
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
function agentRowHTML(node, depth) {
|
|
1086
|
+
const indent = indentHTML(depth);
|
|
1087
|
+
const chevron = `<span class="chevron">▾</span>`;
|
|
1088
|
+
|
|
1089
|
+
// Status dot: pulsing for running, solid for ended
|
|
1090
|
+
const dotColor = node.status === 'running' ? 'var(--yellow)'
|
|
1091
|
+
: node.status === 'done' ? 'var(--green)'
|
|
1092
|
+
: node.status === 'error' ? 'var(--red)' : 'var(--dim)';
|
|
1093
|
+
const dotAnim = node.status === 'running' ? 'animation:ripple-yellow 1.8s ease-out infinite' : '';
|
|
1094
|
+
const dot = `<span style="width:7px;height:7px;border-radius:50%;background:${dotColor};flex-shrink:0;${dotAnim}"></span>`;
|
|
1095
|
+
|
|
1096
|
+
const label = `<span class="agent-label">${esc(node.label)}</span>`;
|
|
1097
|
+
|
|
1098
|
+
// Type badge: worktree / sidechain / permission mode
|
|
1099
|
+
let typeBadge = '';
|
|
1100
|
+
if (node.isWorktree) {
|
|
1101
|
+
typeBadge = `<span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:#0d1f1f;color:var(--teal);border:1px solid #1a4040;flex-shrink:0;letter-spacing:0.04em">⎇ worktree</span>`;
|
|
1102
|
+
} else if (node.isSidechain) {
|
|
1103
|
+
typeBadge = `<span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:#140d1f;color:var(--purple);border:1px solid #3a1f5a;flex-shrink:0;letter-spacing:0.04em">⊕ inline</span>`;
|
|
1104
|
+
} else {
|
|
1105
|
+
const m = MODE_BADGE[node.permissionMode] || (node.permissionMode ? { label: esc(node.permissionMode), color: 'var(--muted)', bg: 'var(--surface2)', border: 'var(--border-dim)' } : null);
|
|
1106
|
+
if (m) typeBadge = `<span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:${m.bg};color:${m.color};border:1px solid ${m.border};flex-shrink:0;letter-spacing:0.04em">${m.label}</span>`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const toks = (node.tokens.input + node.tokens.output > 0)
|
|
1110
|
+
? `<span class="tok-badge"><span class="tok-icon">◎</span>${fmt(node.tokens.input + node.tokens.output)}</span>` : '';
|
|
1111
|
+
const d = node.endedAt ? `<span class="dur-pill">${dur(node.endedAt - node.startedAt)}</span>` : '';
|
|
1112
|
+
const cost = node.costUsd > 0 ? `<span class="dur-pill" style="color:var(--green)">$${node.costUsd.toFixed(3)}</span>` : '';
|
|
1113
|
+
const startTs = node.startedAt ? `<span style="font-size:9px;color:var(--dim);flex-shrink:0;font-variant-numeric:tabular-nums">${new Date(node.startedAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'})}</span>` : '';
|
|
1114
|
+
const meta = `<span class="agent-meta">${typeBadge}${toks}${d}${cost}${startTs}</span>`;
|
|
1115
|
+
return indent + chevron + dot + `<span style="width:6px;flex-shrink:0"></span>` + label + meta;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function indentHTML(depth) {
|
|
1119
|
+
if (depth === 0) return '<span class="guide-stub"></span>';
|
|
1120
|
+
let html = '<span class="indent">';
|
|
1121
|
+
for (let i = 0; i < depth; i++) {
|
|
1122
|
+
html += `<span class="guide-line"></span>`;
|
|
1123
|
+
}
|
|
1124
|
+
html += '</span>';
|
|
1125
|
+
return html;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function patchAgentRow(els, node, depth) {
|
|
1129
|
+
const row = els.row;
|
|
1130
|
+
const isSelected = selected?.type === 'agent' && selected?.sessionId === node.sessionId;
|
|
1131
|
+
const cls = 'agent-row' + (isSelected ? ' selected' : '');
|
|
1132
|
+
if (row.className !== cls) row.className = cls;
|
|
1133
|
+
|
|
1134
|
+
// Rebuild innerHTML only when key fields change — track via data attrs
|
|
1135
|
+
const newStatus = node.status;
|
|
1136
|
+
const newLabel = node.label;
|
|
1137
|
+
const newToks = fmt(node.tokens.input + node.tokens.output);
|
|
1138
|
+
const newDur = node.endedAt ? dur(node.endedAt - node.startedAt) : '';
|
|
1139
|
+
|
|
1140
|
+
const newCost = node.costUsd > 0 ? node.costUsd.toFixed(4) : '';
|
|
1141
|
+
const newMode = node.permissionMode || '';
|
|
1142
|
+
// For running sessions use current elapsed time so it always updates
|
|
1143
|
+
const newElapsed = node.status === 'running' ? String(Math.floor((Date.now() - node.startedAt) / 1000)) : '';
|
|
1144
|
+
if (row.dataset.status !== newStatus || row.dataset.label !== newLabel ||
|
|
1145
|
+
row.dataset.toks !== newToks || row.dataset.dur !== newDur ||
|
|
1146
|
+
row.dataset.depth !== String(depth) || row.dataset.cost !== newCost ||
|
|
1147
|
+
row.dataset.mode !== newMode || row.dataset.elapsed !== newElapsed) {
|
|
1148
|
+
row.innerHTML = agentRowHTML(node, depth);
|
|
1149
|
+
row.onclick = (e) => {
|
|
1150
|
+
const x = e.clientX - row.getBoundingClientRect().left;
|
|
1151
|
+
const chevronEnd = 12 + depth * 16 + 28;
|
|
1152
|
+
if (x < chevronEnd) toggleCollapse(node.sessionId, els.group);
|
|
1153
|
+
else selectAgent(node.sessionId);
|
|
1154
|
+
};
|
|
1155
|
+
row.dataset.status = newStatus;
|
|
1156
|
+
row.dataset.label = newLabel;
|
|
1157
|
+
row.dataset.toks = newToks;
|
|
1158
|
+
row.dataset.dur = newDur;
|
|
1159
|
+
row.dataset.depth = depth;
|
|
1160
|
+
row.dataset.cost = newCost;
|
|
1161
|
+
row.dataset.mode = newMode;
|
|
1162
|
+
row.dataset.elapsed = newElapsed;
|
|
1163
|
+
// Refresh session info strip
|
|
1164
|
+
if (els.sessionInfo) els.sessionInfo.innerHTML = sessionInfoHTML(node);
|
|
1165
|
+
}
|
|
1166
|
+
// Apply collapse state
|
|
1167
|
+
els.group.classList.toggle('collapsed', collapsedSessions.has(node.sessionId));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function patchToolRows(els, node, depth) {
|
|
1171
|
+
const container = els.toolRows;
|
|
1172
|
+
const toolIndent = depth + 1; // tools are visually one level deeper
|
|
1173
|
+
|
|
1174
|
+
const seenTools = new Set();
|
|
1175
|
+
|
|
1176
|
+
// Interleave tool calls and compaction events by timestamp — newest first
|
|
1177
|
+
// Only show compacts that occurred during (or after) this session, not pre-session backfills
|
|
1178
|
+
const sessionStart = node.startedAt || 0;
|
|
1179
|
+
const items = [
|
|
1180
|
+
...node.toolCalls.map(tc => ({ kind: 'tool', data: tc, ts: tc.startedAt || 0 })),
|
|
1181
|
+
...(node.compactions || []).filter(c => c.timestamp >= sessionStart).map(c => ({ kind: 'compact', data: c, ts: c.timestamp })),
|
|
1182
|
+
].sort((a, b) => b.ts - a.ts); // descending = newest first
|
|
1183
|
+
|
|
1184
|
+
for (const item of items) {
|
|
1185
|
+
if (item.kind === 'compact') {
|
|
1186
|
+
const c = item.data;
|
|
1187
|
+
const tkey = `${node.sessionId}:${c.id}`;
|
|
1188
|
+
seenTools.add(tkey);
|
|
1189
|
+
|
|
1190
|
+
let rowEl = toolEls.get(tkey);
|
|
1191
|
+
if (!rowEl) {
|
|
1192
|
+
rowEl = document.createElement('div');
|
|
1193
|
+
rowEl.className = 'tool-row';
|
|
1194
|
+
rowEl.dataset.tkey = tkey;
|
|
1195
|
+
rowEl.dataset.ts = String(item.ts);
|
|
1196
|
+
rowEl.onclick = e => { e.stopPropagation(); selectCompact(node.sessionId, c.id); };
|
|
1197
|
+
toolEls.set(tkey, rowEl);
|
|
1198
|
+
}
|
|
1199
|
+
patchCompactRow(rowEl, node, c, toolIndent);
|
|
1200
|
+
} else {
|
|
1201
|
+
const tc = item.data;
|
|
1202
|
+
const tkey = `${node.sessionId}:${tc.id}`;
|
|
1203
|
+
seenTools.add(tkey);
|
|
1204
|
+
|
|
1205
|
+
let rowEl = toolEls.get(tkey);
|
|
1206
|
+
if (!rowEl) {
|
|
1207
|
+
rowEl = document.createElement('div');
|
|
1208
|
+
rowEl.className = 'tool-row';
|
|
1209
|
+
rowEl.dataset.tkey = tkey;
|
|
1210
|
+
rowEl.dataset.ts = String(item.ts);
|
|
1211
|
+
rowEl.onclick = e => { e.stopPropagation(); selectTool(node.sessionId, tc.id); };
|
|
1212
|
+
toolEls.set(tkey, rowEl);
|
|
1213
|
+
}
|
|
1214
|
+
patchToolRow(rowEl, node, tc, toolIndent);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Sync DOM order to match sorted items (newest first)
|
|
1219
|
+
for (const item of items) {
|
|
1220
|
+
const kind = item.kind;
|
|
1221
|
+
const id = kind === 'compact' ? item.data.id : item.data.id;
|
|
1222
|
+
const tkey = `${node.sessionId}:${id}`;
|
|
1223
|
+
const rowEl = toolEls.get(tkey);
|
|
1224
|
+
if (rowEl) container.appendChild(rowEl); // move to end = last processed = oldest; reverse by prepending
|
|
1225
|
+
}
|
|
1226
|
+
// Re-order: prepend in reverse so newest ends up at top
|
|
1227
|
+
const orderedKeys = items.map(i => `${node.sessionId}:${i.data.id}`);
|
|
1228
|
+
for (let i = orderedKeys.length - 1; i >= 0; i--) {
|
|
1229
|
+
const rowEl = toolEls.get(orderedKeys[i]);
|
|
1230
|
+
if (rowEl) container.prepend(rowEl);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Remove stale tools/compacts from this node's container
|
|
1234
|
+
for (const [tkey, rowEl] of toolEls) {
|
|
1235
|
+
if (tkey.startsWith(node.sessionId + ':') && !seenTools.has(tkey)) {
|
|
1236
|
+
rowEl.remove();
|
|
1237
|
+
toolEls.delete(tkey);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function patchCompactRow(rowEl, node, c, depth) {
|
|
1243
|
+
const isSelected = selected?.type === 'compact' &&
|
|
1244
|
+
selected?.sessionId === node.sessionId &&
|
|
1245
|
+
selected?.compactId === c.id;
|
|
1246
|
+
const cls = 'tool-row' + (isSelected ? ' selected' : '');
|
|
1247
|
+
if (rowEl.className !== cls) rowEl.className = cls;
|
|
1248
|
+
|
|
1249
|
+
if (rowEl.dataset.cid !== c.id) {
|
|
1250
|
+
const indent = indentHTML(depth);
|
|
1251
|
+
const savings = (c.tokensBefore && c.tokensAfter)
|
|
1252
|
+
? `−${fmt(c.tokensBefore - c.tokensAfter)} tok ${fmt(c.tokensBefore)}→${fmt(c.tokensAfter)}`
|
|
1253
|
+
: c.tokensBefore
|
|
1254
|
+
? `${fmt(c.tokensBefore)} tok compacted`
|
|
1255
|
+
: 'context compacted';
|
|
1256
|
+
const cts = c.timestamp ? new Date(c.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
1257
|
+
rowEl.innerHTML = `${indent}<span class="tool-status done" style="color:var(--teal)">↺</span>` +
|
|
1258
|
+
`<span class="type-badge tb-compact">COMPACT</span>` +
|
|
1259
|
+
`<span class="tool-summary-text" style="color:var(--teal);opacity:0.8">${esc(savings)}</span>` +
|
|
1260
|
+
`${cts ? `<span style="font-size:9px;color:var(--dim);flex-shrink:0;margin-left:5px;font-variant-numeric:tabular-nums">${cts}</span>` : ''}`;
|
|
1261
|
+
rowEl.dataset.cid = c.id;
|
|
1262
|
+
rowEl.dataset.depth = depth;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Extract a compact display string from a tool summary (strip long paths to basename)
|
|
1267
|
+
function shortSummary(name, summary) {
|
|
1268
|
+
if (!summary) return '';
|
|
1269
|
+
// File-based tools: show just the filename
|
|
1270
|
+
if (['Read','Write','Edit','NotebookEdit'].includes(name)) {
|
|
1271
|
+
const base = summary.replace(/\\/g,'/').split('/').pop();
|
|
1272
|
+
return base || summary;
|
|
1273
|
+
}
|
|
1274
|
+
// Bash: strip leading path prefixes, keep command meaningful
|
|
1275
|
+
if (name === 'Bash') {
|
|
1276
|
+
return summary
|
|
1277
|
+
.replace(/\/Users\/[^\s]+?\/(Desktop|home|Documents)\/[^\s/]+\//g, '…/')
|
|
1278
|
+
.slice(0, 72);
|
|
1279
|
+
}
|
|
1280
|
+
// Grep/Glob: truncate pattern
|
|
1281
|
+
if (name === 'Grep' || name === 'Glob') return summary.slice(0, 50);
|
|
1282
|
+
return summary.slice(0, 60);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function patchToolRow(rowEl, node, tc, depth) {
|
|
1286
|
+
const isSelected = selected?.type === 'tool' &&
|
|
1287
|
+
selected?.sessionId === node.sessionId &&
|
|
1288
|
+
selected?.toolId === tc.id;
|
|
1289
|
+
const cls = 'tool-row' + (isSelected ? ' selected' : '');
|
|
1290
|
+
if (rowEl.className !== cls) rowEl.className = cls;
|
|
1291
|
+
|
|
1292
|
+
// If session has ended but tool was never marked done (daemon restart / missed PostToolUse),
|
|
1293
|
+
// treat it as done in the UI to avoid infinite spinner.
|
|
1294
|
+
const effectiveDone = tc.done || (node.status !== 'running');
|
|
1295
|
+
const newDone = String(effectiveDone);
|
|
1296
|
+
const newSummary = tc.summary;
|
|
1297
|
+
const newDur = tc.durationMs ? dur(tc.durationMs) : '';
|
|
1298
|
+
|
|
1299
|
+
if (rowEl.dataset.done !== newDone || rowEl.dataset.summary !== newSummary ||
|
|
1300
|
+
rowEl.dataset.dur !== newDur || rowEl.dataset.depth !== String(depth)) {
|
|
1301
|
+
const indent = indentHTML(depth);
|
|
1302
|
+
const statusCls = effectiveDone ? 'done' : 'running';
|
|
1303
|
+
const statusChar = effectiveDone ? '✓' : '◌';
|
|
1304
|
+
const badge = toolBadgeClass(tc.name);
|
|
1305
|
+
const badgeTxt = toolBadgeLabel(tc.name, tc.summary);
|
|
1306
|
+
const short = shortSummary(tc.name, tc.summary);
|
|
1307
|
+
const ts = tc.startedAt ? new Date(tc.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
1308
|
+
const isDestructive = tc.name === 'Bash' && tc.summary &&
|
|
1309
|
+
/\brm\s+-[a-zA-Z]*[rf]|\bDROP\s+(?:TABLE|DATABASE)\b|\bDELETE\s+FROM\b|\bTRUNCATE\s+(?:TABLE\b)?|\bgit\s+reset\s+--hard\b|\bgit\s+push\b[^|&;]*-(?:-force\b|f\b)|\bgit\s+branch\s+-[Dd]\b|\bkill\s+-9\b|\b(?:pkill|wipefs|mkfs|shred)\b|\bdd\s+if=/i.test(tc.summary);
|
|
1310
|
+
const summaryStyle = isDestructive ? 'color:var(--red)' : '';
|
|
1311
|
+
rowEl.innerHTML = `${indent}<span class="tool-status ${statusCls}">${statusChar}</span>` +
|
|
1312
|
+
`<span class="type-badge ${badge}"${isDestructive ? ' style="background:#2d0f0f;color:var(--red);border-color:#5a1a1a"' : ''}>${badgeTxt}</span>` +
|
|
1313
|
+
`<span class="tool-summary-text"${summaryStyle ? ` style="${summaryStyle}"` : ''} title="${esc(tc.summary)}">${esc(short)}</span>` +
|
|
1314
|
+
`<span class="tool-dur-text">${newDur}</span>` +
|
|
1315
|
+
`${ts ? `<span style="font-size:9px;color:var(--dim);flex-shrink:0;margin-left:4px;font-variant-numeric:tabular-nums">${ts}</span>` : ''}`;
|
|
1316
|
+
rowEl.dataset.done = newDone;
|
|
1317
|
+
rowEl.dataset.summary = newSummary;
|
|
1318
|
+
rowEl.dataset.dur = newDur;
|
|
1319
|
+
rowEl.dataset.depth = depth;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function patchThinking(els, node, depth) {
|
|
1324
|
+
const t = els.thinking;
|
|
1325
|
+
if (node.lastText && node.status === 'running') {
|
|
1326
|
+
const txt = '▸ ' + node.lastText.slice(-120).replace(/\n/g, ' ');
|
|
1327
|
+
if (t.dataset.txt !== txt) {
|
|
1328
|
+
t.textContent = txt;
|
|
1329
|
+
t.dataset.txt = txt;
|
|
1330
|
+
const padLeft = (12 + depth * 16 + 20) + 'px';
|
|
1331
|
+
t.style.paddingLeft = padLeft;
|
|
1332
|
+
}
|
|
1333
|
+
t.style.display = '';
|
|
1334
|
+
} else {
|
|
1335
|
+
t.style.display = 'none';
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// ── Selection ─────────────────────────────────────────────────────────────
|
|
1340
|
+
function selectAgent(sessionId) {
|
|
1341
|
+
selected = { type: 'agent', sessionId };
|
|
1342
|
+
patchTree(lastRoot);
|
|
1343
|
+
renderDetail();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function selectTool(sessionId, toolId) {
|
|
1347
|
+
selected = { type: 'tool', sessionId, toolId };
|
|
1348
|
+
patchTree(lastRoot);
|
|
1349
|
+
renderDetail();
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function selectCompact(sessionId, compactId) {
|
|
1353
|
+
selected = { type: 'compact', sessionId, compactId };
|
|
1354
|
+
patchTree(lastRoot);
|
|
1355
|
+
renderDetail();
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function applyLogFilter(name, listId, chipEl) {
|
|
1359
|
+
const list = document.getElementById(listId);
|
|
1360
|
+
if (!list) return;
|
|
1361
|
+
const filterBar = chipEl.parentElement;
|
|
1362
|
+
const clearBtn = document.getElementById(filterBar?.id + '-clear') ||
|
|
1363
|
+
filterBar?.parentElement?.querySelector('[id$="-clear"]');
|
|
1364
|
+
|
|
1365
|
+
// Toggle: clicking same chip again clears filter
|
|
1366
|
+
const isActive = chipEl.dataset.active === '1';
|
|
1367
|
+
// Reset all chips
|
|
1368
|
+
filterBar?.querySelectorAll('.log-filter-chip').forEach(c => {
|
|
1369
|
+
c.dataset.active = '0';
|
|
1370
|
+
c.style.opacity = '0.55';
|
|
1371
|
+
c.style.outline = 'none';
|
|
1372
|
+
});
|
|
1373
|
+
if (isActive) {
|
|
1374
|
+
// Clear — show all
|
|
1375
|
+
list.querySelectorAll('.log-row').forEach(r => r.style.display = '');
|
|
1376
|
+
if (clearBtn) clearBtn.style.display = 'none';
|
|
1377
|
+
filterBar?.querySelectorAll('.log-filter-chip').forEach(c => c.style.opacity = '1');
|
|
1378
|
+
} else {
|
|
1379
|
+
// Apply filter
|
|
1380
|
+
chipEl.dataset.active = '1';
|
|
1381
|
+
chipEl.style.opacity = '1';
|
|
1382
|
+
chipEl.style.outline = '2px solid currentColor';
|
|
1383
|
+
list.querySelectorAll('.log-row').forEach(r => {
|
|
1384
|
+
r.style.display = r.dataset.toolName === name ? '' : 'none';
|
|
1385
|
+
});
|
|
1386
|
+
if (clearBtn) clearBtn.style.display = '';
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function clearLogFilter(filterId, listId) {
|
|
1391
|
+
const list = document.getElementById(listId);
|
|
1392
|
+
if (list) list.querySelectorAll('.log-row').forEach(r => r.style.display = '');
|
|
1393
|
+
const filterBar = document.getElementById(filterId);
|
|
1394
|
+
if (filterBar) {
|
|
1395
|
+
filterBar.querySelectorAll('.log-filter-chip').forEach(c => {
|
|
1396
|
+
c.dataset.active = '0'; c.style.opacity = '1'; c.style.outline = 'none';
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
const clearBtn = document.getElementById(filterId + '-clear');
|
|
1400
|
+
if (clearBtn) clearBtn.style.display = 'none';
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function findNode(root, sessionId) {
|
|
1404
|
+
if (!root) return null;
|
|
1405
|
+
if (root.sessionId === sessionId) return root;
|
|
1406
|
+
for (const c of (root.children || [])) {
|
|
1407
|
+
const f = findNode(c, sessionId);
|
|
1408
|
+
if (f) return f;
|
|
1409
|
+
}
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
// Search across all roots (for history / non-primary sessions)
|
|
1413
|
+
function findNodeAcrossRoots(sessionId) {
|
|
1414
|
+
for (const r of lastRoots) {
|
|
1415
|
+
const f = findNode(r, sessionId);
|
|
1416
|
+
if (f) return f;
|
|
1417
|
+
}
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ── Detail panel ──────────────────────────────────────────────────────────
|
|
1422
|
+
function renderDetail() {
|
|
1423
|
+
const panel = document.getElementById('detail-panel');
|
|
1424
|
+
if (!selected || !lastRoot) {
|
|
1425
|
+
panel.innerHTML = '<div class="detail-empty"><div class="detail-empty-icon">⊡</div><div>Select an agent or tool call</div></div>';
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const node = findNode(lastRoot, selected.sessionId) || findNodeAcrossRoots(selected.sessionId);
|
|
1429
|
+
if (!node) return;
|
|
1430
|
+
|
|
1431
|
+
if (selected.type === 'agent') {
|
|
1432
|
+
panel.innerHTML = '<div class="detail-body">' + agentDetailHTML(node) + '</div>';
|
|
1433
|
+
// Restore or auto-load thread; invalidate cache for live sessions
|
|
1434
|
+
const threadTabId = `thread-tab-${node.sessionId}`;
|
|
1435
|
+
if (node.status === 'running') threadCache.delete(node.sessionId);
|
|
1436
|
+
if (threadCache.has(node.sessionId)) {
|
|
1437
|
+
const threadEl = document.getElementById(threadTabId);
|
|
1438
|
+
if (threadEl) threadEl.innerHTML = threadCache.get(node.sessionId);
|
|
1439
|
+
} else {
|
|
1440
|
+
loadThread(node.sessionId, threadTabId);
|
|
1441
|
+
}
|
|
1442
|
+
} else if (selected.type === 'compact') {
|
|
1443
|
+
const c = (node.compactions || []).find(x => x.id === selected.compactId);
|
|
1444
|
+
if (c) panel.innerHTML = '<div class="detail-body">' + compactDetailHTML(node, c) + '</div>';
|
|
1445
|
+
} else {
|
|
1446
|
+
const tc = node.toolCalls.find(t => t.id === selected.toolId);
|
|
1447
|
+
if (tc) panel.innerHTML = '<div class="detail-body">' + toolDetailHTML(node, tc) + '</div>';
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function agentDetailHTML(node) {
|
|
1452
|
+
const elapsed = node.endedAt
|
|
1453
|
+
? dur(node.endedAt - node.startedAt)
|
|
1454
|
+
: dur(Date.now() - node.startedAt) + ' (running)';
|
|
1455
|
+
|
|
1456
|
+
const badgeCls = toolBadgeClass('Agent');
|
|
1457
|
+
const threadTabId = `thread-tab-${node.sessionId}`;
|
|
1458
|
+
|
|
1459
|
+
const toolRows = node.toolCalls.map(tc => {
|
|
1460
|
+
const bc = toolBadgeClass(tc.name);
|
|
1461
|
+
const bl = toolBadgeLabel(tc.name, tc.summary);
|
|
1462
|
+
const d = tc.durationMs ? `<span style="color:var(--green);margin-left:4px">${dur(tc.durationMs)}</span>` : '';
|
|
1463
|
+
return `<div style="display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--border-dim)">
|
|
1464
|
+
<span class="type-badge ${bc}" style="flex-shrink:0">${bl}</span>
|
|
1465
|
+
<span style="font-size:12px;color:var(--muted);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(tc.summary)}</span>
|
|
1466
|
+
${d}
|
|
1467
|
+
</div>`;
|
|
1468
|
+
}).join('');
|
|
1469
|
+
|
|
1470
|
+
// Project folder
|
|
1471
|
+
const projectFolder = node.cwd ? (() => {
|
|
1472
|
+
const parts = node.cwd.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
1473
|
+
const name = parts[parts.length - 1] || node.cwd;
|
|
1474
|
+
const parent = parts.length > 1 ? parts[parts.length - 2] : '';
|
|
1475
|
+
return { name, parent, full: node.cwd };
|
|
1476
|
+
})() : null;
|
|
1477
|
+
|
|
1478
|
+
// Type-based summary for logs filter
|
|
1479
|
+
const toolTypeMap = new Map();
|
|
1480
|
+
for (const tc of node.toolCalls) {
|
|
1481
|
+
toolTypeMap.set(tc.name, (toolTypeMap.get(tc.name) || 0) + 1);
|
|
1482
|
+
}
|
|
1483
|
+
const sortedTypes = [...toolTypeMap.entries()].sort((a,b) => b[1]-a[1]);
|
|
1484
|
+
const logFilterId = `log-filter-${node.sessionId}`;
|
|
1485
|
+
const logListId = `log-list-${node.sessionId}`;
|
|
1486
|
+
|
|
1487
|
+
const typeFilterChips = sortedTypes.map(([name, count]) => {
|
|
1488
|
+
const bc = toolBadgeClass(name);
|
|
1489
|
+
const bl = toolBadgeLabel(name, '');
|
|
1490
|
+
return `<span class="log-filter-chip ${bc}" data-filter-name="${esc(name)}" data-log-list="${logListId}"
|
|
1491
|
+
style="cursor:pointer;font-size:9px;font-weight:700;padding:2px 7px;border-radius:4px;letter-spacing:0.05em;user-select:none"
|
|
1492
|
+
title="Filter to ${esc(name)}">${bl} <span style="opacity:0.7">${count}</span></span>`;
|
|
1493
|
+
}).join('');
|
|
1494
|
+
|
|
1495
|
+
const logRows = node.toolCalls.map((tc, i) => {
|
|
1496
|
+
const bc = toolBadgeClass(tc.name);
|
|
1497
|
+
const bl = toolBadgeLabel(tc.name, tc.summary);
|
|
1498
|
+
const d = tc.durationMs ? `<span style="font-size:9px;color:var(--green);flex-shrink:0">${dur(tc.durationMs)}</span>` : '';
|
|
1499
|
+
const ts = tc.startedAt ? new Date(tc.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
1500
|
+
const isDestructive = tc.name === 'Bash' && tc.summary &&
|
|
1501
|
+
/\brm\s+-[a-zA-Z]*[rf]|\bDROP\s+|\bDELETE\s+FROM\b|\bTRUNCATE\b|\bgit\s+reset\s+--hard|\bgit\s+push\b[^|&;]*-(?:-force\b|f\b)|\bkill\s+-9\b|\b(?:pkill|wipefs|mkfs|shred)\b/i.test(tc.summary);
|
|
1502
|
+
const statusDot = tc.done
|
|
1503
|
+
? `<span style="width:5px;height:5px;border-radius:50%;background:${isDestructive?'var(--red)':'var(--green)'};flex-shrink:0"></span>`
|
|
1504
|
+
: `<span style="width:5px;height:5px;border-radius:50%;background:var(--yellow);flex-shrink:0;animation:ripple 1.4s ease-out infinite"></span>`;
|
|
1505
|
+
const summaryColor = isDestructive ? 'color:var(--red)' : 'color:var(--muted)';
|
|
1506
|
+
return `<div class="log-row" data-tool-name="${esc(tc.name)}" data-log-list="${logListId}"
|
|
1507
|
+
style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-bottom:1px solid var(--border-dim);min-height:0;cursor:pointer"
|
|
1508
|
+
onclick="selectTool('${esc(node.sessionId)}','${esc(tc.id)}')">
|
|
1509
|
+
<span style="font-size:9px;color:var(--dim);width:20px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums">${i+1}</span>
|
|
1510
|
+
${statusDot}
|
|
1511
|
+
<span class="type-badge ${bc}" style="font-size:8px;padding:1px 4px;flex-shrink:0;border-radius:3px${isDestructive?';background:#2d0f0f;color:var(--red);border-color:#5a1a1a':''}">${bl}</span>
|
|
1512
|
+
<span style="font-size:11px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;${summaryColor}" title="${esc(tc.summary)}">${esc(tc.summary)}</span>
|
|
1513
|
+
${d}
|
|
1514
|
+
${ts ? `<span style="font-size:9px;color:var(--dim);flex-shrink:0;font-variant-numeric:tabular-nums">${ts}</span>` : ''}
|
|
1515
|
+
</div>`;
|
|
1516
|
+
}).join('');
|
|
1517
|
+
|
|
1518
|
+
return `
|
|
1519
|
+
<div class="detail-title">
|
|
1520
|
+
<span class="type-badge ${badgeCls}">AGENT</span>
|
|
1521
|
+
${esc(node.label)}
|
|
1522
|
+
</div>
|
|
1523
|
+
|
|
1524
|
+
<div class="section">
|
|
1525
|
+
<div class="section-label">Overview</div>
|
|
1526
|
+
<div class="prop-grid">
|
|
1527
|
+
<span class="prop-key">Status</span>
|
|
1528
|
+
<span class="prop-val"><span class="status-chip ${node.status}">${node.status}</span></span>
|
|
1529
|
+
<span class="prop-key">Duration</span>
|
|
1530
|
+
<span class="prop-val">${elapsed}</span>
|
|
1531
|
+
<span class="prop-key">Cost</span>
|
|
1532
|
+
<span class="prop-val green">$${node.costUsd.toFixed(4)}</span>
|
|
1533
|
+
${node.isSidechain ? `<span class="prop-key">Type</span><span class="prop-val" style="color:var(--purple)">⊕ Inline sidechain</span>` : ''}
|
|
1534
|
+
${node.isWorktree ? `<span class="prop-key">Type</span><span class="prop-val" style="color:var(--teal)">⎇ Worktree</span>` : ''}
|
|
1535
|
+
${node.permissionMode && !node.isSidechain && !node.isWorktree ? `<span class="prop-key">Mode</span><span class="prop-val">${esc(node.permissionMode)}</span>` : ''}
|
|
1536
|
+
${node.gitBranch ? `<span class="prop-key">Branch</span><span class="prop-val mono" style="color:var(--green)">⎇ ${esc(node.gitBranch)}</span>` : ''}
|
|
1537
|
+
<span class="prop-key mono" style="font-size:11px">Session ID</span>
|
|
1538
|
+
<span class="prop-val mono" style="font-size:10px;color:var(--dim);word-break:break-all">${esc(node.sessionId)}</span>
|
|
1539
|
+
</div>
|
|
1540
|
+
</div>
|
|
1541
|
+
|
|
1542
|
+
${projectFolder ? `
|
|
1543
|
+
<div class="section">
|
|
1544
|
+
<div class="section-label">Project</div>
|
|
1545
|
+
<div style="display:flex;align-items:flex-start;gap:10px;padding:10px 12px;background:var(--surface2);border:1px solid var(--border-dim);border-radius:8px">
|
|
1546
|
+
<span style="font-size:18px;line-height:1;flex-shrink:0;margin-top:1px">📁</span>
|
|
1547
|
+
<div style="flex:1;min-width:0">
|
|
1548
|
+
<div style="font-size:13px;font-weight:600;color:var(--text)">${esc(projectFolder.name)}</div>
|
|
1549
|
+
<div style="font-size:10px;color:var(--dim);margin-top:2px;word-break:break-all" title="${esc(projectFolder.full)}">${esc(projectFolder.full)}</div>
|
|
1550
|
+
${node.entrypoint ? `<div style="font-size:10px;color:var(--muted);margin-top:4px">⎋ ${esc(node.entrypoint)}${node.version ? ` <span style="color:var(--dim)">v${esc(node.version)}</span>` : ''}</div>` : ''}
|
|
1551
|
+
</div>
|
|
1552
|
+
</div>
|
|
1553
|
+
</div>` : ''}
|
|
1554
|
+
|
|
1555
|
+
<div class="section">
|
|
1556
|
+
<div class="section-label">Tokens</div>
|
|
1557
|
+
<div class="token-bar">
|
|
1558
|
+
<div class="token-item">
|
|
1559
|
+
<span class="token-item-icon">↑</span>
|
|
1560
|
+
<span class="token-item-label">In</span>
|
|
1561
|
+
<span class="token-item-val">${fmt(node.tokens.input)}</span>
|
|
1562
|
+
</div>
|
|
1563
|
+
<div class="token-item">
|
|
1564
|
+
<span class="token-item-icon">↓</span>
|
|
1565
|
+
<span class="token-item-label">Out</span>
|
|
1566
|
+
<span class="token-item-val">${fmt(node.tokens.output)}</span>
|
|
1567
|
+
</div>
|
|
1568
|
+
<div class="token-item">
|
|
1569
|
+
<span class="token-item-icon">◎</span>
|
|
1570
|
+
<span class="token-item-label">Cache</span>
|
|
1571
|
+
<span class="token-item-val">${fmt(node.tokens.cacheRead)}</span>
|
|
1572
|
+
</div>
|
|
1573
|
+
</div>
|
|
1574
|
+
</div>
|
|
1575
|
+
|
|
1576
|
+
${(node.compactions || []).length ? (() => {
|
|
1577
|
+
const sessionStart = node.startedAt || 0;
|
|
1578
|
+
const preCompacts = (node.compactions || []).filter(c => c.timestamp < sessionStart);
|
|
1579
|
+
const inSession = (node.compactions || []).filter(c => c.timestamp >= sessionStart);
|
|
1580
|
+
const rows = (node.compactions || []).map(c => {
|
|
1581
|
+
const isPreSession = c.timestamp < sessionStart;
|
|
1582
|
+
const savings = (c.tokensBefore && c.tokensAfter)
|
|
1583
|
+
? `${fmt(c.tokensBefore)}→${fmt(c.tokensAfter)} (−${fmt(c.tokensBefore - c.tokensAfter)})`
|
|
1584
|
+
: c.tokensBefore ? `${fmt(c.tokensBefore)} tok` : '—';
|
|
1585
|
+
const ts = c.timestamp ? new Date(c.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
1586
|
+
const cleanSummary = (c.summary || '').replace(/^<analysis>\s*/i, '').replace(/\s*<\/analysis>\s*$/i, '').trim();
|
|
1587
|
+
const isContinuation = /^This session is being continued/i.test(cleanSummary);
|
|
1588
|
+
const typeLabel = isPreSession ? 'Continuation' : isContinuation ? 'Continuation' : 'Compact';
|
|
1589
|
+
const typeColor = isPreSession || isContinuation ? 'var(--teal)' : 'var(--purple)';
|
|
1590
|
+
return `<div style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;border-bottom:1px solid var(--border-dim);cursor:pointer"
|
|
1591
|
+
onclick="selectCompact('${esc(node.sessionId)}','${esc(c.id)}')">
|
|
1592
|
+
<span style="font-size:9px;font-weight:700;color:${typeColor};flex-shrink:0;width:72px;margin-top:1px">${typeLabel}</span>
|
|
1593
|
+
<span style="font-size:10px;color:var(--muted);flex:1">${savings}</span>
|
|
1594
|
+
${ts ? `<span style="font-size:9px;color:var(--dim);flex-shrink:0;font-variant-numeric:tabular-nums">${ts}</span>` : ''}
|
|
1595
|
+
</div>`;
|
|
1596
|
+
}).join('');
|
|
1597
|
+
return `<div class="section">
|
|
1598
|
+
<div class="section-label">Context Events <span style="font-size:10px;color:var(--dim);font-weight:400;text-transform:none;letter-spacing:0">(${(node.compactions || []).length})</span></div>
|
|
1599
|
+
<div style="border:1px solid var(--border-dim);border-radius:6px;padding:4px 10px;background:var(--surface2)">${rows}</div>
|
|
1600
|
+
</div>`;
|
|
1601
|
+
})() : ''}
|
|
1602
|
+
|
|
1603
|
+
${node.toolCalls.length ? `
|
|
1604
|
+
<div class="section">
|
|
1605
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
|
1606
|
+
<span class="section-label" style="margin-bottom:0">Logs <span style="color:var(--dim);font-weight:400">(${node.toolCalls.length})</span></span>
|
|
1607
|
+
<span id="${logFilterId}-clear" style="font-size:10px;color:var(--dim);cursor:pointer;display:none" onclick="clearLogFilter('${esc(logFilterId)}','${esc(logListId)}')">✕ clear filter</span>
|
|
1608
|
+
</div>
|
|
1609
|
+
${sortedTypes.length > 1 ? `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px" id="${logFilterId}">${typeFilterChips}</div>` : ''}
|
|
1610
|
+
<div id="${logListId}" style="max-height:320px;overflow-y:auto;border:1px solid var(--border-dim);border-radius:6px;background:var(--surface2)">
|
|
1611
|
+
${logRows}
|
|
1612
|
+
</div>
|
|
1613
|
+
</div>` : ''}
|
|
1614
|
+
|
|
1615
|
+
${node.lastText ? `
|
|
1616
|
+
<div class="section">
|
|
1617
|
+
<div class="section-label">Last output</div>
|
|
1618
|
+
<div class="code-block">${esc(node.lastText)}</div>
|
|
1619
|
+
</div>` : ''}
|
|
1620
|
+
|
|
1621
|
+
<div class="section">
|
|
1622
|
+
<div class="section-label" style="display:flex;align-items:center;justify-content:space-between">
|
|
1623
|
+
Conversation thread
|
|
1624
|
+
<button data-reload-thread="${esc(node.sessionId)}" data-thread-container="${esc(threadTabId)}"
|
|
1625
|
+
style="background:var(--surface2);border:1px solid var(--border-dim);color:var(--muted);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px">
|
|
1626
|
+
↺ Reload
|
|
1627
|
+
</button>
|
|
1628
|
+
</div>
|
|
1629
|
+
<div id="${threadTabId}" style="color:var(--dim);font-size:12px;margin-top:10px;max-height:400px;overflow-y:auto;border:1px solid var(--border-dim);border-radius:6px;padding:12px">
|
|
1630
|
+
Loading…
|
|
1631
|
+
</div>
|
|
1632
|
+
</div>`;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function toolDetailHTML(node, tc) {
|
|
1636
|
+
const bc = toolBadgeClass(tc.name);
|
|
1637
|
+
const bl = toolBadgeLabel(tc.name, tc.summary);
|
|
1638
|
+
const status = tc.done ? 'done' : 'running';
|
|
1639
|
+
|
|
1640
|
+
const hasInput = tc.input && Object.keys(tc.input).length > 0;
|
|
1641
|
+
const hasOutput = tc.output !== undefined && tc.output !== null;
|
|
1642
|
+
|
|
1643
|
+
const inputHtml = hasInput ? renderInputPayload(tc.name, tc.input) : '';
|
|
1644
|
+
const outputHtml = hasOutput ? renderOutputPayload(tc.name, tc.output) : '';
|
|
1645
|
+
|
|
1646
|
+
const tabsHtml = (hasInput || hasOutput) ? `
|
|
1647
|
+
<div class="tabs">
|
|
1648
|
+
${hasInput ? `<div class="tab active" onclick="switchTab(this,'io-input')">Input</div>` : ''}
|
|
1649
|
+
${hasOutput ? `<div class="tab ${hasInput?'':'active'}" onclick="switchTab(this,'io-output')">Output</div>` : ''}
|
|
1650
|
+
</div>
|
|
1651
|
+
${hasInput ? `<div class="tab-body active" id="io-input">${inputHtml}</div>` : ''}
|
|
1652
|
+
${hasOutput ? `<div class="tab-body ${hasInput?'':'active'}" id="io-output">${outputHtml}</div>` : ''}
|
|
1653
|
+
` : '';
|
|
1654
|
+
|
|
1655
|
+
return `
|
|
1656
|
+
<div class="detail-title">
|
|
1657
|
+
<span class="type-badge ${bc}">${bl}</span>
|
|
1658
|
+
${esc(tc.name)}
|
|
1659
|
+
</div>
|
|
1660
|
+
|
|
1661
|
+
<div class="section">
|
|
1662
|
+
<div class="section-label">Details</div>
|
|
1663
|
+
<div class="prop-grid">
|
|
1664
|
+
<span class="prop-key">Status</span>
|
|
1665
|
+
<span class="prop-val"><span class="status-chip ${status}">${status}</span></span>
|
|
1666
|
+
<span class="prop-key">Duration</span>
|
|
1667
|
+
<span class="prop-val">${tc.durationMs ? dur(tc.durationMs) : '—'}</span>
|
|
1668
|
+
<span class="prop-key">Agent</span>
|
|
1669
|
+
<span class="prop-val blue">${esc(node.label)}</span>
|
|
1670
|
+
<span class="prop-key">Summary</span>
|
|
1671
|
+
<span class="prop-val" style="color:var(--muted)">${esc(tc.summary)}</span>
|
|
1672
|
+
</div>
|
|
1673
|
+
</div>
|
|
1674
|
+
|
|
1675
|
+
${(hasInput || hasOutput) ? `
|
|
1676
|
+
<div class="section">
|
|
1677
|
+
<div class="section-label">Payload</div>
|
|
1678
|
+
${tabsHtml}
|
|
1679
|
+
</div>` : ''}`;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function compactDetailHTML(node, c) {
|
|
1683
|
+
const savings = (c.tokensBefore && c.tokensAfter)
|
|
1684
|
+
? `${fmt(c.tokensBefore)} → ${fmt(c.tokensAfter)} (−${fmt(c.tokensBefore - c.tokensAfter)})`
|
|
1685
|
+
: '—';
|
|
1686
|
+
const pct = (c.tokensBefore && c.tokensAfter)
|
|
1687
|
+
? Math.round((1 - c.tokensAfter / c.tokensBefore) * 100) + '% reduction'
|
|
1688
|
+
: '';
|
|
1689
|
+
|
|
1690
|
+
return `
|
|
1691
|
+
<div class="detail-title">
|
|
1692
|
+
<span class="type-badge tb-compact">COMPACT</span>
|
|
1693
|
+
Context Compaction
|
|
1694
|
+
</div>
|
|
1695
|
+
|
|
1696
|
+
<div class="section">
|
|
1697
|
+
<div class="section-label">Details</div>
|
|
1698
|
+
<div class="prop-grid">
|
|
1699
|
+
<span class="prop-key">Agent</span>
|
|
1700
|
+
<span class="prop-val blue">${esc(node.label)}</span>
|
|
1701
|
+
<span class="prop-key">Tokens</span>
|
|
1702
|
+
<span class="prop-val" style="color:var(--teal)">${savings}</span>
|
|
1703
|
+
${pct ? `<span class="prop-key">Savings</span><span class="prop-val green">${pct}</span>` : ''}
|
|
1704
|
+
<span class="prop-key">Time</span>
|
|
1705
|
+
<span class="prop-val">${new Date(c.timestamp).toLocaleTimeString()}</span>
|
|
1706
|
+
</div>
|
|
1707
|
+
</div>
|
|
1708
|
+
|
|
1709
|
+
${c.summary && c.summary.length > 20 ? (() => {
|
|
1710
|
+
// Strip XML analysis wrapper tags if present (Claude wraps compaction summaries in <analysis>...</analysis>)
|
|
1711
|
+
const cleanSummary = c.summary.replace(/^<analysis>\s*/i, '').replace(/\s*<\/analysis>\s*$/i, '').trim();
|
|
1712
|
+
// Detect if this is a continuation prompt (from previous session's compaction injected at session start)
|
|
1713
|
+
const isContinuation = /^This session is being continued from a previous conversation/i.test(cleanSummary);
|
|
1714
|
+
const label = isContinuation ? 'Continuation Context' : 'Compact Summary';
|
|
1715
|
+
const note = isContinuation
|
|
1716
|
+
? '<span style="font-size:10px;color:var(--dim);font-weight:400;text-transform:none;letter-spacing:0">· summary injected at session start from previous compaction</span>'
|
|
1717
|
+
: '<span style="font-size:10px;color:var(--dim);font-weight:400;text-transform:none;letter-spacing:0">· what Claude preserved across context window</span>';
|
|
1718
|
+
return `
|
|
1719
|
+
<div class="section">
|
|
1720
|
+
<div class="section-label">${label} ${note}</div>
|
|
1721
|
+
<div class="code-block" style="white-space:pre-wrap;word-break:break-word;max-height:480px">${esc(cleanSummary)}</div>
|
|
1722
|
+
</div>`;
|
|
1723
|
+
})() : `
|
|
1724
|
+
<div class="section">
|
|
1725
|
+
<p style="color:var(--muted);font-size:12px">Claude Code compacted the conversation context to free up tokens.</p>
|
|
1726
|
+
<p style="color:var(--dim);font-size:11px;margin-top:8px">Summary not available — may be read from transcript after the session ends.</p>
|
|
1727
|
+
</div>`}`;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ── Summary popover ───────────────────────────────────────────────────────
|
|
1731
|
+
function openSummary(type) {
|
|
1732
|
+
const panel = document.getElementById('summary-panel-content');
|
|
1733
|
+
panel.innerHTML = buildSummary(type);
|
|
1734
|
+
document.getElementById('summary-popover').classList.add('open');
|
|
1735
|
+
}
|
|
1736
|
+
function closeSummary() {
|
|
1737
|
+
document.getElementById('summary-popover').classList.remove('open');
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function buildSummary(type) {
|
|
1741
|
+
const closeBtn = `<span class="summary-close" onclick="closeSummary()">✕</span>`;
|
|
1742
|
+
|
|
1743
|
+
if (!lastRoot) {
|
|
1744
|
+
return `<div class="summary-title">No active session ${closeBtn}</div>
|
|
1745
|
+
<p style="color:var(--muted);font-size:12px">Run Claude Code to see data here.</p>`;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// Flatten all nodes
|
|
1749
|
+
function flatten(node, depth = 0) {
|
|
1750
|
+
if (!node) return [];
|
|
1751
|
+
return [{ ...node, depth }, ...(node.children || []).flatMap(c => flatten(c, depth + 1))];
|
|
1752
|
+
}
|
|
1753
|
+
const allNodes = flatten(lastRoot);
|
|
1754
|
+
|
|
1755
|
+
if (type === 'agents') {
|
|
1756
|
+
const rows = allNodes.map(n => {
|
|
1757
|
+
const indent = ' '.repeat(n.depth);
|
|
1758
|
+
const elapsed = n.endedAt ? dur(n.endedAt - n.startedAt) : dur(Date.now() - n.startedAt);
|
|
1759
|
+
const dot = n.status === 'running' ? '⟳' : n.status === 'done' ? '✓' : '✗';
|
|
1760
|
+
const dotColor = n.status === 'running' ? 'var(--yellow)' : n.status === 'done' ? 'var(--green)' : 'var(--red)';
|
|
1761
|
+
return `<div class="summary-row" style="cursor:pointer" data-goto-sid="${esc(n.sessionId)}">
|
|
1762
|
+
<span class="summary-row-label" style="display:flex;align-items:center;gap:6px">
|
|
1763
|
+
<span style="color:${dotColor}">${dot}</span>
|
|
1764
|
+
<span style="font-size:11px">${indent}${esc(n.label)}</span>
|
|
1765
|
+
</span>
|
|
1766
|
+
<span class="summary-row-val">${elapsed}</span>
|
|
1767
|
+
</div>`;
|
|
1768
|
+
}).join('');
|
|
1769
|
+
return `<div class="summary-title">Agents (${allNodes.length}) ${closeBtn}</div>${rows}`;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
if (type === 'tools') {
|
|
1773
|
+
// Aggregate tool call counts across all nodes
|
|
1774
|
+
const toolCounts = new Map();
|
|
1775
|
+
for (const n of allNodes) {
|
|
1776
|
+
for (const tc of n.toolCalls) {
|
|
1777
|
+
const e = toolCounts.get(tc.name) || { calls: 0, done: 0, totalMs: 0 };
|
|
1778
|
+
e.calls++;
|
|
1779
|
+
if (tc.done) e.done++;
|
|
1780
|
+
if (tc.durationMs) e.totalMs += tc.durationMs;
|
|
1781
|
+
toolCounts.set(tc.name, e);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const sorted = [...toolCounts.entries()].sort((a,b) => b[1].calls - a[1].calls);
|
|
1785
|
+
const rows = sorted.map(([name, e]) => {
|
|
1786
|
+
const bc = toolBadgeClass(name);
|
|
1787
|
+
const bl = toolBadgeLabel(name);
|
|
1788
|
+
const avg = e.totalMs && e.done ? ' · avg ' + dur(Math.round(e.totalMs / e.done)) : '';
|
|
1789
|
+
return `<div class="summary-row">
|
|
1790
|
+
<span style="display:flex;align-items:center;gap:8px">
|
|
1791
|
+
<span class="type-badge ${bc}">${bl}</span>
|
|
1792
|
+
<span class="summary-row-label">${esc(name)}</span>
|
|
1793
|
+
</span>
|
|
1794
|
+
<span class="summary-row-val blue">${e.calls} calls${avg}</span>
|
|
1795
|
+
</div>`;
|
|
1796
|
+
}).join('');
|
|
1797
|
+
const total = [...toolCounts.values()].reduce((s,e) => s + e.calls, 0);
|
|
1798
|
+
return `<div class="summary-title">Tool calls (${total}) ${closeBtn}</div>${rows || '<p style="color:var(--muted);font-size:12px">No tool calls yet.</p>'}`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (type === 'tokens') {
|
|
1802
|
+
const rows = allNodes.map(n => {
|
|
1803
|
+
const total = n.tokens.input + n.tokens.output;
|
|
1804
|
+
if (!total && !n.tokens.cacheRead) return '';
|
|
1805
|
+
return `<div class="summary-row" style="cursor:pointer" data-goto-sid="${esc(n.sessionId)}">
|
|
1806
|
+
<span class="summary-row-label" style="font-size:11px">${esc(n.label)}</span>
|
|
1807
|
+
<span style="display:flex;gap:10px;font-size:11px;font-variant-numeric:tabular-nums">
|
|
1808
|
+
<span style="color:var(--dim)">↑${fmt(n.tokens.input)}</span>
|
|
1809
|
+
<span style="color:var(--dim)">↓${fmt(n.tokens.output)}</span>
|
|
1810
|
+
<span style="color:var(--blue-soft)">◎${fmt(n.tokens.cacheRead)}</span>
|
|
1811
|
+
</span>
|
|
1812
|
+
</div>`;
|
|
1813
|
+
}).filter(Boolean).join('');
|
|
1814
|
+
// Root nodes already include child tokens recursively — sum roots only to avoid double-counting
|
|
1815
|
+
const rootNodes = allNodes.filter(n => n.depth === 0);
|
|
1816
|
+
const totalIn = rootNodes.reduce((s,n) => s + n.tokens.input, 0);
|
|
1817
|
+
const totalOut = rootNodes.reduce((s,n) => s + n.tokens.output, 0);
|
|
1818
|
+
const totalCache = rootNodes.reduce((s,n) => s + n.tokens.cacheRead, 0);
|
|
1819
|
+
return `<div class="summary-title">Token breakdown ${closeBtn}</div>
|
|
1820
|
+
<div class="summary-row"><span class="summary-row-label">Total input</span><span class="summary-row-val">${fmt(totalIn)}</span></div>
|
|
1821
|
+
<div class="summary-row"><span class="summary-row-label">Total output</span><span class="summary-row-val">${fmt(totalOut)}</span></div>
|
|
1822
|
+
<div class="summary-row"><span class="summary-row-label">Cache read</span><span class="summary-row-val blue">${fmt(totalCache)}</span></div>
|
|
1823
|
+
<div style="height:12px"></div>
|
|
1824
|
+
${rows || '<p style="color:var(--muted);font-size:12px">No token data yet.</p>'}`;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
if (type === 'cost') {
|
|
1828
|
+
const rows = allNodes.map(n => {
|
|
1829
|
+
if (!n.costUsd) return '';
|
|
1830
|
+
return `<div class="summary-row" style="cursor:pointer" data-goto-sid="${esc(n.sessionId)}">
|
|
1831
|
+
<span class="summary-row-label" style="font-size:11px">${esc(n.label)}</span>
|
|
1832
|
+
<span class="summary-row-val green">$${n.costUsd.toFixed(4)}</span>
|
|
1833
|
+
</div>`;
|
|
1834
|
+
}).filter(Boolean).join('');
|
|
1835
|
+
// Root nodes include child costs recursively — sum roots only to avoid double-counting
|
|
1836
|
+
const rootNodes = allNodes.filter(n => n.depth === 0);
|
|
1837
|
+
const total = rootNodes.reduce((s,n) => s + (n.costUsd || 0), 0);
|
|
1838
|
+
return `<div class="summary-title">Cost breakdown ${closeBtn}</div>
|
|
1839
|
+
<div class="summary-row"><span class="summary-row-label">Total this session</span><span class="summary-row-val green">$${total.toFixed(4)}</span></div>
|
|
1840
|
+
<div style="height:12px"></div>
|
|
1841
|
+
${rows || '<p style="color:var(--muted);font-size:12px">Cost data available after session ends.</p>'}
|
|
1842
|
+
<div style="margin-top:16px;padding-top:12px;border-top:1px solid var(--border-dim)">
|
|
1843
|
+
<div style="font-size:11px;color:var(--dim);margin-bottom:8px">Past sessions →</div>
|
|
1844
|
+
<div style="cursor:pointer;font-size:12px;color:var(--blue)" onclick="closeSummary();showView('history')">View history</div>
|
|
1845
|
+
</div>`;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return `<div class="summary-title">Summary ${closeBtn}</div>`;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// ── View switching ─────────────────────────────────────────────────────────
|
|
1852
|
+
let currentView = 'trace';
|
|
1853
|
+
function showView(view) {
|
|
1854
|
+
currentView = view;
|
|
1855
|
+
document.querySelectorAll('.nav-tab').forEach((t, i) => {
|
|
1856
|
+
const views = ['trace', 'history', 'permissions'];
|
|
1857
|
+
t.classList.toggle('active', views[i] === view);
|
|
1858
|
+
});
|
|
1859
|
+
document.getElementById('trace-view').style.display = view === 'trace' ? 'flex' : 'none';
|
|
1860
|
+
document.getElementById('history-view').style.display = view === 'history' ? 'flex' : 'none';
|
|
1861
|
+
document.getElementById('permissions-view').style.display = view === 'permissions' ? 'flex' : 'none';
|
|
1862
|
+
|
|
1863
|
+
// Graph only makes sense on the Trace view — close it on other views and hide button
|
|
1864
|
+
if (view !== 'trace' && graphOpen) closeGraph();
|
|
1865
|
+
const graphBtn = document.getElementById('graph-btn');
|
|
1866
|
+
if (graphBtn) graphBtn.style.display = view === 'trace' ? '' : 'none';
|
|
1867
|
+
|
|
1868
|
+
if (view === 'history') loadHistoryList();
|
|
1869
|
+
if (view === 'permissions') loadPermissions();
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// ── History ────────────────────────────────────────────────────────────────
|
|
1873
|
+
let selectedHistorySid = null;
|
|
1874
|
+
|
|
1875
|
+
// ── Projects panel ────────────────────────────────────────────────────────
|
|
1876
|
+
let projectsOpen = true;
|
|
1877
|
+
let projectsData = null;
|
|
1878
|
+
|
|
1879
|
+
function toggleProjects() {
|
|
1880
|
+
projectsOpen = !projectsOpen;
|
|
1881
|
+
const list = document.getElementById('projects-list');
|
|
1882
|
+
const header = document.getElementById('proj-header');
|
|
1883
|
+
const chev = document.getElementById('proj-chevron');
|
|
1884
|
+
if (list) list.style.display = projectsOpen ? '' : 'none';
|
|
1885
|
+
if (header) header.classList.toggle('collapsed', !projectsOpen);
|
|
1886
|
+
if (chev) chev.style.transform = projectsOpen ? '' : 'rotate(-90deg)';
|
|
1887
|
+
if (projectsOpen && !projectsData) loadProjects();
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function loadProjects() {
|
|
1891
|
+
try {
|
|
1892
|
+
const res = await fetch('/api/projects');
|
|
1893
|
+
projectsData = await res.json();
|
|
1894
|
+
renderProjects();
|
|
1895
|
+
} catch {}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function relativeTime(ms) {
|
|
1899
|
+
const s = Math.floor((Date.now() - ms) / 1000);
|
|
1900
|
+
if (s < 60) return `${s}s ago`;
|
|
1901
|
+
if (s < 3600) return `${Math.floor(s/60)}m ago`;
|
|
1902
|
+
if (s < 86400) return `${Math.floor(s/3600)}h ago`;
|
|
1903
|
+
return `${Math.floor(s/86400)}d ago`;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function renderProjects() {
|
|
1907
|
+
const list = document.getElementById('projects-list');
|
|
1908
|
+
const countEl = document.getElementById('proj-count');
|
|
1909
|
+
if (!list || !projectsData) return;
|
|
1910
|
+
|
|
1911
|
+
const projects = projectsData.filter(p => p.cwd); // skip unknown
|
|
1912
|
+
if (countEl) countEl.textContent = projects.length ? `(${projects.length})` : '';
|
|
1913
|
+
|
|
1914
|
+
list.innerHTML = projects.map(proj => {
|
|
1915
|
+
const sessions = proj.sessions.slice(0, 6);
|
|
1916
|
+
const activeSid = selected?.sessionId;
|
|
1917
|
+
const sessionRows = sessions.map(s => {
|
|
1918
|
+
const dotColor = s.status === 'running' ? 'var(--yellow)' : s.status === 'done' ? 'var(--green)' : 'var(--red)';
|
|
1919
|
+
const cost = s.cost_usd > 0 ? `$${s.cost_usd.toFixed(2)}` : '';
|
|
1920
|
+
const age = s.started_at ? relativeTime(s.started_at) : '';
|
|
1921
|
+
const isActive = s.id === activeSid;
|
|
1922
|
+
return `<div class="proj-session-row${isActive?' active':''}" data-proj-sid="${esc(s.id)}">
|
|
1923
|
+
<span class="proj-status-dot" style="background:${dotColor}"></span>
|
|
1924
|
+
<span class="proj-session-label" title="${esc(s.label)}">${esc(s.label)}</span>
|
|
1925
|
+
${cost ? `<span class="proj-session-cost">${esc(cost)}</span>` : ''}
|
|
1926
|
+
<span class="proj-session-age">${esc(age)}</span>
|
|
1927
|
+
</div>`;
|
|
1928
|
+
}).join('');
|
|
1929
|
+
|
|
1930
|
+
const hasRunning = sessions.some(s => s.status === 'running');
|
|
1931
|
+
const folderColor = hasRunning ? 'var(--yellow)' : 'var(--dim)';
|
|
1932
|
+
return `<div class="proj-group">
|
|
1933
|
+
<div class="proj-folder-row" title="${esc(proj.cwd)}">
|
|
1934
|
+
<span style="font-size:14px">📁</span>
|
|
1935
|
+
<div style="flex:1;min-width:0">
|
|
1936
|
+
<div class="proj-folder-name">${esc(proj.name)}</div>
|
|
1937
|
+
<div style="font-size:9px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(proj.cwd)}</div>
|
|
1938
|
+
</div>
|
|
1939
|
+
<span class="proj-folder-count">${sessions.length}</span>
|
|
1940
|
+
</div>
|
|
1941
|
+
${sessionRows}
|
|
1942
|
+
</div>`;
|
|
1943
|
+
}).join('') || '<div style="padding:12px 14px;font-size:11px;color:var(--dim)">No sessions recorded yet</div>';
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Handle project session clicks
|
|
1947
|
+
document.addEventListener('click', e => {
|
|
1948
|
+
const projEl = e.target.closest('[data-proj-sid]');
|
|
1949
|
+
if (projEl) {
|
|
1950
|
+
const sid = projEl.dataset.projSid;
|
|
1951
|
+
// Try selecting live session first, else load from history
|
|
1952
|
+
if (sessions && sessions.has && sessions.has(sid)) {
|
|
1953
|
+
showView('trace');
|
|
1954
|
+
selectAgent(sid);
|
|
1955
|
+
} else {
|
|
1956
|
+
showView('history');
|
|
1957
|
+
loadHistorySession(sid);
|
|
1958
|
+
}
|
|
1959
|
+
// Refresh active state
|
|
1960
|
+
renderProjects();
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// Refresh project panel periodically and after session updates
|
|
1966
|
+
let projRefreshTimer = null;
|
|
1967
|
+
function scheduleProjectRefresh() {
|
|
1968
|
+
clearTimeout(projRefreshTimer);
|
|
1969
|
+
projRefreshTimer = setTimeout(() => { projectsData = null; if (projectsOpen) loadProjects(); }, 3000);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
async function loadHistoryList() {
|
|
1973
|
+
try {
|
|
1974
|
+
const res = await fetch('/api/sessions');
|
|
1975
|
+
const sessions = await res.json();
|
|
1976
|
+
const list = document.getElementById('history-list');
|
|
1977
|
+
if (!sessions.length) {
|
|
1978
|
+
list.innerHTML = '<div style="padding:20px 14px;color:var(--dim);font-size:12px">No sessions yet.</div>';
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
// Group by date
|
|
1982
|
+
const groups = new Map(); // 'Apr 6, 2026' → sessions[]
|
|
1983
|
+
for (const s of sessions) {
|
|
1984
|
+
const d = new Date(s.started_at);
|
|
1985
|
+
const key = d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
|
|
1986
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
1987
|
+
groups.get(key).push(s);
|
|
1988
|
+
}
|
|
1989
|
+
let html = '';
|
|
1990
|
+
for (const [dateKey, group] of groups) {
|
|
1991
|
+
html += `<div style="padding:6px 14px 2px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:var(--dim)">${esc(dateKey)}</div>`;
|
|
1992
|
+
for (const s of group) {
|
|
1993
|
+
const cost = s.cost_usd > 0 ? `$${s.cost_usd.toFixed(2)}` : '';
|
|
1994
|
+
const time = new Date(s.started_at).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
|
|
1995
|
+
const active = s.id === selectedHistorySid ? ' active' : '';
|
|
1996
|
+
html += `<div class="history-session${active}" data-history-sid="${esc(s.id)}">
|
|
1997
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
1998
|
+
<div class="history-status-dot ${esc(s.status)}"></div>
|
|
1999
|
+
<div class="history-session-label">${esc(s.label)}</div>
|
|
2000
|
+
</div>
|
|
2001
|
+
<div class="history-session-meta">
|
|
2002
|
+
<span class="history-session-date">${esc(time)}</span>
|
|
2003
|
+
${cost ? `<span class="history-session-cost">${esc(cost)}</span>` : ''}
|
|
2004
|
+
</div>
|
|
2005
|
+
</div>`;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
list.innerHTML = html;
|
|
2009
|
+
} catch(e) {
|
|
2010
|
+
document.getElementById('history-list').innerHTML =
|
|
2011
|
+
'<div style="padding:20px 14px;color:var(--red);font-size:12px">Failed to load sessions.</div>';
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
async function loadHistorySession(sid) {
|
|
2016
|
+
selectedHistorySid = sid;
|
|
2017
|
+
// Update active state without re-fetching
|
|
2018
|
+
document.querySelectorAll('.history-session').forEach(el => {
|
|
2019
|
+
el.classList.toggle('active', el.dataset.historySid === sid);
|
|
2020
|
+
});
|
|
2021
|
+
const detail = document.getElementById('history-detail');
|
|
2022
|
+
detail.innerHTML = '<div class="detail-empty"><div class="detail-empty-icon">◌</div><div>Loading…</div></div>';
|
|
2023
|
+
try {
|
|
2024
|
+
const res = await fetch(`/api/sessions/${encodeURIComponent(sid)}`);
|
|
2025
|
+
const node = await res.json();
|
|
2026
|
+
detail.innerHTML = '<div style="padding:24px 28px">' + agentDetailHTML(node) + '</div>';
|
|
2027
|
+
} catch {
|
|
2028
|
+
detail.innerHTML = '<div style="padding:24px;color:var(--red);font-size:12px">Failed to load session.</div>';
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function relDate(d) {
|
|
2033
|
+
const diff = Date.now() - d.getTime();
|
|
2034
|
+
if (diff < 60000) return 'just now';
|
|
2035
|
+
if (diff < 3600000) return Math.floor(diff/60000) + 'm ago';
|
|
2036
|
+
if (diff < 86400000) return Math.floor(diff/3600000) + 'h ago';
|
|
2037
|
+
return d.toLocaleDateString();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// ── Permissions ────────────────────────────────────────────────────────────
|
|
2041
|
+
async function loadPermissions() {
|
|
2042
|
+
const el = document.getElementById('permissions-content');
|
|
2043
|
+
try {
|
|
2044
|
+
const [permRes, auditRes] = await Promise.all([
|
|
2045
|
+
fetch('/api/permissions'),
|
|
2046
|
+
fetch('/api/security-audit'),
|
|
2047
|
+
]);
|
|
2048
|
+
const data = await permRes.json();
|
|
2049
|
+
const audit = auditRes.ok ? await auditRes.json() : {};
|
|
2050
|
+
el.innerHTML = renderPermissions(data, audit);
|
|
2051
|
+
} catch {
|
|
2052
|
+
el.innerHTML = '<div style="color:var(--dim);font-size:12px">Could not load permissions — daemon may not support this endpoint yet.</div>';
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function renderPermissions(data, audit = {}) {
|
|
2057
|
+
const { defaultMode, allow = [], deny = [], ask = [], toolStats = [], plugins = [], env = {}, mcpServers = [], fileAccess = {} } = data;
|
|
2058
|
+
const { hooks = [], bashCommands = [], sensitiveFiles = [], webRequests = [], bypassSessions = [], blockedActions = [] } = audit;
|
|
2059
|
+
|
|
2060
|
+
const chipList = (items, cls) => items.length
|
|
2061
|
+
? items.map(i => `<span class="perm-chip ${cls}">${esc(i)}</span>`).join('')
|
|
2062
|
+
: `<span class="perm-empty">None</span>`;
|
|
2063
|
+
|
|
2064
|
+
// MCP servers
|
|
2065
|
+
const mcpRows = mcpServers.length ? mcpServers.map(s => {
|
|
2066
|
+
const cmdStr = s.command ? esc(s.command + (s.args?.length ? ' ' + s.args.join(' ') : '')) : '—';
|
|
2067
|
+
const typeBadge = `<span class="perm-chip mode" style="font-size:9px;padding:1px 5px">${esc(s.type)}</span>`;
|
|
2068
|
+
return `<div class="perm-tool-row" style="flex-direction:column;align-items:flex-start;gap:3px">
|
|
2069
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
2070
|
+
${typeBadge}
|
|
2071
|
+
<span style="font-size:12px;font-weight:600;color:var(--text)">${esc(s.name)}</span>
|
|
2072
|
+
</div>
|
|
2073
|
+
<span class="mono" style="font-size:10px;color:var(--dim)">${cmdStr}</span>
|
|
2074
|
+
</div>`;
|
|
2075
|
+
}).join('') : `<span class="perm-empty">No MCP servers configured</span>`;
|
|
2076
|
+
|
|
2077
|
+
// File access
|
|
2078
|
+
const cwdRow = fileAccess.cwd
|
|
2079
|
+
? `<div class="perm-tool-row">
|
|
2080
|
+
<span class="perm-chip allow">✓ ${esc(fileAccess.cwd)}</span>
|
|
2081
|
+
<span style="font-size:10px;color:var(--dim);margin-left:4px">working directory</span>
|
|
2082
|
+
</div>`
|
|
2083
|
+
: `<div style="color:var(--dim);font-size:12px;font-style:italic">No active session — working directory unknown</div>`;
|
|
2084
|
+
|
|
2085
|
+
const extraDirs = (fileAccess.additionalDirs || []).map(d =>
|
|
2086
|
+
`<div class="perm-tool-row"><span class="perm-chip allow">✓ ${esc(d)}</span></div>`
|
|
2087
|
+
).join('');
|
|
2088
|
+
|
|
2089
|
+
const deniedPaths = (fileAccess.denied || []).map(d =>
|
|
2090
|
+
`<span class="perm-chip deny">${esc(d)}</span>`
|
|
2091
|
+
).join('');
|
|
2092
|
+
|
|
2093
|
+
// Tool stats
|
|
2094
|
+
const toolRows = toolStats.map(t =>
|
|
2095
|
+
`<div class="perm-tool-row">
|
|
2096
|
+
<span class="perm-tool-name">${esc(t.name)}</span>
|
|
2097
|
+
<span class="perm-tool-count">${t.calls} call${t.calls !== 1 ? 's' : ''}${t.totalMs ? ' · ' + (t.totalMs/1000).toFixed(1) + 's' : ''}</span>
|
|
2098
|
+
</div>`).join('');
|
|
2099
|
+
|
|
2100
|
+
// Plugins
|
|
2101
|
+
const pluginRows = plugins.map(p => `<span class="perm-chip mode">${esc(p)}</span>`).join('');
|
|
2102
|
+
|
|
2103
|
+
// Env
|
|
2104
|
+
const envRows = Object.entries(env).map(([k, v]) =>
|
|
2105
|
+
`<div class="perm-tool-row">
|
|
2106
|
+
<span class="perm-tool-name mono" style="font-size:11px">${esc(k)}</span>
|
|
2107
|
+
<span class="perm-tool-count mono" style="font-size:11px">${esc(v)}</span>
|
|
2108
|
+
</div>`).join('');
|
|
2109
|
+
|
|
2110
|
+
// ── Audit: hooks ──
|
|
2111
|
+
const DESTRUCTIVE_BASH_RE = /\brm\s+-[a-zA-Z]*[rf]|\bDROP\s+(?:TABLE|DATABASE)\b|\btruncate\b|\bchmod\s+777\b|\bgit\s+push\b[^|&;]*-(?:-force\b|f\b)|\b(?:pkill|wipefs|mkfs|shred)\b|>\s*\/dev\/sda|\bdd\b.*of=/i;
|
|
2112
|
+
// Strip quoted strings before testing so `echo "rm -rf"` doesn't false-positive
|
|
2113
|
+
function isDestructiveBash(cmd) {
|
|
2114
|
+
const stripped = cmd.replace(/'(?:[^'\\]|\\.)*'/g, "''").replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
2115
|
+
return DESTRUCTIVE_BASH_RE.test(stripped);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const hookRows = hooks.length ? hooks.map(h => {
|
|
2119
|
+
const eventColor = { PreToolUse:'var(--yellow)', PostToolUse:'var(--teal)', Stop:'var(--purple)', PreCompact:'var(--muted)', PostCompact:'var(--muted)', SessionStart:'var(--blue)' }[h.event] || 'var(--muted)';
|
|
2120
|
+
const sourceBadge = h.source === 'global' ? '' : `<span style="font-size:9px;color:var(--muted);border:1px solid var(--border-dim);padding:1px 4px;border-radius:3px">${esc(h.source)}</span>`;
|
|
2121
|
+
return `<div class="perm-tool-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px 10px;background:var(--surface2);border-radius:6px;margin-bottom:6px">
|
|
2122
|
+
<div style="display:flex;align-items:center;gap:8px;width:100%">
|
|
2123
|
+
<span style="font-size:10px;font-weight:700;color:${eventColor};letter-spacing:0.05em">${esc(h.event)}</span>
|
|
2124
|
+
${h.matcher ? `<span style="font-size:10px;color:var(--dim)">on <span style="color:var(--text)">${esc(h.matcher)}</span></span>` : ''}
|
|
2125
|
+
${sourceBadge}
|
|
2126
|
+
</div>
|
|
2127
|
+
<div class="mono" style="font-size:10px;color:var(--muted);word-break:break-all;line-height:1.6">${esc(h.command)}</div>
|
|
2128
|
+
</div>`;
|
|
2129
|
+
}).join('') : `<span class="perm-empty">No hooks configured</span>`;
|
|
2130
|
+
|
|
2131
|
+
// ── Audit: bash commands ──
|
|
2132
|
+
const bashRows2 = bashCommands.length ? bashCommands.slice(0, 100).map(b => {
|
|
2133
|
+
const isDanger = isDestructiveBash(b.command);
|
|
2134
|
+
const ts = b.startedAt ? new Date(b.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
2135
|
+
return `<div class="perm-tool-row" style="gap:8px;align-items:flex-start;padding:5px 8px;border-radius:4px;${isDanger?'background:#1a0808;':''}">
|
|
2136
|
+
<span class="mono" style="font-size:10px;color:${isDanger?'var(--red)':'var(--muted)'};flex:1;word-break:break-all">${esc(b.command)}</span>
|
|
2137
|
+
<span style="font-size:9px;color:var(--dim);white-space:nowrap;flex-shrink:0">${esc(ts)}</span>
|
|
2138
|
+
</div>`;
|
|
2139
|
+
}).join('') : `<span class="perm-empty">No bash commands recorded</span>`;
|
|
2140
|
+
|
|
2141
|
+
// ── Audit: sensitive files ──
|
|
2142
|
+
const sensitiveRows = sensitiveFiles.length ? sensitiveFiles.map(f => {
|
|
2143
|
+
const toolColor = { Read:'var(--blue)', Write:'var(--red)', Edit:'var(--yellow)' }[f.tool] || 'var(--muted)';
|
|
2144
|
+
return `<div class="perm-tool-row" style="gap:8px;padding:5px 8px;border-radius:4px;background:var(--surface2)">
|
|
2145
|
+
<span style="font-size:9px;font-weight:700;color:${toolColor};width:34px;flex-shrink:0">${esc(f.tool)}</span>
|
|
2146
|
+
<span class="mono" style="font-size:10px;color:var(--red);flex:1;word-break:break-all">${esc(f.filePath)}</span>
|
|
2147
|
+
</div>`;
|
|
2148
|
+
}).join('') : `<span class="perm-empty">No sensitive file access detected</span>`;
|
|
2149
|
+
|
|
2150
|
+
// ── Audit: network requests ──
|
|
2151
|
+
const webRows2 = webRequests.length ? webRequests.map(w => {
|
|
2152
|
+
return `<div class="perm-tool-row" style="gap:8px;padding:5px 8px;border-radius:4px;background:var(--surface2)">
|
|
2153
|
+
<span style="font-size:9px;font-weight:700;color:var(--blue);width:60px;flex-shrink:0">${esc(w.tool)}</span>
|
|
2154
|
+
<span class="mono" style="font-size:10px;color:var(--blue-soft);flex:1;word-break:break-all">${esc(w.url)}</span>
|
|
2155
|
+
</div>`;
|
|
2156
|
+
}).join('') : `<span class="perm-empty">No network requests recorded</span>`;
|
|
2157
|
+
|
|
2158
|
+
// ── Audit: bypass sessions ──
|
|
2159
|
+
const bypassRows2 = bypassSessions.length ? bypassSessions.map(s => {
|
|
2160
|
+
const ts = new Date(s.started_at).toLocaleDateString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
|
|
2161
|
+
return `<div class="perm-tool-row" style="gap:8px;padding:6px 10px;border-radius:4px;background:#2d0f0f;border:1px solid #5a1a1a">
|
|
2162
|
+
<span style="font-size:9px;font-weight:700;color:var(--red)">BYPASS</span>
|
|
2163
|
+
<span style="font-size:11px;flex:1;color:var(--text)">${esc(s.label)}</span>
|
|
2164
|
+
<span style="font-size:10px;color:var(--dim);white-space:nowrap">${esc(ts)}</span>
|
|
2165
|
+
</div>`;
|
|
2166
|
+
}).join('') : `<span class="perm-empty" style="color:var(--green)">No bypass-mode sessions — good</span>`;
|
|
2167
|
+
|
|
2168
|
+
// ── Risk summary banner ──
|
|
2169
|
+
const dangerCount = bashCommands.filter(b => isDestructiveBash(b.command)).length;
|
|
2170
|
+
const riskItems = [
|
|
2171
|
+
bypassSessions.length && `<span style="color:var(--red);font-weight:600">${bypassSessions.length} bypass-mode session${bypassSessions.length>1?'s':''}</span>`,
|
|
2172
|
+
dangerCount && `<span style="color:var(--red)">${dangerCount} destructive command${dangerCount>1?'s':''}</span>`,
|
|
2173
|
+
sensitiveFiles.length && `<span style="color:var(--yellow)">${sensitiveFiles.length} sensitive file access${sensitiveFiles.length>1?'es':''}</span>`,
|
|
2174
|
+
hooks.length && `<span style="color:var(--muted)">${hooks.length} active hook${hooks.length>1?'s':''}</span>`,
|
|
2175
|
+
].filter(Boolean);
|
|
2176
|
+
|
|
2177
|
+
const riskBanner = riskItems.length
|
|
2178
|
+
? `<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;padding:10px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;margin-bottom:24px;font-size:11px">
|
|
2179
|
+
<span style="color:var(--dim);font-weight:600;letter-spacing:0.05em;text-transform:uppercase;font-size:10px">Risk Signals</span>
|
|
2180
|
+
${riskItems.join('<span style="color:var(--border)">·</span>')}
|
|
2181
|
+
</div>`
|
|
2182
|
+
: `<div style="padding:10px 14px;background:#0d1f12;border:1px solid #1a4a2e;border-radius:8px;margin-bottom:24px;font-size:11px;color:var(--green)">No risk signals detected</div>`;
|
|
2183
|
+
|
|
2184
|
+
const result = `
|
|
2185
|
+
${riskBanner}
|
|
2186
|
+
|
|
2187
|
+
<div class="perm-section">
|
|
2188
|
+
<div class="section-label">Permission Mode</div>
|
|
2189
|
+
<div class="perm-group"><span class="perm-chip mode">${esc(defaultMode || 'default')}</span></div>
|
|
2190
|
+
</div>
|
|
2191
|
+
|
|
2192
|
+
<div class="perm-section">
|
|
2193
|
+
<div class="section-label">File Access</div>
|
|
2194
|
+
${cwdRow}
|
|
2195
|
+
${extraDirs}
|
|
2196
|
+
${deniedPaths ? `<div style="margin-top:8px"><div style="font-size:10px;color:var(--dim);margin-bottom:6px">Denied read patterns:</div><div class="perm-group">${deniedPaths}</div></div>` : ''}
|
|
2197
|
+
</div>
|
|
2198
|
+
|
|
2199
|
+
<div class="perm-section">
|
|
2200
|
+
<div class="section-label">MCP Servers</div>
|
|
2201
|
+
${mcpRows}
|
|
2202
|
+
</div>
|
|
2203
|
+
|
|
2204
|
+
<div class="perm-section">
|
|
2205
|
+
<div class="section-label">Allowed Rules</div>
|
|
2206
|
+
<div class="perm-group">${chipList(allow, 'allow')}</div>
|
|
2207
|
+
</div>
|
|
2208
|
+
|
|
2209
|
+
<div class="perm-section">
|
|
2210
|
+
<div class="section-label">Denied Rules</div>
|
|
2211
|
+
<div class="perm-group">${chipList(deny, 'deny')}</div>
|
|
2212
|
+
</div>
|
|
2213
|
+
|
|
2214
|
+
${ask.length ? `<div class="perm-section"><div class="section-label">Always Ask</div><div class="perm-group">${chipList(ask, 'ask')}</div></div>` : ''}
|
|
2215
|
+
|
|
2216
|
+
${plugins.length ? `<div class="perm-section"><div class="section-label">Active Plugins</div><div class="perm-group">${pluginRows}</div></div>` : ''}
|
|
2217
|
+
|
|
2218
|
+
${envRows ? `<div class="perm-section"><div class="section-label">Environment</div>${envRows}</div>` : ''}
|
|
2219
|
+
|
|
2220
|
+
<div class="perm-section">
|
|
2221
|
+
<div class="section-label">Configured Hooks <span style="font-size:9px;color:var(--yellow);font-weight:400;margin-left:6px;text-transform:none;letter-spacing:0">runs arbitrary shell code</span></div>
|
|
2222
|
+
${hookRows}
|
|
2223
|
+
</div>
|
|
2224
|
+
|
|
2225
|
+
<div class="perm-section">
|
|
2226
|
+
<div class="section-label">Bypass-mode Sessions</div>
|
|
2227
|
+
${bypassRows2}
|
|
2228
|
+
</div>
|
|
2229
|
+
|
|
2230
|
+
<div class="perm-section">
|
|
2231
|
+
<div class="section-label">Sensitive File Access</div>
|
|
2232
|
+
<div style="max-height:240px;overflow-y:auto">${sensitiveRows}</div>
|
|
2233
|
+
</div>
|
|
2234
|
+
|
|
2235
|
+
<div class="perm-section">
|
|
2236
|
+
<div class="section-label">Bash Command Audit <span style="font-size:9px;color:var(--dim);font-weight:400;margin-left:6px;text-transform:none;letter-spacing:0">last 100 · destructive in red</span></div>
|
|
2237
|
+
<div style="max-height:300px;overflow-y:auto">${bashRows2}</div>
|
|
2238
|
+
</div>
|
|
2239
|
+
|
|
2240
|
+
<div class="perm-section">
|
|
2241
|
+
<div class="section-label">Network Requests</div>
|
|
2242
|
+
<div style="max-height:240px;overflow-y:auto">${webRows2}</div>
|
|
2243
|
+
</div>
|
|
2244
|
+
|
|
2245
|
+
${toolRows ? `<div class="perm-section"><div class="section-label">Tool Usage This Session</div>${toolRows}</div>` : ''}
|
|
2246
|
+
|
|
2247
|
+
<div class="perm-section" id="packages-section">
|
|
2248
|
+
<div class="section-label" style="display:flex;align-items:center;justify-content:space-between">
|
|
2249
|
+
<span>Packages</span>
|
|
2250
|
+
<span style="font-size:10px;color:var(--blue);cursor:pointer;font-weight:400;text-transform:none;letter-spacing:0" onclick="loadPackages()">↺ Refresh</span>
|
|
2251
|
+
</div>
|
|
2252
|
+
<div id="packages-content" style="color:var(--dim);font-size:12px">Loading…</div>
|
|
2253
|
+
</div>
|
|
2254
|
+
`;
|
|
2255
|
+
|
|
2256
|
+
setTimeout(loadPackages, 0);
|
|
2257
|
+
return result;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
async function loadPackages() {
|
|
2261
|
+
const el = document.getElementById('packages-content');
|
|
2262
|
+
if (!el) return;
|
|
2263
|
+
try {
|
|
2264
|
+
const res = await fetch('/api/packages');
|
|
2265
|
+
const data = await res.json();
|
|
2266
|
+
el.innerHTML = renderPackages(data);
|
|
2267
|
+
} catch {
|
|
2268
|
+
el.innerHTML = '<span style="color:var(--dim);font-size:12px">Could not load packages.</span>';
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
function renderPackages(data) {
|
|
2273
|
+
if (!data.cwd) return '<span class="perm-empty">No active session — run Claude Code to detect project packages</span>';
|
|
2274
|
+
let html = `<div style="font-size:11px;color:var(--dim);margin-bottom:10px">Project: <span style="color:var(--muted)">${esc(data.cwd)}</span></div>`;
|
|
2275
|
+
|
|
2276
|
+
if (data.npm) {
|
|
2277
|
+
const allPkgs = [...data.npm.deps, ...data.npm.devDeps];
|
|
2278
|
+
if (allPkgs.length) {
|
|
2279
|
+
html += `<div style="font-size:11px;font-weight:600;color:var(--muted);margin-bottom:6px">npm — ${data.npm.name}@${data.npm.version} (${allPkgs.length} packages)</div>`;
|
|
2280
|
+
html += allPkgs.map(p =>
|
|
2281
|
+
`<div class="perm-tool-row">
|
|
2282
|
+
<span class="perm-tool-name mono" style="font-size:11px">${esc(p.name)}</span>
|
|
2283
|
+
<span style="display:flex;gap:8px;align-items:center">
|
|
2284
|
+
<span class="perm-tool-count mono">${esc(p.version)}</span>
|
|
2285
|
+
${p.type === 'dev' ? `<span style="font-size:9px;color:var(--dim);border:1px solid var(--border-dim);padding:1px 4px;border-radius:3px">dev</span>` : ''}
|
|
2286
|
+
</span>
|
|
2287
|
+
</div>`
|
|
2288
|
+
).join('');
|
|
2289
|
+
} else {
|
|
2290
|
+
html += '<span class="perm-empty">No npm dependencies</span>';
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
if (data.pip && data.pip.length) {
|
|
2295
|
+
html += `<div style="font-size:11px;font-weight:600;color:var(--muted);margin:14px 0 6px">pip (${data.pip.length} packages)</div>`;
|
|
2296
|
+
html += data.pip.map(p =>
|
|
2297
|
+
`<div class="perm-tool-row">
|
|
2298
|
+
<span class="perm-tool-name mono" style="font-size:11px">${esc(p.name)}</span>
|
|
2299
|
+
<span class="perm-tool-count mono">${esc(p.version)}</span>
|
|
2300
|
+
</div>`
|
|
2301
|
+
).join('');
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
for (const o of (data.other || [])) {
|
|
2305
|
+
html += `<div style="font-size:11px;font-weight:600;color:var(--muted);margin:14px 0 6px">${esc(o.file)}</div>`;
|
|
2306
|
+
html += o.packages.map(p => `<div class="perm-tool-row"><span class="perm-tool-name mono" style="font-size:11px">${esc(p)}</span></div>`).join('');
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
if (!data.npm && (!data.pip || !data.pip.length) && !data.other?.length) {
|
|
2310
|
+
html += '<span class="perm-empty">No package files found (package.json, requirements.txt)</span>';
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
return html;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function switchTab(tabEl, targetId) {
|
|
2317
|
+
const parent = tabEl.closest('.section') || tabEl.parentElement.parentElement;
|
|
2318
|
+
parent.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
2319
|
+
parent.querySelectorAll('.tab-body').forEach(b => b.classList.remove('active'));
|
|
2320
|
+
tabEl.classList.add('active');
|
|
2321
|
+
const target = parent.querySelector('#' + targetId);
|
|
2322
|
+
if (target) target.classList.add('active');
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// ── Payload rendering ─────────────────────────────────────────────────────
|
|
2326
|
+
|
|
2327
|
+
function highlightJson(obj) {
|
|
2328
|
+
const raw = JSON.stringify(obj, null, 2);
|
|
2329
|
+
return raw
|
|
2330
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
2331
|
+
.replace(
|
|
2332
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+\.?\d*([eE][+-]?\d+)?)/g,
|
|
2333
|
+
m => {
|
|
2334
|
+
if (/^"/.test(m)) {
|
|
2335
|
+
return /:$/.test(m)
|
|
2336
|
+
? `<span class="jk">${m}</span>`
|
|
2337
|
+
: `<span class="js">${m}</span>`;
|
|
2338
|
+
}
|
|
2339
|
+
if (/true|false/.test(m)) return `<span class="jb">${m}</span>`;
|
|
2340
|
+
if (/null/.test(m)) return `<span class="jz">${m}</span>`;
|
|
2341
|
+
return `<span class="jn">${m}</span>`;
|
|
2342
|
+
}
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function copyCode(btn, text) {
|
|
2347
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
2348
|
+
btn.textContent = 'Copied!';
|
|
2349
|
+
btn.classList.add('copied');
|
|
2350
|
+
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500);
|
|
2351
|
+
}).catch(() => {
|
|
2352
|
+
btn.textContent = 'Failed';
|
|
2353
|
+
setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function codeBlock(text, extraClass) {
|
|
2358
|
+
const cls = 'code-block' + (extraClass ? ' ' + extraClass : '');
|
|
2359
|
+
return `<div class="code-wrap"><div class="${cls}">${esc(text)}</div>` +
|
|
2360
|
+
`<button class="code-copy" onclick="copyCode(this,${JSON.stringify(text)})">Copy</button></div>`;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function jsonBlock(obj) {
|
|
2364
|
+
const raw = JSON.stringify(obj, null, 2);
|
|
2365
|
+
return `<div class="code-wrap"><div class="code-block">${highlightJson(obj)}</div>` +
|
|
2366
|
+
`<button class="code-copy" onclick="copyCode(this,${JSON.stringify(raw)})">Copy</button></div>`;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function payloadField(label, content) {
|
|
2370
|
+
return `<div class="payload-field"><div class="payload-field-label">${esc(label)}</div>${content}</div>`;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function renderInputPayload(name, input) {
|
|
2374
|
+
if (!input || !Object.keys(input).length) return '';
|
|
2375
|
+
|
|
2376
|
+
if (input._truncated) {
|
|
2377
|
+
return `<div style="padding:10px 14px;background:#2d1f06;border:1px solid #5a3a10;border-radius:8px;color:#fbbf24;font-size:12px">
|
|
2378
|
+
Input exceeded 64 KB and was not stored. Check the session transcript for the full content.
|
|
2379
|
+
</div>`;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
if (name === 'Bash') {
|
|
2383
|
+
const cmd = input.command || '';
|
|
2384
|
+
return payloadField('Command', codeBlock(cmd));
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
if (name === 'Write') {
|
|
2388
|
+
return payloadField('File', `<div class="payload-path">${esc(input.file_path || '')}</div>`) +
|
|
2389
|
+
(input.content != null ? payloadField('Content', codeBlock(input.content)) : '');
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
if (name === 'Edit' || name === 'NotebookEdit') {
|
|
2393
|
+
let out = payloadField('File', `<div class="payload-path">${esc(input.file_path || input.notebook_path || '')}</div>`);
|
|
2394
|
+
if (input.old_string != null) {
|
|
2395
|
+
out += payloadField('Remove', `<div class="payload-diff-old">${esc(input.old_string)}</div>`);
|
|
2396
|
+
out += payloadField('Insert', `<div class="payload-diff-new">${esc(input.new_string || '')}</div>`);
|
|
2397
|
+
} else if (input.edits) {
|
|
2398
|
+
// MultiEdit
|
|
2399
|
+
input.edits.forEach((e, i) => {
|
|
2400
|
+
out += payloadField(`Edit ${i + 1} — Remove`, `<div class="payload-diff-old">${esc(e.old_string)}</div>`);
|
|
2401
|
+
out += payloadField(`Edit ${i + 1} — Insert`, `<div class="payload-diff-new">${esc(e.new_string || '')}</div>`);
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
return out;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
if (name === 'Read') {
|
|
2408
|
+
let out = payloadField('File', `<div class="payload-path">${esc(input.file_path || '')}</div>`);
|
|
2409
|
+
const meta = {};
|
|
2410
|
+
if (input.offset != null) meta.offset = input.offset;
|
|
2411
|
+
if (input.limit != null) meta.limit = input.limit;
|
|
2412
|
+
if (Object.keys(meta).length) out += payloadField('Range', jsonBlock(meta));
|
|
2413
|
+
return out;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
if (name === 'Agent' || name === 'Task') {
|
|
2417
|
+
let out = '';
|
|
2418
|
+
if (input.prompt) out += payloadField('Prompt', `<div class="payload-text">${esc(input.prompt)}</div>`);
|
|
2419
|
+
const rest = Object.fromEntries(Object.entries(input).filter(([k]) => k !== 'prompt'));
|
|
2420
|
+
if (Object.keys(rest).length) out += payloadField('Options', jsonBlock(rest));
|
|
2421
|
+
return out;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
if (name === 'Grep') {
|
|
2425
|
+
let out = payloadField('Pattern', codeBlock(input.pattern || ''));
|
|
2426
|
+
if (input.path) out += payloadField('Path', `<div class="payload-path">${esc(input.path)}</div>`);
|
|
2427
|
+
const rest = Object.fromEntries(Object.entries(input).filter(([k]) => !['pattern','path'].includes(k)));
|
|
2428
|
+
if (Object.keys(rest).length) out += payloadField('Options', jsonBlock(rest));
|
|
2429
|
+
return out;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
if (name === 'Glob') {
|
|
2433
|
+
let out = payloadField('Pattern', codeBlock(input.pattern || ''));
|
|
2434
|
+
if (input.path) out += payloadField('Path', `<div class="payload-path">${esc(input.path)}</div>`);
|
|
2435
|
+
return out;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
if (name === 'WebFetch' || name === 'WebSearch') {
|
|
2439
|
+
const key = input.url || input.query || '';
|
|
2440
|
+
const label = input.url ? 'URL' : 'Query';
|
|
2441
|
+
let out = payloadField(label, `<div class="payload-path">${esc(key)}</div>`);
|
|
2442
|
+
const rest = Object.fromEntries(Object.entries(input).filter(([k]) => k !== 'url' && k !== 'query'));
|
|
2443
|
+
if (Object.keys(rest).length) out += payloadField('Options', jsonBlock(rest));
|
|
2444
|
+
return out;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// Default: syntax-highlighted JSON
|
|
2448
|
+
return jsonBlock(input);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function renderOutputPayload(name, output) {
|
|
2452
|
+
if (output === undefined || output === null) return '';
|
|
2453
|
+
if (typeof output === 'string') {
|
|
2454
|
+
// Try to detect and highlight JSON strings
|
|
2455
|
+
const trimmed = output.trim();
|
|
2456
|
+
if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && trimmed.length < 50000) {
|
|
2457
|
+
try {
|
|
2458
|
+
const parsed = JSON.parse(trimmed);
|
|
2459
|
+
return jsonBlock(parsed);
|
|
2460
|
+
} catch (e) { /* not JSON */ }
|
|
2461
|
+
}
|
|
2462
|
+
return codeBlock(output);
|
|
2463
|
+
}
|
|
2464
|
+
return jsonBlock(output);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// ── Search / filter ──────────────────────────────────────────────────────
|
|
2468
|
+
let searchQuery = '';
|
|
2469
|
+
function applySearch(q) {
|
|
2470
|
+
searchQuery = q.toLowerCase().trim();
|
|
2471
|
+
if (!lastRoot) return;
|
|
2472
|
+
applySearchToTree(lastRoot);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function applySearchToTree(root) {
|
|
2476
|
+
if (!root) return;
|
|
2477
|
+
// Walk all agent groups and tool rows, show/hide based on query
|
|
2478
|
+
for (const [sid, els] of agentEls) {
|
|
2479
|
+
const node = findNodeAnywhere(lastRoot, sid);
|
|
2480
|
+
if (!node) continue;
|
|
2481
|
+
if (!searchQuery) {
|
|
2482
|
+
els.group.style.display = '';
|
|
2483
|
+
for (const [tkey, rowEl] of toolEls) {
|
|
2484
|
+
if (tkey.startsWith(sid + ':')) rowEl.style.display = '';
|
|
2485
|
+
}
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
// Check if agent label matches
|
|
2489
|
+
const agentMatch = node.label.toLowerCase().includes(searchQuery);
|
|
2490
|
+
let anyToolMatch = false;
|
|
2491
|
+
for (const [tkey, rowEl] of toolEls) {
|
|
2492
|
+
if (!tkey.startsWith(sid + ':')) continue;
|
|
2493
|
+
const toolId = tkey.slice(sid.length + 1);
|
|
2494
|
+
const tc = node.toolCalls.find(t => t.id === toolId);
|
|
2495
|
+
const match = tc && (tc.name.toLowerCase().includes(searchQuery) ||
|
|
2496
|
+
(tc.summary || '').toLowerCase().includes(searchQuery));
|
|
2497
|
+
rowEl.style.display = (agentMatch || match) ? '' : 'none';
|
|
2498
|
+
if (match) anyToolMatch = true;
|
|
2499
|
+
}
|
|
2500
|
+
els.group.style.display = (agentMatch || anyToolMatch) ? '' : 'none';
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function findNodeAnywhere(root, sessionId) {
|
|
2505
|
+
if (!root) return null;
|
|
2506
|
+
if (root.sessionId === sessionId) return root;
|
|
2507
|
+
for (const c of (root.children || [])) {
|
|
2508
|
+
const f = findNodeAnywhere(c, sessionId);
|
|
2509
|
+
if (f) return f;
|
|
2510
|
+
}
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// ── Graph (agent tree map) ────────────────────────────────────────────────
|
|
2515
|
+
let graphOpen = false;
|
|
2516
|
+
let graphAutoOpened = false;
|
|
2517
|
+
|
|
2518
|
+
function toggleGraph() {
|
|
2519
|
+
if (graphOpen) closeGraph(); else openGraph();
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
function openGraph() {
|
|
2523
|
+
graphOpen = true;
|
|
2524
|
+
document.getElementById('graph-overlay').classList.add('open');
|
|
2525
|
+
document.getElementById('graph-btn').classList.add('active');
|
|
2526
|
+
renderGraph();
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function closeGraph() {
|
|
2530
|
+
graphOpen = false;
|
|
2531
|
+
document.getElementById('graph-overlay').classList.remove('open');
|
|
2532
|
+
document.getElementById('graph-btn').classList.remove('active');
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// ── Graph state ──────────────────────────────────────────────────────────
|
|
2536
|
+
let graphScale = 1, graphTx = 24, graphTy = 24;
|
|
2537
|
+
let graphPanning = false, graphPanStart = null;
|
|
2538
|
+
let graphDocMM = null, graphDocMU = null; // document-level handlers (cleaned up on re-render)
|
|
2539
|
+
let graphLayout = null; // { nodes: [{node, x, y}], totalW, totalH }
|
|
2540
|
+
|
|
2541
|
+
function graphZoom(delta) {
|
|
2542
|
+
const newScale = Math.max(0.3, Math.min(3, graphScale + delta));
|
|
2543
|
+
const wrap = document.getElementById('graph-svg-wrap');
|
|
2544
|
+
if (!wrap) return;
|
|
2545
|
+
const rect = wrap.getBoundingClientRect();
|
|
2546
|
+
const cx = rect.width / 2, cy = rect.height / 2;
|
|
2547
|
+
graphTx = cx - (cx - graphTx) * (newScale / graphScale);
|
|
2548
|
+
graphTy = cy - (cy - graphTy) * (newScale / graphScale);
|
|
2549
|
+
graphScale = newScale;
|
|
2550
|
+
applyGraphTransform();
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function graphFit() {
|
|
2554
|
+
if (!graphLayout) return;
|
|
2555
|
+
const wrap = document.getElementById('graph-svg-wrap');
|
|
2556
|
+
if (!wrap) return;
|
|
2557
|
+
const { width: ww, height: wh } = wrap.getBoundingClientRect();
|
|
2558
|
+
const pad = 32;
|
|
2559
|
+
const scaleX = (ww - pad * 2) / (graphLayout.totalW || 1);
|
|
2560
|
+
const scaleY = (wh - pad * 2) / (graphLayout.totalH || 1);
|
|
2561
|
+
graphScale = Math.max(0.3, Math.min(2, Math.min(scaleX, scaleY)));
|
|
2562
|
+
graphTx = (ww - graphLayout.totalW * graphScale) / 2;
|
|
2563
|
+
graphTy = pad;
|
|
2564
|
+
applyGraphTransform();
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function applyGraphTransform() {
|
|
2568
|
+
const g = document.getElementById('graph-content');
|
|
2569
|
+
if (g) g.setAttribute('transform', `translate(${graphTx.toFixed(1)},${graphTy.toFixed(1)}) scale(${graphScale.toFixed(3)})`);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
function renderGraph() {
|
|
2573
|
+
if (!graphOpen) return;
|
|
2574
|
+
const wrap = document.getElementById('graph-svg-wrap');
|
|
2575
|
+
if (!lastRoot) {
|
|
2576
|
+
wrap.innerHTML = '<div style="color:var(--muted);font-size:12px;padding:20px">No active session</div>';
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
const NODE_W = 200, NODE_H = 64, H_GAP = 28, V_GAP = 60;
|
|
2581
|
+
|
|
2582
|
+
// Compute positions
|
|
2583
|
+
let leafCounter = 0;
|
|
2584
|
+
function computeLayout(node, depth) {
|
|
2585
|
+
const children = (node.children || []).map(c => computeLayout(c, depth + 1));
|
|
2586
|
+
let x;
|
|
2587
|
+
if (children.length === 0) {
|
|
2588
|
+
x = leafCounter++ * (NODE_W + H_GAP);
|
|
2589
|
+
} else {
|
|
2590
|
+
x = (children[0].x + children[children.length - 1].x) / 2;
|
|
2591
|
+
}
|
|
2592
|
+
return { node, x, y: depth * (NODE_H + V_GAP), children };
|
|
2593
|
+
}
|
|
2594
|
+
const layout = computeLayout(lastRoot, 0);
|
|
2595
|
+
const totalW = Math.max(leafCounter * (NODE_W + H_GAP) - H_GAP, NODE_W);
|
|
2596
|
+
|
|
2597
|
+
function getMaxDepth(l) {
|
|
2598
|
+
if (!l.children.length) return 0;
|
|
2599
|
+
return 1 + Math.max(...l.children.map(getMaxDepth));
|
|
2600
|
+
}
|
|
2601
|
+
const totalH = (getMaxDepth(layout) + 1) * (NODE_H + V_GAP) - V_GAP;
|
|
2602
|
+
graphLayout = { totalW, totalH };
|
|
2603
|
+
|
|
2604
|
+
// Collect all layout nodes flat (for tooltip lookup)
|
|
2605
|
+
const flatNodes = [];
|
|
2606
|
+
function collectFlat(l) { flatNodes.push(l); l.children.forEach(collectFlat); }
|
|
2607
|
+
collectFlat(layout);
|
|
2608
|
+
|
|
2609
|
+
// Build SVG content
|
|
2610
|
+
let edges = '', nodes = '';
|
|
2611
|
+
function buildSvg(l) {
|
|
2612
|
+
const cx = l.x + NODE_W / 2;
|
|
2613
|
+
for (const child of l.children) {
|
|
2614
|
+
const ccx = child.x + NODE_W / 2;
|
|
2615
|
+
const midy = l.y + NODE_H + V_GAP / 2;
|
|
2616
|
+
const edgeCls = child.node.isWorktree ? ' worktree' : child.node.isSidechain ? ' sidechain' : '';
|
|
2617
|
+
edges += `<path class="gedge${edgeCls}" d="M ${cx} ${l.y + NODE_H} C ${cx} ${midy}, ${ccx} ${midy}, ${ccx} ${child.y}"/>`;
|
|
2618
|
+
}
|
|
2619
|
+
const isSelected = selected?.sessionId === l.node.sessionId;
|
|
2620
|
+
const status = l.node.status || 'running';
|
|
2621
|
+
const toolCount = l.node.toolCalls?.length || 0;
|
|
2622
|
+
const cost = l.node.costUsd > 0 ? `$${l.node.costUsd.toFixed(3)}` : '';
|
|
2623
|
+
const duration = l.node.endedAt ? dur(l.node.endedAt - l.node.startedAt) : '…';
|
|
2624
|
+
const dotColor = status === 'running' ? '#d29922' : status === 'done' ? '#3fb950' : '#f85149';
|
|
2625
|
+
const compactCount = (l.node.compactions || []).length;
|
|
2626
|
+
const isWorktree = !!l.node.isWorktree;
|
|
2627
|
+
const isSidechain = !!l.node.isSidechain;
|
|
2628
|
+
|
|
2629
|
+
let typeBadge = '';
|
|
2630
|
+
if (isWorktree) {
|
|
2631
|
+
typeBadge = `<text x="${l.x + 10}" y="${l.y + 58}" font-size="9" fill="#2dd4bf" font-weight="700">⎇ WORKTREE</text>`;
|
|
2632
|
+
} else if (isSidechain) {
|
|
2633
|
+
typeBadge = `<text x="${l.x + 10}" y="${l.y + 58}" font-size="9" fill="#c084fc" font-weight="700">⊕ INLINE</text>`;
|
|
2634
|
+
} else if (l.node.permissionMode && l.node.permissionMode !== 'default') {
|
|
2635
|
+
const pColor = l.node.permissionMode === 'bypassPermissions' ? '#f85149' :
|
|
2636
|
+
l.node.permissionMode === 'auto' ? '#d29922' :
|
|
2637
|
+
l.node.permissionMode === 'acceptEdits' ? '#79c0ff' : '#c084fc';
|
|
2638
|
+
typeBadge = `<text x="${l.x + 10}" y="${l.y + 58}" font-size="9" fill="${pColor}" font-weight="700">${esc(l.node.permissionMode.toUpperCase())}</text>`;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
const nodeH = typeBadge ? NODE_H + 12 : NODE_H;
|
|
2642
|
+
const nodeClass = `gnode ${status}${isWorktree ? ' worktree' : isSidechain ? ' sidechain' : ''}${isSelected ? ' selected' : ''}`;
|
|
2643
|
+
|
|
2644
|
+
// Running pulse ring
|
|
2645
|
+
const pulse = status === 'running'
|
|
2646
|
+
? `<circle cx="${l.x + 14}" cy="${l.y + 14}" r="5" fill="none" stroke="#d29922" stroke-width="1" opacity="0.3">
|
|
2647
|
+
<animate attributeName="r" values="5;11;5" dur="2s" repeatCount="indefinite"/>
|
|
2648
|
+
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite"/>
|
|
2649
|
+
</circle>`
|
|
2650
|
+
: '';
|
|
2651
|
+
|
|
2652
|
+
nodes += `
|
|
2653
|
+
<g class="${nodeClass}" data-graph-sid="${esc(l.node.sessionId)}">
|
|
2654
|
+
<rect x="${l.x}" y="${l.y}" width="${NODE_W}" height="${nodeH}" rx="7"/>
|
|
2655
|
+
${pulse}
|
|
2656
|
+
<circle cx="${l.x + 14}" cy="${l.y + 14}" r="5" fill="${dotColor}"/>
|
|
2657
|
+
<text x="${l.x + 26}" y="${l.y + 18}" font-size="12" font-weight="600" fill="#e6edf3">${esc(l.node.label.slice(0, 22))}</text>
|
|
2658
|
+
<text x="${l.x + 10}" y="${l.y + 36}" font-size="10" fill="#8b949e">${isSidechain ? 'inline agent' : toolCount + ' tool' + (toolCount !== 1 ? 's' : '') + (compactCount ? ` · ${compactCount}⇢` : '')}</text>
|
|
2659
|
+
<text x="${l.x + NODE_W - 10}" y="${l.y + 36}" font-size="10" fill="#3fb950" text-anchor="end">${cost}</text>
|
|
2660
|
+
<text x="${l.x + NODE_W - 10}" y="${l.y + 50}" font-size="9" fill="#6e7681" text-anchor="end">${duration}</text>
|
|
2661
|
+
${typeBadge}
|
|
2662
|
+
</g>`;
|
|
2663
|
+
|
|
2664
|
+
for (const child of l.children) buildSvg(child);
|
|
2665
|
+
}
|
|
2666
|
+
buildSvg(layout);
|
|
2667
|
+
|
|
2668
|
+
const svgW = totalW + 48;
|
|
2669
|
+
const svgH = totalH + 48;
|
|
2670
|
+
|
|
2671
|
+
const prevSvg = wrap.querySelector('svg');
|
|
2672
|
+
if (!prevSvg) {
|
|
2673
|
+
// First render — reset transform and fit
|
|
2674
|
+
graphTx = 24; graphTy = 24; graphScale = 1;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
wrap.innerHTML = `<svg id="graph-svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}"
|
|
2678
|
+
xmlns="http://www.w3.org/2000/svg" style="display:block;overflow:visible;font-family:system-ui,sans-serif;cursor:grab">
|
|
2679
|
+
<style>
|
|
2680
|
+
.gnode rect { transition: fill 0.12s; }
|
|
2681
|
+
.gnode:hover rect { fill: #21262d !important; }
|
|
2682
|
+
.gnode.selected rect { fill: #1a2d45 !important; stroke: #58a6ff !important; stroke-width: 2 !important; }
|
|
2683
|
+
</style>
|
|
2684
|
+
<g id="graph-content" transform="translate(${graphTx.toFixed(1)},${graphTy.toFixed(1)}) scale(${graphScale.toFixed(3)})">
|
|
2685
|
+
${edges}${nodes}
|
|
2686
|
+
</g>
|
|
2687
|
+
</svg>`;
|
|
2688
|
+
|
|
2689
|
+
// Bind pan/zoom/tooltip on the new SVG
|
|
2690
|
+
const svg = wrap.querySelector('svg');
|
|
2691
|
+
const tooltip = document.getElementById('graph-tooltip');
|
|
2692
|
+
|
|
2693
|
+
// Pan
|
|
2694
|
+
svg.addEventListener('mousedown', e => {
|
|
2695
|
+
if (e.target.closest('.gnode')) return;
|
|
2696
|
+
graphPanning = true;
|
|
2697
|
+
graphPanStart = { x: e.clientX - graphTx, y: e.clientY - graphTy };
|
|
2698
|
+
svg.style.cursor = 'grabbing';
|
|
2699
|
+
e.preventDefault();
|
|
2700
|
+
});
|
|
2701
|
+
if (graphDocMM) document.removeEventListener('mousemove', graphDocMM);
|
|
2702
|
+
if (graphDocMU) document.removeEventListener('mouseup', graphDocMU);
|
|
2703
|
+
graphDocMM = e => {
|
|
2704
|
+
if (!graphPanning) return;
|
|
2705
|
+
graphTx = e.clientX - graphPanStart.x;
|
|
2706
|
+
graphTy = e.clientY - graphPanStart.y;
|
|
2707
|
+
applyGraphTransform();
|
|
2708
|
+
};
|
|
2709
|
+
graphDocMU = () => {
|
|
2710
|
+
if (graphPanning) { graphPanning = false; if (svg) svg.style.cursor = 'grab'; }
|
|
2711
|
+
};
|
|
2712
|
+
document.addEventListener('mousemove', graphDocMM);
|
|
2713
|
+
document.addEventListener('mouseup', graphDocMU);
|
|
2714
|
+
|
|
2715
|
+
// Wheel zoom
|
|
2716
|
+
svg.addEventListener('wheel', e => {
|
|
2717
|
+
e.preventDefault();
|
|
2718
|
+
const rect = svg.getBoundingClientRect();
|
|
2719
|
+
const mx = e.clientX - rect.left;
|
|
2720
|
+
const my = e.clientY - rect.top;
|
|
2721
|
+
const delta = e.deltaY < 0 ? 0.1 : -0.1;
|
|
2722
|
+
const newScale = Math.max(0.3, Math.min(3, graphScale + delta));
|
|
2723
|
+
graphTx = mx - (mx - graphTx) * (newScale / graphScale);
|
|
2724
|
+
graphTy = my - (my - graphTy) * (newScale / graphScale);
|
|
2725
|
+
graphScale = newScale;
|
|
2726
|
+
applyGraphTransform();
|
|
2727
|
+
}, { passive: false });
|
|
2728
|
+
|
|
2729
|
+
// Hover tooltip
|
|
2730
|
+
svg.addEventListener('mouseover', e => {
|
|
2731
|
+
const g = e.target.closest('[data-graph-sid]');
|
|
2732
|
+
if (!g || !tooltip) return;
|
|
2733
|
+
const sid = g.dataset.graphSid;
|
|
2734
|
+
const entry = flatNodes.find(n => n.node.sessionId === sid);
|
|
2735
|
+
if (!entry) return;
|
|
2736
|
+
const n = entry.node;
|
|
2737
|
+
const statusLabel = n.status === 'running' ? '⟳ Running' : n.status === 'done' ? '✓ Done' : '✗ Error';
|
|
2738
|
+
const statusColor = n.status === 'running' ? '#d29922' : n.status === 'done' ? '#3fb950' : '#f85149';
|
|
2739
|
+
const lines = [
|
|
2740
|
+
`<div style="font-weight:600;color:var(--text);margin-bottom:6px">${esc(n.label)}</div>`,
|
|
2741
|
+
`<div style="color:${statusColor}">${statusLabel}</div>`,
|
|
2742
|
+
n.costUsd > 0 ? `<div>Cost: <span style="color:var(--green)">$${n.costUsd.toFixed(4)}</span></div>` : '',
|
|
2743
|
+
`<div>Tools: <span style="color:var(--muted)">${n.toolCalls?.length || 0}</span></div>`,
|
|
2744
|
+
n.tokens?.input ? `<div>Tokens: <span style="color:var(--dim)">${fmt(n.tokens.input + n.tokens.output)} in/out</span></div>` : '',
|
|
2745
|
+
n.endedAt ? `<div>Duration: <span style="color:var(--dim)">${dur(n.endedAt - n.startedAt)}</span></div>` : '',
|
|
2746
|
+
n.gitBranch ? `<div>Branch: <span style="color:var(--blue-soft)">${esc(n.gitBranch)}</span></div>` : '',
|
|
2747
|
+
n.permissionMode && n.permissionMode !== 'default' ? `<div>Mode: <span style="color:var(--yellow)">${esc(n.permissionMode)}</span></div>` : '',
|
|
2748
|
+
].filter(Boolean).join('');
|
|
2749
|
+
tooltip.innerHTML = lines;
|
|
2750
|
+
tooltip.classList.add('visible');
|
|
2751
|
+
});
|
|
2752
|
+
svg.addEventListener('mousemove', e => {
|
|
2753
|
+
if (!tooltip?.classList.contains('visible')) return;
|
|
2754
|
+
tooltip.style.left = (e.clientX + 14) + 'px';
|
|
2755
|
+
tooltip.style.top = (e.clientY - 8) + 'px';
|
|
2756
|
+
});
|
|
2757
|
+
svg.addEventListener('mouseout', e => {
|
|
2758
|
+
if (!e.target.closest('[data-graph-sid]') && tooltip) tooltip.classList.remove('visible');
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// ── Conversation thread ──────────────────────────────────────────────────
|
|
2763
|
+
const threadCache = new Map(); // sid → rendered HTML
|
|
2764
|
+
|
|
2765
|
+
async function loadThread(sid, containerId) {
|
|
2766
|
+
const el = document.getElementById(containerId);
|
|
2767
|
+
if (!el) return;
|
|
2768
|
+
el.innerHTML = '<div style="color:var(--dim);font-size:12px">Loading…</div>';
|
|
2769
|
+
try {
|
|
2770
|
+
const res = await fetch(`/api/sessions/${encodeURIComponent(sid)}/thread`);
|
|
2771
|
+
if (!res.ok) { el.innerHTML = '<div style="color:var(--muted);font-size:12px">No transcript available yet.</div>'; return; }
|
|
2772
|
+
const msgs = await res.json();
|
|
2773
|
+
if (!msgs.length) { el.innerHTML = '<div style="color:var(--dim);font-size:12px">No messages found.</div>'; return; }
|
|
2774
|
+
const html = msgs.map(m => {
|
|
2775
|
+
const isUser = m.role === 'user';
|
|
2776
|
+
const ts = m.timestamp ? new Date(m.timestamp).toLocaleTimeString() : '';
|
|
2777
|
+
return `<div style="margin-bottom:12px;${isUser ? 'padding-left:0' : 'padding-left:12px;border-left:2px solid var(--border-dim)'}">
|
|
2778
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
|
2779
|
+
<span style="font-size:10px;font-weight:700;color:${isUser ? 'var(--blue-soft)' : 'var(--purple)'};text-transform:uppercase;letter-spacing:0.05em">${isUser ? 'User' : 'Claude'}</span>
|
|
2780
|
+
${ts ? `<span style="font-size:10px;color:var(--dim)">${ts}</span>` : ''}
|
|
2781
|
+
</div>
|
|
2782
|
+
<div style="font-size:12px;color:var(--muted);white-space:pre-wrap;word-break:break-word;line-height:1.6">${esc(m.text)}</div>
|
|
2783
|
+
</div>`;
|
|
2784
|
+
}).join('<div style="height:1px;background:var(--border-dim);margin:12px 0"></div>');
|
|
2785
|
+
if (threadCache.size >= 30) threadCache.delete(threadCache.keys().next().value); // evict oldest
|
|
2786
|
+
threadCache.set(sid, html);
|
|
2787
|
+
el.innerHTML = html;
|
|
2788
|
+
} catch(e) {
|
|
2789
|
+
el.innerHTML = '<div style="color:var(--red);font-size:12px">Failed to load thread.</div>';
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// Auto-open graph when subagents are first detected
|
|
2794
|
+
let knownAgentCount = 0;
|
|
2795
|
+
function maybeAutoOpenGraph(root) {
|
|
2796
|
+
if (!root) return;
|
|
2797
|
+
function countAgents(n) {
|
|
2798
|
+
return 1 + (n.children || []).reduce((s, c) => s + countAgents(c), 0);
|
|
2799
|
+
}
|
|
2800
|
+
const count = countAgents(root);
|
|
2801
|
+
if (count > 1 && count !== knownAgentCount && !graphOpen) {
|
|
2802
|
+
knownAgentCount = count;
|
|
2803
|
+
openGraph();
|
|
2804
|
+
graphAutoOpened = true;
|
|
2805
|
+
} else {
|
|
2806
|
+
knownAgentCount = count;
|
|
2807
|
+
if (graphOpen) renderGraph(); // refresh graph on each update
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
</script>
|
|
2811
|
+
</body>
|
|
2812
|
+
</html>
|