coding-agent-wrapper 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,847 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Trajectory Viewer</title>
7
+ <style>
8
+ /* ── Theme variables ─────────────────────────────────────────────── */
9
+ [data-theme="dark"] {
10
+ --bg-primary: #1a1a2e;
11
+ --bg-secondary: #16213e;
12
+ --bg-tertiary: #0f3460;
13
+ --bg-hover: #1a2744;
14
+ --text-primary: #eee;
15
+ --text-secondary: #aaa;
16
+ --text-tertiary: #888;
17
+ --text-muted: #666;
18
+ --text-placeholder: #555;
19
+ --border-color: #0f3460;
20
+ --accent-red: #e94560;
21
+ --accent-green: #4ecca3;
22
+ --accent-blue: #4a90d9;
23
+ --bg-input: #0f3460;
24
+ --bg-disabled: #444;
25
+ --text-on-dark: #1a1a2e;
26
+ --text-ddd: #ddd;
27
+ }
28
+ [data-theme="light"] {
29
+ --bg-primary: #f5f5f5;
30
+ --bg-secondary: #ffffff;
31
+ --bg-tertiary: #e8edf2;
32
+ --bg-hover: #eef2f7;
33
+ --text-primary: #1a1a2e;
34
+ --text-secondary: #555;
35
+ --text-tertiary: #777;
36
+ --text-muted: #999;
37
+ --text-placeholder: #aaa;
38
+ --border-color: #d0d7de;
39
+ --accent-red: #d63851;
40
+ --accent-green: #2da67e;
41
+ --accent-blue: #3a7bc8;
42
+ --bg-input: #e8edf2;
43
+ --bg-disabled: #ccc;
44
+ --text-on-dark: #1a1a2e;
45
+ --text-ddd: #333;
46
+ }
47
+
48
+ /* ── Reset ───────────────────────────────────────────────────────── */
49
+ * { box-sizing: border-box; margin: 0; padding: 0; }
50
+ body {
51
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
52
+ background: var(--bg-primary);
53
+ color: var(--text-primary);
54
+ min-height: 100vh;
55
+ }
56
+
57
+ /* ── Theme toggle ────────────────────────────────────────────────── */
58
+ .theme-toggle {
59
+ background: var(--bg-tertiary);
60
+ border: 1px solid var(--border-color);
61
+ color: var(--text-tertiary);
62
+ width: 28px; height: 28px;
63
+ border-radius: 4px;
64
+ cursor: pointer;
65
+ display: flex; align-items: center; justify-content: center;
66
+ font-size: 14px;
67
+ transition: all 0.2s;
68
+ flex-shrink: 0;
69
+ }
70
+ .theme-toggle:hover {
71
+ background: var(--bg-hover);
72
+ color: var(--accent-red);
73
+ border-color: var(--accent-red);
74
+ }
75
+
76
+ /* ── Trajectory Viewer ───────────────────────────────────────────── */
77
+ .tj-page {
78
+ max-width: 1140px;
79
+ margin: 0 auto;
80
+ padding: 24px;
81
+ min-height: 100vh;
82
+ }
83
+ .tj-header-bar {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 16px;
87
+ margin-bottom: 20px;
88
+ }
89
+ .tj-title {
90
+ font-size: 22px;
91
+ flex: 1;
92
+ }
93
+ .tj-path-form {
94
+ display: flex;
95
+ gap: 10px;
96
+ margin-bottom: 16px;
97
+ }
98
+ .tj-path-input {
99
+ flex: 1;
100
+ padding: 10px 14px;
101
+ border-radius: 8px;
102
+ border: 1px solid var(--border-color);
103
+ background: var(--bg-input);
104
+ color: var(--text-primary);
105
+ font-family: 'JetBrains Mono', 'SFMono-Regular', monospace;
106
+ font-size: 14px;
107
+ }
108
+ .tj-path-input::placeholder { color: var(--text-placeholder); }
109
+ .tj-load-btn {
110
+ padding: 10px 24px;
111
+ border-radius: 8px;
112
+ border: 1px solid var(--accent-blue);
113
+ background: var(--accent-blue);
114
+ color: #fff;
115
+ cursor: pointer;
116
+ font-weight: 600;
117
+ font-size: 14px;
118
+ white-space: nowrap;
119
+ }
120
+ .tj-load-btn:hover { opacity: 0.9; }
121
+ .tj-load-btn:disabled { opacity: 0.5; cursor: not-allowed; }
122
+ .tj-error {
123
+ background: var(--accent-red);
124
+ color: #fff;
125
+ padding: 12px 16px;
126
+ border-radius: 8px;
127
+ margin-bottom: 16px;
128
+ font-size: 14px;
129
+ }
130
+ .tj-source-display {
131
+ background: var(--bg-secondary);
132
+ border: 1px solid var(--border-color);
133
+ border-radius: 8px;
134
+ padding: 10px 16px;
135
+ margin-bottom: 20px;
136
+ font-size: 13px;
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 8px;
140
+ overflow-x: auto;
141
+ }
142
+ .tj-source-label {
143
+ color: var(--text-muted);
144
+ font-weight: 600;
145
+ text-transform: uppercase;
146
+ font-size: 11px;
147
+ letter-spacing: .05em;
148
+ white-space: nowrap;
149
+ }
150
+ .tj-source-display code {
151
+ font-family: 'JetBrains Mono', 'SFMono-Regular', monospace;
152
+ color: var(--text-secondary);
153
+ word-break: break-all;
154
+ }
155
+ .tj-conversation-layout {
156
+ display: flex;
157
+ gap: 12px;
158
+ align-items: flex-start;
159
+ }
160
+ .tj-conversation-card {
161
+ flex: 1;
162
+ min-width: 0;
163
+ }
164
+ .tj-sidebar {
165
+ position: sticky;
166
+ top: 16px;
167
+ flex-shrink: 0;
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 8px;
171
+ height: calc(100vh - 32px);
172
+ }
173
+ .tj-collapse-tools-btn {
174
+ background: var(--bg-tertiary);
175
+ border: 1px solid var(--border-color);
176
+ border-radius: 6px;
177
+ color: var(--text-tertiary);
178
+ font-size: 11px;
179
+ padding: 5px 6px;
180
+ cursor: pointer;
181
+ white-space: nowrap;
182
+ }
183
+ .tj-collapse-tools-btn:hover {
184
+ color: var(--accent-blue);
185
+ border-color: var(--accent-blue);
186
+ }
187
+ .tj-minimap {
188
+ width: 18px;
189
+ flex-shrink: 0;
190
+ flex: 1;
191
+ min-height: 0;
192
+ display: flex;
193
+ flex-direction: column;
194
+ gap: 1px;
195
+ }
196
+ .tj-minimap-block {
197
+ width: 18px;
198
+ flex: 1;
199
+ min-height: 0;
200
+ border-radius: 2px;
201
+ cursor: pointer;
202
+ opacity: 0.5;
203
+ transition: opacity 0.15s, transform 0.15s;
204
+ border-bottom: 1px solid var(--bg-primary);
205
+ }
206
+ .tj-minimap-block:hover { opacity: 0.85; transform: scaleX(1.4); }
207
+ .tj-minimap-block.active { opacity: 1; transform: scaleX(1.6); }
208
+ .tj-card {
209
+ background: var(--bg-secondary);
210
+ border: 1px solid var(--border-color);
211
+ border-radius: 12px;
212
+ padding: 24px;
213
+ margin-bottom: 24px;
214
+ }
215
+ .tj-card h2 {
216
+ margin-bottom: 16px;
217
+ font-size: 17px;
218
+ color: var(--text-primary);
219
+ }
220
+ .tj-metadata-grid {
221
+ display: grid;
222
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
223
+ gap: 12px;
224
+ }
225
+ .tj-metadata-item {
226
+ background: var(--bg-tertiary);
227
+ border-radius: 8px;
228
+ padding: 10px 14px;
229
+ }
230
+ .tj-metadata-label {
231
+ font-size: 11px;
232
+ text-transform: uppercase;
233
+ letter-spacing: .05em;
234
+ color: var(--text-muted);
235
+ margin-bottom: 2px;
236
+ }
237
+ .tj-metadata-value {
238
+ font-weight: 600;
239
+ font-size: 14px;
240
+ color: var(--text-primary);
241
+ word-break: break-all;
242
+ }
243
+ .tj-tool-stat {
244
+ display: flex;
245
+ justify-content: space-between;
246
+ padding: 6px 0;
247
+ border-bottom: 1px solid var(--border-color);
248
+ font-size: 14px;
249
+ }
250
+ .tj-tool-name { font-weight: 500; color: var(--text-primary); }
251
+ .tj-tool-count { font-family: monospace; color: var(--text-secondary); }
252
+ .tj-muted { color: var(--text-muted); font-size: 14px; }
253
+ .tj-message {
254
+ border-left: 4px solid var(--border-color);
255
+ padding: 12px 16px;
256
+ margin-bottom: 10px;
257
+ background: var(--bg-primary);
258
+ border-radius: 8px;
259
+ }
260
+ .tj-message.tj-role-system { border-left-color: var(--text-muted); }
261
+ .tj-message.tj-role-user { border-left-color: var(--accent-blue); }
262
+ .tj-message.tj-role-assistant { border-left-color: var(--accent-green); }
263
+ .tj-message.tj-role-tool { border-left-color: #e67e22; }
264
+ .tj-message-header {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 10px;
268
+ margin-bottom: 6px;
269
+ user-select: none;
270
+ cursor: pointer;
271
+ }
272
+ .tj-message-role {
273
+ font-size: 12px;
274
+ text-transform: uppercase;
275
+ letter-spacing: .05em;
276
+ color: var(--text-muted);
277
+ font-weight: 700;
278
+ }
279
+ .tj-message-index { font-size: 11px; color: var(--text-muted); }
280
+ .tj-message-model {
281
+ font-size: 11px;
282
+ color: var(--text-tertiary);
283
+ font-family: monospace;
284
+ }
285
+ .tj-collapse-icon {
286
+ font-size: 10px;
287
+ color: var(--text-muted);
288
+ margin-left: auto;
289
+ }
290
+ .tj-message-content {
291
+ font-size: 14px;
292
+ line-height: 1.55;
293
+ color: var(--text-primary);
294
+ }
295
+ .tj-text { margin: 4px 0; }
296
+ .tj-code {
297
+ font-family: 'JetBrains Mono', 'SFMono-Regular', monospace;
298
+ background: var(--bg-tertiary);
299
+ border-radius: 6px;
300
+ padding: 10px 12px;
301
+ display: block;
302
+ white-space: pre-wrap;
303
+ word-break: break-all;
304
+ margin: 6px 0;
305
+ font-size: 12px;
306
+ color: var(--text-secondary);
307
+ max-height: 400px;
308
+ overflow-y: auto;
309
+ }
310
+ .tj-tool-bundle {
311
+ border: 1px solid var(--border-color);
312
+ border-radius: 8px;
313
+ margin: 8px 0;
314
+ padding: 10px 12px;
315
+ background: var(--bg-secondary);
316
+ }
317
+ .tj-tool-call-name {
318
+ font-weight: 600;
319
+ color: var(--accent-blue);
320
+ font-size: 13px;
321
+ margin-bottom: 4px;
322
+ }
323
+ .tj-tool-result {
324
+ margin-top: 8px;
325
+ border-top: 1px solid var(--border-color);
326
+ padding-top: 8px;
327
+ }
328
+ .tj-tool-result.error .tj-tool-call-name { color: var(--accent-red); }
329
+ .tj-subagent {
330
+ margin-top: 8px;
331
+ background: var(--bg-secondary);
332
+ border-radius: 8px;
333
+ border: 1px solid var(--border-color);
334
+ padding: 10px 12px;
335
+ }
336
+ .tj-subagent h5 { margin: 0 0 6px 0; font-size: 13px; color: var(--text-muted); }
337
+ .tj-subagent-meta { font-size: 12px; color: var(--text-tertiary); margin-bottom: 4px; }
338
+ .tj-subagent-line { font-size: 13px; margin-bottom: 4px; }
339
+ .tj-expand-btn {
340
+ background: none;
341
+ border: 1px solid var(--border-color);
342
+ border-radius: 4px;
343
+ color: var(--text-tertiary);
344
+ font-size: 12px;
345
+ padding: 2px 10px;
346
+ cursor: pointer;
347
+ margin-top: 4px;
348
+ }
349
+ .tj-expand-btn:hover { color: var(--accent-blue); border-color: var(--accent-blue); }
350
+ .tj-role { font-weight: 600; margin-right: 6px; color: var(--accent-blue); }
351
+
352
+ /* ── Thinking block ──────────────────────────────────────────────── */
353
+ .tj-thinking {
354
+ border-left: 3px solid #a855f7;
355
+ padding-left: 10px;
356
+ margin: 6px 0;
357
+ opacity: 0.85;
358
+ }
359
+ .tj-thinking-label {
360
+ font-size: 11px;
361
+ text-transform: uppercase;
362
+ letter-spacing: .05em;
363
+ color: #a855f7;
364
+ font-weight: 600;
365
+ margin-bottom: 2px;
366
+ }
367
+
368
+ </style>
369
+ </head>
370
+ <body>
371
+
372
+ <div class="tj-page" id="app">
373
+ <div class="tj-header-bar">
374
+ <h1 class="tj-title">Trajectory Viewer</h1>
375
+ <button class="theme-toggle" id="theme-toggle-btn"></button>
376
+ </div>
377
+
378
+ <form class="tj-path-form" id="path-form">
379
+ <input type="text" class="tj-path-input" id="path-input"
380
+ placeholder="/path/to/trajectory.json" spellcheck="false">
381
+ <button type="submit" class="tj-load-btn" id="load-btn">Load</button>
382
+ </form>
383
+
384
+ <div class="tj-error" id="error-display" style="display:none"></div>
385
+ <div id="content"></div>
386
+ </div>
387
+
388
+ <script>
389
+ /* ──────────────────────────────────────────────────────────────────
390
+ State
391
+ ────────────────────────────────────────────────────────────────── */
392
+ const state = {
393
+ trajPath: new URLSearchParams(location.search).get('local') || '',
394
+ data: null,
395
+ loading: false,
396
+ error: null,
397
+ toolsCollapsed: false,
398
+ theme: localStorage.getItem('theme') || 'dark',
399
+ };
400
+
401
+ const ROLE_COLORS = {
402
+ system: '#636e72',
403
+ user: '#4a90d9',
404
+ assistant: '#4ecca3',
405
+ tool: '#e67e22',
406
+ };
407
+ const COLLAPSED_LINES = 5;
408
+ let minimapObserver = null;
409
+
410
+ /* ──────────────────────────────────────────────────────────────────
411
+ Helpers
412
+ ────────────────────────────────────────────────────────────────── */
413
+ function esc(t) {
414
+ return String(t)
415
+ .replace(/&/g, '&amp;')
416
+ .replace(/</g, '&lt;')
417
+ .replace(/>/g, '&gt;')
418
+ .replace(/"/g, '&quot;')
419
+ .replace(/'/g, '&#x27;');
420
+ }
421
+
422
+ function fmtDuration(ms) {
423
+ if (!ms) return '0s';
424
+ if (ms < 1000) return ms + 'ms';
425
+ const s = ms / 1000;
426
+ if (s < 60) return s.toFixed(1) + 's';
427
+ const m = Math.floor(s / 60);
428
+ return m + 'm ' + Math.round(s % 60) + 's';
429
+ }
430
+
431
+ function fmtCost(usd) {
432
+ if (usd == null) return '';
433
+ return '$' + usd.toFixed(4);
434
+ }
435
+
436
+ function fmtTokens(n) {
437
+ if (n == null) return '';
438
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
439
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
440
+ return String(n);
441
+ }
442
+
443
+ function fmtTime(iso) {
444
+ if (!iso) return '';
445
+ try {
446
+ const d = new Date(iso);
447
+ return d.toLocaleString();
448
+ } catch { return iso; }
449
+ }
450
+
451
+ /* ──────────────────────────────────────────────────────────────────
452
+ Data transformation: caw trajectory → viewer model
453
+ ────────────────────────────────────────────────────────────────── */
454
+ function buildViewData(raw) {
455
+ const usage = raw.total_usage || raw.usage || {};
456
+
457
+ // Metadata
458
+ const meta = {};
459
+ if (raw.agent) meta.agent = raw.agent;
460
+ if (raw.model) meta.model = raw.model;
461
+ if (raw.session_id) meta.session_id = raw.session_id;
462
+ if (raw.created_at) meta.created_at = raw.created_at;
463
+ if (raw.reasoning) meta.reasoning = raw.reasoning;
464
+ meta.num_turns = (raw.turns || []).length;
465
+ if (raw.duration_ms) meta.duration = fmtDuration(raw.duration_ms);
466
+ if (usage.input_tokens) meta.input_tokens = fmtTokens(usage.input_tokens);
467
+ if (usage.output_tokens) meta.output_tokens = fmtTokens(usage.output_tokens);
468
+ if (usage.cache_read_tokens) meta.cache_read_tokens = fmtTokens(usage.cache_read_tokens);
469
+ if (usage.cache_write_tokens) meta.cache_write_tokens = fmtTokens(usage.cache_write_tokens);
470
+ if (usage.cost_usd) meta.cost = fmtCost(usage.cost_usd);
471
+
472
+ // Custom metadata
473
+ for (const [k, v] of Object.entries(raw.metadata || {})) {
474
+ meta[k] = typeof v === 'object' ? JSON.stringify(v) : String(v);
475
+ }
476
+
477
+ // Tool usage counts
478
+ const toolUsage = {};
479
+ function countTools(blocks) {
480
+ for (const b of (blocks || [])) {
481
+ if (b.type === 'tool_use') {
482
+ toolUsage[b.name] = (toolUsage[b.name] || 0) + 1;
483
+ if (b.subagent_trajectory) {
484
+ for (const t of (b.subagent_trajectory.turns || []))
485
+ countTools(t.output);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ for (const turn of (raw.turns || [])) countTools(turn.output);
491
+
492
+ // Messages list — split each turn's output into separate messages
493
+ // when block types alternate between text/thinking and tool_use.
494
+ const messages = [];
495
+ if (raw.system_prompt) {
496
+ messages.push({ role: 'system', content: [{ type: 'text', text: raw.system_prompt }] });
497
+ }
498
+ for (const turn of (raw.turns || [])) {
499
+ messages.push({ role: 'user', content: [{ type: 'text', text: turn.input }] });
500
+ const output = turn.output || [];
501
+
502
+ // Group consecutive blocks: text/thinking together, tool_use together
503
+ const groups = [];
504
+ for (const block of output) {
505
+ const isTool = block.type === 'tool_use';
506
+ if (groups.length === 0 || groups[groups.length - 1].isTool !== isTool) {
507
+ groups.push({ isTool, blocks: [block] });
508
+ } else {
509
+ groups[groups.length - 1].blocks.push(block);
510
+ }
511
+ }
512
+
513
+ // First group gets usage/duration metadata
514
+ for (let gi = 0; gi < groups.length; gi++) {
515
+ const g = groups[gi];
516
+ messages.push({
517
+ role: g.isTool ? 'tool' : 'assistant',
518
+ model: raw.model,
519
+ content: g.blocks,
520
+ ...(gi === 0 ? { usage: turn.usage, duration_ms: turn.duration_ms } : {}),
521
+ });
522
+ }
523
+ }
524
+ return { metadata: meta, tool_usage: toolUsage, messages };
525
+ }
526
+
527
+ /* ──────────────────────────────────────────────────────────────────
528
+ Rendering helpers
529
+ ────────────────────────────────────────────────────────────────── */
530
+ function renderMetadataGrid(meta) {
531
+ const priorityKeys = ['agent', 'model', 'session_id', 'created_at'];
532
+ const ordered = [
533
+ ...priorityKeys.filter(k => k in meta),
534
+ ...Object.keys(meta).filter(k => !priorityKeys.includes(k)),
535
+ ];
536
+ return '<div class="tj-metadata-grid">' +
537
+ ordered.map(k =>
538
+ '<div class="tj-metadata-item">' +
539
+ '<div class="tj-metadata-label">' + esc(k.replace(/_/g, ' ')) + '</div>' +
540
+ '<div class="tj-metadata-value">' + esc(meta[k]) + '</div>' +
541
+ '</div>'
542
+ ).join('') +
543
+ '</div>';
544
+ }
545
+
546
+ function renderToolUsage(usage) {
547
+ const entries = Object.entries(usage).sort(([,a],[,b]) => b - a);
548
+ if (!entries.length) return '<p class="tj-muted">No tool usage recorded.</p>';
549
+ return entries.map(([name, count]) =>
550
+ '<div class="tj-tool-stat">' +
551
+ '<span class="tj-tool-name">' + esc(name) + '</span>' +
552
+ '<span class="tj-tool-count">' + count + '</span>' +
553
+ '</div>'
554
+ ).join('');
555
+ }
556
+
557
+ function renderCollapsiblePre(text) {
558
+ if (!text) return '';
559
+ const lines = text.split('\n');
560
+ if (lines.length > COLLAPSED_LINES) {
561
+ return '<div class="tj-collapsible">' +
562
+ '<pre class="tj-code tj-code-short">' + esc(lines.slice(0, COLLAPSED_LINES).join('\n')) + '</pre>' +
563
+ '<pre class="tj-code tj-code-full" style="display:none">' + esc(text) + '</pre>' +
564
+ '<button class="tj-expand-btn" onclick="togglePre(this)">Show all ' + lines.length + ' lines</button>' +
565
+ '</div>';
566
+ }
567
+ return '<pre class="tj-code">' + esc(text) + '</pre>';
568
+ }
569
+
570
+ function renderTextBlock(text) {
571
+ if (!text) return '';
572
+ const lines = text.split('\n');
573
+ if (lines.length > COLLAPSED_LINES) return renderCollapsiblePre(text);
574
+ return '<p class="tj-text">' + esc(text).replace(/\n/g, '<br>') + '</p>';
575
+ }
576
+
577
+ function renderContentBlock(block) {
578
+ if (block.type === 'text') return renderTextBlock(block.text || '');
579
+
580
+ if (block.type === 'thinking') {
581
+ return '<div class="tj-thinking">' +
582
+ '<div class="tj-thinking-label">Thinking</div>' +
583
+ renderCollapsiblePre(block.text || '') +
584
+ '</div>';
585
+ }
586
+
587
+ if (block.type === 'tool_use') {
588
+ const args = block.arguments && Object.keys(block.arguments).length > 0
589
+ ? JSON.stringify(block.arguments, null, 2) : 'no arguments';
590
+ let resultHtml = '';
591
+ if (block.output != null && block.output !== '') {
592
+ const isErr = !!block.is_error;
593
+ const out = typeof block.output === 'string' ? block.output : JSON.stringify(block.output, null, 2);
594
+ resultHtml = '<div class="tj-tool-result' + (isErr ? ' error' : '') + '">' +
595
+ '<div class="tj-tool-call-name">' + (isErr ? '&#10060; Error' : '&#9989; Result') + '</div>' +
596
+ renderCollapsiblePre(out) +
597
+ '</div>';
598
+ }
599
+ let subHtml = '';
600
+ if (block.subagent_trajectory) {
601
+ subHtml = renderSubagentTrajectory(block.subagent_trajectory);
602
+ }
603
+ return '<div class="tj-tool-bundle">' +
604
+ '<div class="tj-tool-call-name">&#128295; ' + esc(block.name) + '</div>' +
605
+ renderCollapsiblePre(args) +
606
+ resultHtml + subHtml +
607
+ '</div>';
608
+ }
609
+
610
+ // Unknown block type
611
+ return '<div class="tj-muted">' + esc(JSON.stringify(block)) + '</div>';
612
+ }
613
+
614
+ function renderSubagentTrajectory(traj) {
615
+ const turns = traj.turns || [];
616
+ const agent = traj.agent || '';
617
+ const model = traj.model || '';
618
+ let linesHtml = '';
619
+ for (const turn of turns) {
620
+ const inputPreview = (turn.input || '').slice(0, 200);
621
+ linesHtml += '<div class="tj-subagent-line"><span class="tj-role">user</span>' + esc(inputPreview) + (turn.input.length > 200 ? '...' : '') + '</div>';
622
+ for (const block of (turn.output || [])) {
623
+ if (block.type === 'text') {
624
+ const preview = (block.text || '').slice(0, 200);
625
+ linesHtml += '<div class="tj-subagent-line"><span class="tj-role">assistant</span>' + esc(preview) + (block.text.length > 200 ? '...' : '') + '</div>';
626
+ } else if (block.type === 'tool_use') {
627
+ linesHtml += '<div class="tj-subagent-line"><span class="tj-role">tool</span>&#128295; ' + esc(block.name) + '</div>';
628
+ }
629
+ }
630
+ }
631
+ const label = 'Subagent' + (agent ? ' (' + esc(agent) + ')' : '') + (model ? ' &mdash; ' + esc(model) : '');
632
+ return '<div class="tj-subagent">' +
633
+ '<h5>' + label + '</h5>' +
634
+ '<div class="tj-subagent-meta"><strong>Turns:</strong> ' + turns.length + '</div>' +
635
+ linesHtml +
636
+ '</div>';
637
+ }
638
+
639
+ function renderMessage(msg, index, total) {
640
+ const role = msg.role;
641
+ const startCollapsed = role === 'system';
642
+ const displayNone = startCollapsed ? ' style="display:none"' : '';
643
+ const icon = startCollapsed ? '&#9654;' : '&#9660;';
644
+
645
+ let headerExtra = '';
646
+ if (msg.model) headerExtra += '<span class="tj-message-model">' + esc(msg.model) + '</span>';
647
+
648
+ const blocks = (msg.content || []).map(b => renderContentBlock(b)).join('');
649
+
650
+ return '<div id="tj-msg-' + index + '" class="tj-message tj-role-' + esc(role) + '">' +
651
+ '<div class="tj-message-header" onclick="toggleMsg(' + index + ')">' +
652
+ '<span class="tj-message-role">' + esc(role) + '</span>' +
653
+ '<span class="tj-message-index">#' + (index + 1) + '</span>' +
654
+ headerExtra +
655
+ '<span class="tj-collapse-icon" id="tj-msg-icon-' + index + '">' + icon + '</span>' +
656
+ '</div>' +
657
+ '<div class="tj-message-content" id="tj-msg-content-' + index + '"' + displayNone + '>' +
658
+ blocks +
659
+ '</div>' +
660
+ '</div>';
661
+ }
662
+
663
+ function renderMinimap(messages) {
664
+ return messages.map((msg, i) => {
665
+ const color = ROLE_COLORS[msg.role] || '#636e72';
666
+ return '<div class="tj-minimap-block" style="background-color:' + color + '" ' +
667
+ 'title="#' + (i + 1) + ' ' + esc(msg.role) + '" ' +
668
+ 'onclick="scrollToMsg(' + i + ')"></div>';
669
+ }).join('');
670
+ }
671
+
672
+ /* ──────────────────────────────────────────────────────────────────
673
+ Main render
674
+ ────────────────────────────────────────────────────────────────── */
675
+ function render() {
676
+ const content = document.getElementById('content');
677
+ if (!state.data) { content.innerHTML = ''; return; }
678
+
679
+ const vd = buildViewData(state.data);
680
+
681
+ content.innerHTML =
682
+ '<div class="tj-source-display">' +
683
+ '<span class="tj-source-label">Source:</span>' +
684
+ '<code>' + esc(state.trajPath) + '</code>' +
685
+ '</div>' +
686
+
687
+ '<div class="tj-card"><h2>Metadata</h2>' + renderMetadataGrid(vd.metadata) + '</div>' +
688
+ '<div class="tj-card"><h2>Tool Usage</h2>' + renderToolUsage(vd.tool_usage) + '</div>' +
689
+
690
+ '<div class="tj-conversation-layout">' +
691
+ '<div class="tj-card tj-conversation-card">' +
692
+ '<h2>Conversation (' + vd.messages.length + ' messages)</h2>' +
693
+ vd.messages.map((m, i) => renderMessage(m, i, vd.messages.length)).join('') +
694
+ '</div>' +
695
+ '<div class="tj-sidebar">' +
696
+ '<button class="tj-collapse-tools-btn" id="tools-toggle-btn" onclick="toggleToolsCollapsed()">' +
697
+ (state.toolsCollapsed ? 'Show Tools' : 'Hide Tools') +
698
+ '</button>' +
699
+ '<div class="tj-minimap">' + renderMinimap(vd.messages) + '</div>' +
700
+ '</div>' +
701
+ '</div>';
702
+
703
+ setupMinimapObserver();
704
+ }
705
+
706
+ /* ──────────────────────────────────────────────────────────────────
707
+ Interaction handlers
708
+ ────────────────────────────────────────────────────────────────── */
709
+ function toggleMsg(index) {
710
+ const c = document.getElementById('tj-msg-content-' + index);
711
+ const icon = document.getElementById('tj-msg-icon-' + index);
712
+ if (!c) return;
713
+ if (c.style.display === 'none') {
714
+ c.style.display = '';
715
+ icon.innerHTML = '&#9660;';
716
+ } else {
717
+ c.style.display = 'none';
718
+ icon.innerHTML = '&#9654;';
719
+ }
720
+ }
721
+
722
+ function togglePre(btn) {
723
+ const container = btn.closest('.tj-collapsible');
724
+ const short = container.querySelector('.tj-code-short');
725
+ const full = container.querySelector('.tj-code-full');
726
+ if (full.style.display === 'none') {
727
+ short.style.display = 'none';
728
+ full.style.display = '';
729
+ btn.textContent = 'Collapse';
730
+ } else {
731
+ short.style.display = '';
732
+ full.style.display = 'none';
733
+ const totalLines = full.textContent.split('\n').length;
734
+ btn.textContent = 'Show all ' + totalLines + ' lines';
735
+ }
736
+ }
737
+
738
+ function toggleToolsCollapsed() {
739
+ state.toolsCollapsed = !state.toolsCollapsed;
740
+ document.getElementById('tools-toggle-btn').textContent =
741
+ state.toolsCollapsed ? 'Show Tools' : 'Hide Tools';
742
+ document.querySelectorAll('.tj-message.tj-role-tool').forEach(el => {
743
+ const idx = el.id.replace('tj-msg-', '');
744
+ const c = document.getElementById('tj-msg-content-' + idx);
745
+ const icon = document.getElementById('tj-msg-icon-' + idx);
746
+ if (c) {
747
+ c.style.display = state.toolsCollapsed ? 'none' : '';
748
+ icon.innerHTML = state.toolsCollapsed ? '&#9654;' : '&#9660;';
749
+ }
750
+ });
751
+ }
752
+
753
+ function scrollToMsg(i) {
754
+ const el = document.getElementById('tj-msg-' + i);
755
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
756
+ }
757
+
758
+ function setupMinimapObserver() {
759
+ if (minimapObserver) minimapObserver.disconnect();
760
+ const msgEls = document.querySelectorAll('[id^="tj-msg-"]');
761
+ if (!msgEls.length) return;
762
+ minimapObserver = new IntersectionObserver(entries => {
763
+ for (const entry of entries) {
764
+ if (entry.isIntersecting) {
765
+ const idx = parseInt(entry.target.id.replace('tj-msg-', ''), 10);
766
+ if (!isNaN(idx)) {
767
+ document.querySelectorAll('.tj-minimap-block').forEach((b, i) => {
768
+ b.classList.toggle('active', i === idx);
769
+ });
770
+ }
771
+ }
772
+ }
773
+ }, { rootMargin: '-20% 0px -70% 0px' });
774
+ msgEls.forEach(el => { if (el.id.match(/^tj-msg-\d+$/)) minimapObserver.observe(el); });
775
+ }
776
+
777
+ /* ──────────────────────────────────────────────────────────────────
778
+ API calls
779
+ ────────────────────────────────────────────────────────────────── */
780
+ function loadTrajectory(path) {
781
+ if (!path.trim()) return;
782
+ state.loading = true;
783
+ state.error = null;
784
+ document.getElementById('load-btn').disabled = true;
785
+ document.getElementById('load-btn').textContent = 'Loading\u2026';
786
+ document.getElementById('error-display').style.display = 'none';
787
+
788
+ const url = new URL(location.href);
789
+ url.searchParams.set('local', path);
790
+ history.replaceState({}, '', url.toString());
791
+
792
+ fetch('/api/trajectory?path=' + encodeURIComponent(path))
793
+ .then(res => {
794
+ if (!res.ok) return res.json().then(d => { throw new Error(d.detail || 'HTTP ' + res.status); });
795
+ return res.json();
796
+ })
797
+ .then(data => {
798
+ state.data = data;
799
+ state.trajPath = path;
800
+ state.toolsCollapsed = false;
801
+ render();
802
+ })
803
+ .catch(err => {
804
+ state.data = null;
805
+ state.error = err.message || 'Failed to load trajectory';
806
+ document.getElementById('error-display').textContent = state.error;
807
+ document.getElementById('error-display').style.display = '';
808
+ document.getElementById('content').innerHTML = '';
809
+ })
810
+ .finally(() => {
811
+ state.loading = false;
812
+ document.getElementById('load-btn').disabled = false;
813
+ document.getElementById('load-btn').textContent = 'Load';
814
+ });
815
+ }
816
+
817
+ /* ──────────────────────────────────────────────────────────────────
818
+ Init
819
+ ────────────────────────────────────────────────────────────────── */
820
+ (function init() {
821
+ // Theme
822
+ function applyTheme() {
823
+ document.documentElement.setAttribute('data-theme', state.theme);
824
+ localStorage.setItem('theme', state.theme);
825
+ document.getElementById('theme-toggle-btn').textContent =
826
+ state.theme === 'dark' ? '\u2600\uFE0F' : '\uD83C\uDF19';
827
+ }
828
+ applyTheme();
829
+ document.getElementById('theme-toggle-btn').addEventListener('click', () => {
830
+ state.theme = state.theme === 'dark' ? 'light' : 'dark';
831
+ applyTheme();
832
+ });
833
+
834
+ // Path form
835
+ const pathInput = document.getElementById('path-input');
836
+ pathInput.value = state.trajPath;
837
+ document.getElementById('path-form').addEventListener('submit', e => {
838
+ e.preventDefault();
839
+ loadTrajectory(pathInput.value);
840
+ });
841
+
842
+ // Auto-load from URL
843
+ if (state.trajPath) loadTrajectory(state.trajPath);
844
+ })();
845
+ </script>
846
+ </body>
847
+ </html>