prompt_objects 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +2 -2
  5. data/exe/prompt_objects +548 -1
  6. data/frontend/src/App.tsx +11 -3
  7. data/frontend/src/components/ContextMenu.tsx +67 -0
  8. data/frontend/src/components/MessageBus.tsx +4 -3
  9. data/frontend/src/components/ModelSelector.tsx +5 -1
  10. data/frontend/src/components/ThreadsSidebar.tsx +46 -2
  11. data/frontend/src/components/UsagePanel.tsx +105 -0
  12. data/frontend/src/hooks/useWebSocket.ts +53 -0
  13. data/frontend/src/store/index.ts +10 -0
  14. data/frontend/src/types/index.ts +4 -1
  15. data/lib/prompt_objects/cli.rb +1 -0
  16. data/lib/prompt_objects/connectors/mcp.rb +1 -0
  17. data/lib/prompt_objects/environment.rb +24 -1
  18. data/lib/prompt_objects/llm/anthropic_adapter.rb +15 -1
  19. data/lib/prompt_objects/llm/factory.rb +93 -6
  20. data/lib/prompt_objects/llm/gemini_adapter.rb +13 -1
  21. data/lib/prompt_objects/llm/openai_adapter.rb +21 -4
  22. data/lib/prompt_objects/llm/pricing.rb +49 -0
  23. data/lib/prompt_objects/llm/response.rb +3 -2
  24. data/lib/prompt_objects/mcp/server.rb +1 -0
  25. data/lib/prompt_objects/message_bus.rb +27 -8
  26. data/lib/prompt_objects/prompt_object.rb +6 -4
  27. data/lib/prompt_objects/server/api/routes.rb +186 -29
  28. data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +1 -0
  29. data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +77 -0
  30. data/lib/prompt_objects/server/public/index.html +2 -2
  31. data/lib/prompt_objects/server/websocket_handler.rb +93 -9
  32. data/lib/prompt_objects/server.rb +54 -0
  33. data/lib/prompt_objects/session/store.rb +399 -4
  34. data/lib/prompt_objects.rb +1 -0
  35. data/prompt_objects.gemspec +1 -1
  36. data/templates/arc-agi-1/manifest.yml +22 -0
  37. data/templates/arc-agi-1/objects/data_manager.md +42 -0
  38. data/templates/arc-agi-1/objects/observer.md +100 -0
  39. data/templates/arc-agi-1/objects/solver.md +118 -0
  40. data/templates/arc-agi-1/objects/verifier.md +79 -0
  41. data/templates/arc-agi-1/primitives/check_arc_data.rb +53 -0
  42. data/templates/arc-agi-1/primitives/find_objects.rb +72 -0
  43. data/templates/arc-agi-1/primitives/grid_diff.rb +70 -0
  44. data/templates/arc-agi-1/primitives/grid_info.rb +42 -0
  45. data/templates/arc-agi-1/primitives/grid_transform.rb +50 -0
  46. data/templates/arc-agi-1/primitives/load_arc_task.rb +68 -0
  47. data/templates/arc-agi-1/primitives/render_grid.rb +78 -0
  48. data/templates/arc-agi-1/primitives/test_solution.rb +131 -0
  49. data/tools/thread-explorer.html +1043 -0
  50. metadata +21 -3
  51. data/lib/prompt_objects/server/public/assets/index-CeNJvqLG.js +0 -77
  52. data/lib/prompt_objects/server/public/assets/index-Vx4-uMOU.css +0 -1
@@ -0,0 +1,1043 @@
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>Thread Explorer | PromptObjects</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* ======== THEME ======== */
12
+ :root {
13
+ --bg-deep: #0a0e0a;
14
+ --bg: #0d120d;
15
+ --bg-raised: #131a13;
16
+ --bg-surface: #182018;
17
+ --border: #1e2b1e;
18
+ --border-bright: #2a3d2a;
19
+ --phosphor: #33ff66;
20
+ --phosphor-dim: #1a8033;
21
+ --phosphor-faint: #0d4020;
22
+ --phosphor-glow: rgba(51,255,102,0.15);
23
+ --amber: #ffaa33;
24
+ --amber-dim: #805515;
25
+ --amber-glow: rgba(255,170,51,0.12);
26
+ --cyan: #33ddff;
27
+ --cyan-dim: #1a7088;
28
+ --magenta: #dd55ff;
29
+ --magenta-dim: #6b2a80;
30
+ --red: #ff4455;
31
+ --red-dim: #661a22;
32
+ --pink: #ff6699;
33
+ --teal: #33ffcc;
34
+ --yellow: #ffdd44;
35
+ --text: #b8ccb8;
36
+ --text-dim: #667766;
37
+ --text-bright: #ddeedd;
38
+ --mono: 'IBM Plex Mono','Menlo',monospace;
39
+ }
40
+ * { margin:0; padding:0; box-sizing:border-box; }
41
+ html,body { height:100%; background:var(--bg-deep); color:var(--text); font-family:var(--mono); font-size:13px; line-height:1.6; overflow:hidden; }
42
+ body::after { content:''; position:fixed; inset:0; background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.03) 2px,rgba(0,0,0,0.03) 4px); pointer-events:none; z-index:9999; }
43
+ ::selection { background:var(--phosphor-dim); color:var(--text-bright); }
44
+ ::-webkit-scrollbar { width:5px; height:5px; }
45
+ ::-webkit-scrollbar-track { background:var(--bg); }
46
+ ::-webkit-scrollbar-thumb { background:var(--border-bright); border-radius:3px; }
47
+ ::-webkit-scrollbar-thumb:hover { background:var(--phosphor-dim); }
48
+ button { font-family:var(--mono); cursor:pointer; }
49
+
50
+ /* ======== DROP ZONE ======== */
51
+ #drop-zone { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:100; background:var(--bg-deep); transition:opacity 0.4s; }
52
+ #drop-zone.hidden { opacity:0; pointer-events:none; }
53
+ #drop-zone .logo { font-size:36px; font-weight:700; color:var(--phosphor); text-shadow:0 0 30px var(--phosphor-glow); letter-spacing:-1px; margin-bottom:6px; animation:flicker 4s ease-in-out infinite; }
54
+ #drop-zone .subtitle { font-size:11px; color:var(--text-dim); margin-bottom:40px; letter-spacing:3px; text-transform:uppercase; }
55
+ .drop-area { width:480px; height:240px; border:2px dashed var(--border-bright); border-radius:4px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; transition:all 0.2s; cursor:pointer; background:var(--bg); }
56
+ .drop-area:hover,.drop-area.dragover { border-color:var(--phosphor-dim); background:rgba(51,255,102,0.02); }
57
+ .drop-area .icon { font-size:40px; color:var(--phosphor-dim); transition:color 0.2s; }
58
+ .drop-area:hover .icon,.drop-area.dragover .icon { color:var(--phosphor); }
59
+ .drop-area p { color:var(--text-dim); font-size:13px; }
60
+ .drop-area p span { color:var(--phosphor); text-decoration:underline; text-underline-offset:3px; }
61
+ .drop-area .hint { font-size:11px; opacity:0.5; }
62
+ #file-input { display:none; }
63
+ .sample-link { margin-top:16px; font-size:11px; color:var(--text-dim); cursor:pointer; text-decoration:underline; text-underline-offset:3px; opacity:0.6; transition:opacity 0.2s; }
64
+ .sample-link:hover { opacity:1; color:var(--phosphor-dim); }
65
+
66
+ /* ======== APP LAYOUT ======== */
67
+ #app { display:none; height:100vh; grid-template-columns:1fr; grid-template-rows:auto auto 1fr; }
68
+ #app.visible { display:grid; }
69
+
70
+ /* Header */
71
+ #header { display:flex; align-items:center; gap:16px; padding:0 16px; height:40px; background:var(--bg); border-bottom:1px solid var(--border); }
72
+ .brand-name { font-weight:700; font-size:14px; color:var(--phosphor); text-shadow:0 0 20px var(--phosphor-glow); white-space:nowrap; }
73
+ .brand-sep { color:var(--border-bright); }
74
+ .thread-name { color:var(--text); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
75
+ .header-stats { display:flex; gap:14px; font-size:11px; color:var(--text-dim); margin-left:auto; white-space:nowrap; }
76
+ .header-stats .v { color:var(--text); font-weight:500; }
77
+ .btn-open { background:none; border:1px solid var(--border-bright); color:var(--text-dim); font-size:10px; padding:3px 10px; border-radius:3px; transition:all 0.15s; margin-left:8px; }
78
+ .btn-open:hover { border-color:var(--phosphor-dim); color:var(--phosphor); }
79
+
80
+ /* Token cost bar */
81
+ #cost-bar { display:flex; height:20px; background:var(--bg); border-bottom:1px solid var(--border); padding:0 16px; align-items:center; gap:8px; }
82
+ #cost-bar .label { font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.5px; white-space:nowrap; }
83
+ #cost-bar .bar { flex:1; height:8px; background:var(--bg-deep); border-radius:2px; overflow:hidden; display:flex; }
84
+ #cost-bar .bar-seg { height:100%; transition:opacity 0.15s; cursor:pointer; position:relative; min-width:2px; }
85
+ #cost-bar .bar-seg:hover { opacity:0.8; }
86
+ #cost-bar .bar-legend { display:flex; gap:10px; font-size:10px; color:var(--text-dim); }
87
+ #cost-bar .bar-legend .dot { display:inline-block; width:6px; height:6px; border-radius:50%; margin-right:3px; vertical-align:middle; }
88
+
89
+ /* Toolbar */
90
+ #toolbar { display:flex; align-items:center; height:32px; background:var(--bg); border-bottom:1px solid var(--border); padding:0 16px; gap:8px; }
91
+ .tab { background:none; border:none; color:var(--text-dim); font-size:11px; padding:4px 12px; border-radius:3px; transition:all 0.15s; text-transform:uppercase; letter-spacing:0.5px; font-weight:500; }
92
+ .tab:hover { color:var(--text); background:var(--bg-raised); }
93
+ .tab.active { color:var(--phosphor); background:var(--phosphor-faint); }
94
+ .toolbar-sep { width:1px; height:16px; background:var(--border); }
95
+ #search { background:var(--bg-deep); border:1px solid var(--border); color:var(--text); font-family:var(--mono); font-size:11px; padding:3px 8px; border-radius:3px; width:200px; outline:none; transition:border-color 0.15s; }
96
+ #search:focus { border-color:var(--phosphor-dim); }
97
+ #search::placeholder { color:var(--text-dim); }
98
+ .filter-btns { display:flex; gap:4px; margin-left:auto; }
99
+ .filter-btn { background:none; border:1px solid var(--border); color:var(--text-dim); font-size:10px; padding:2px 8px; border-radius:3px; transition:all 0.15s; }
100
+ .filter-btn:hover { border-color:var(--border-bright); color:var(--text); }
101
+ .filter-btn.active { border-color:var(--phosphor-dim); color:var(--phosphor); background:var(--phosphor-faint); }
102
+
103
+ /* ======== MAIN CONTENT ======== */
104
+ #main { overflow:hidden; display:grid; }
105
+
106
+ /* Split view */
107
+ #main.split { grid-template-columns:1fr 1fr; }
108
+ #main.full { grid-template-columns:1fr; }
109
+
110
+ /* ======== SEQUENCE VIEW ======== */
111
+ #seq-view { overflow:auto; background:var(--bg-deep); }
112
+ .seq-actors { display:flex; position:sticky; top:0; z-index:5; background:var(--bg); border-bottom:1px solid var(--border); }
113
+ .seq-actor { flex:1; min-width:100px; text-align:center; padding:6px 4px; font-size:11px; font-weight:600; color:var(--text-dim); border-right:1px solid var(--border); cursor:pointer; transition:all 0.15s; position:relative; }
114
+ .seq-actor:last-child { border-right:none; }
115
+ .seq-actor:hover { background:var(--bg-raised); }
116
+ .seq-actor .actor-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:4px; vertical-align:middle; }
117
+ .seq-actor.filtered { background:var(--phosphor-faint); color:var(--phosphor); }
118
+ .seq-events { position:relative; }
119
+
120
+ /* Lifelines */
121
+ .seq-lifelines { position:absolute; inset:0; display:flex; pointer-events:none; z-index:0; }
122
+ .seq-lifeline { flex:1; min-width:100px; border-right:1px dashed var(--border); position:relative; }
123
+ .seq-lifeline:last-child { border-right:none; }
124
+
125
+ /* Event rows */
126
+ .seq-row { position:relative; min-height:32px; display:flex; align-items:center; border-bottom:1px solid rgba(30,43,30,0.3); z-index:1; cursor:pointer; transition:background 0.1s; }
127
+ .seq-row:hover { background:rgba(51,255,102,0.02); }
128
+ .seq-row.active { background:rgba(51,255,102,0.05); }
129
+ .seq-row.structural { background:rgba(255,68,85,0.03); }
130
+ .seq-row .row-content { position:absolute; left:0; right:0; display:flex; align-items:center; padding:0; height:100%; }
131
+
132
+ /* Arrow rendering */
133
+ .seq-arrow-container { position:absolute; top:50%; height:0; display:flex; align-items:center; }
134
+ .seq-arrow-line { height:2px; flex:1; position:relative; }
135
+ .seq-arrow-head { width:0; height:0; border-top:5px solid transparent; border-bottom:5px solid transparent; flex-shrink:0; }
136
+ .seq-arrow-head.right { border-left:8px solid; }
137
+ .seq-arrow-head.left { border-right:8px solid; }
138
+ .seq-arrow-label { position:absolute; top:-16px; font-size:10px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; padding:0 4px; }
139
+ .seq-arrow-label.above-right { left:4px; }
140
+ .seq-arrow-label.above-left { right:4px; text-align:right; }
141
+
142
+ /* Event type styling on arrows */
143
+ .seq-row.type-delegation .seq-arrow-line { height:3px; }
144
+ .seq-row.type-delegation_return .seq-arrow-line { height:1px; opacity:0.5; }
145
+ .seq-row.type-tool_call .seq-arrow-line { height:1px; opacity:0.6; }
146
+ .seq-row.type-tool_result .seq-arrow-line { height:1px; opacity:0.4; }
147
+
148
+ /* Structural event badge */
149
+ .seq-structural-badge { position:absolute; font-size:9px; padding:1px 6px; border-radius:2px; background:var(--red-dim); color:var(--red); font-weight:600; text-transform:uppercase; letter-spacing:0.5px; white-space:nowrap; top:50%; transform:translateY(-50%); }
150
+
151
+ /* Self-arrow (tool on same actor) */
152
+ .seq-self-arrow { position:absolute; width:30px; height:20px; border:1px solid; border-left:none; border-radius:0 6px 6px 0; top:50%; transform:translateY(-50%); }
153
+
154
+ /* Collapsed group */
155
+ .seq-group { position:relative; border-bottom:1px solid rgba(30,43,30,0.3); cursor:pointer; transition:background 0.1s; }
156
+ .seq-group:hover { background:rgba(51,255,102,0.02); }
157
+ .seq-group-header { display:flex; align-items:center; padding:4px 8px; min-height:28px; font-size:11px; color:var(--text-dim); gap:6px; }
158
+ .seq-group-header .chevron { font-size:9px; transition:transform 0.15s; width:10px; color:var(--text-dim); }
159
+ .seq-group-header.expanded .chevron { transform:rotate(90deg); }
160
+ .seq-group-header .group-label { opacity:0.7; }
161
+ .seq-group-body { display:none; }
162
+ .seq-group-body.expanded { display:block; }
163
+
164
+ /* ======== TIMELINE VIEW ======== */
165
+ #timeline-view { overflow:auto; background:var(--bg-deep); }
166
+ .tl-event { display:flex; align-items:flex-start; padding:6px 16px; gap:10px; border-bottom:1px solid rgba(30,43,30,0.3); cursor:pointer; transition:background 0.1s; min-height:32px; }
167
+ .tl-event:hover { background:rgba(51,255,102,0.02); }
168
+ .tl-event.active { background:rgba(51,255,102,0.05); }
169
+ .tl-event.structural { border-left:2px solid var(--red); }
170
+ .tl-idx { font-size:10px; color:var(--text-dim); width:28px; text-align:right; flex-shrink:0; padding-top:2px; opacity:0.5; }
171
+ .tl-time { font-size:10px; color:var(--text-dim); width:60px; flex-shrink:0; padding-top:2px; }
172
+ .tl-actor-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; margin-top:5px; }
173
+ .tl-body { flex:1; min-width:0; }
174
+ .tl-header-line { display:flex; align-items:center; gap:6px; font-size:12px; }
175
+ .tl-from { font-weight:600; }
176
+ .tl-arrow { color:var(--text-dim); font-size:10px; }
177
+ .tl-to { font-weight:500; }
178
+ .tl-type-badge { font-size:9px; padding:1px 5px; border-radius:2px; text-transform:uppercase; letter-spacing:0.3px; font-weight:600; margin-left:4px; }
179
+ .tl-type-badge.delegation { background:rgba(255,170,51,0.15); color:var(--amber); }
180
+ .tl-type-badge.structural { background:rgba(255,68,85,0.15); color:var(--red); }
181
+ .tl-type-badge.tool { background:rgba(102,119,102,0.15); color:var(--text-dim); }
182
+ .tl-preview { font-size:11px; color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:2px; max-width:600px; }
183
+ .tl-usage { font-size:10px; color:var(--text-dim); opacity:0.5; margin-top:1px; }
184
+ .tl-depth-indent { display:inline-block; }
185
+
186
+ /* ======== DETAIL VIEW ======== */
187
+ #detail-view { overflow:auto; background:var(--bg-deep); border-left:1px solid var(--border); }
188
+ .detail-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--text-dim); gap:6px; }
189
+ .detail-empty .icon { font-size:28px; opacity:0.3; }
190
+ .detail-header { position:sticky; top:0; z-index:5; background:var(--bg); border-bottom:1px solid var(--border); padding:10px 16px; display:flex; align-items:center; justify-content:space-between; }
191
+ .detail-header .po-name { font-weight:700; font-size:15px; color:var(--text-bright); }
192
+ .detail-header .type-badge { font-size:9px; text-transform:uppercase; letter-spacing:0.5px; padding:2px 6px; border-radius:2px; font-weight:600; margin-left:8px; }
193
+ .badge-root { background:var(--phosphor-faint); color:var(--phosphor); }
194
+ .badge-delegation { background:rgba(255,170,51,0.1); color:var(--amber); }
195
+ .detail-header .parent-info { font-size:10px; color:var(--text-dim); margin-left:8px; }
196
+ .detail-header .parent-info em { color:var(--amber); font-style:normal; }
197
+ .detail-header .stats { font-size:10px; color:var(--text-dim); display:flex; gap:12px; }
198
+ .detail-header .stats .v { color:var(--text); }
199
+
200
+ /* Breadcrumbs */
201
+ .breadcrumbs { padding:6px 16px; background:var(--bg); border-bottom:1px solid var(--border); font-size:11px; display:flex; align-items:center; gap:4px; }
202
+ .breadcrumb { color:var(--text-dim); cursor:pointer; transition:color 0.15s; }
203
+ .breadcrumb:hover { color:var(--phosphor); }
204
+ .breadcrumb.current { color:var(--text-bright); cursor:default; }
205
+ .breadcrumb-sep { color:var(--border-bright); }
206
+
207
+ /* Messages */
208
+ .messages { padding:12px 16px 60px; }
209
+ .msg { margin-bottom:2px; animation:fadeIn 0.12s ease; }
210
+ .msg-head { display:flex; align-items:center; gap:6px; padding-top:10px; margin-bottom:4px; }
211
+ .msg-sender { font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:0.3px; }
212
+ .msg-sender.human { color:var(--cyan); }
213
+ .msg-sender.assistant { color:var(--phosphor); }
214
+ .msg-sender.delegator { color:var(--amber); }
215
+ .msg-ts { font-size:10px; color:var(--text-dim); opacity:0.4; }
216
+ .msg-tokens { font-size:10px; color:var(--text-dim); margin-left:auto; }
217
+ .msg-body { padding:8px 12px; border-radius:3px; font-size:12px; line-height:1.65; white-space:pre-wrap; word-break:break-word; }
218
+ .msg.user .msg-body { background:rgba(51,221,255,0.04); border-left:2px solid var(--cyan-dim); }
219
+ .msg.user.from-po .msg-body { background:rgba(255,170,51,0.04); border-left:2px solid var(--amber-dim); }
220
+ .msg.assistant .msg-body { background:rgba(51,255,102,0.03); border-left:2px solid var(--phosphor-faint); }
221
+ .msg.highlighted .msg-body { box-shadow:inset 0 0 0 1px var(--phosphor-dim); }
222
+
223
+ /* Tool calls in detail */
224
+ .tc { margin:3px 0; border:1px solid var(--border); border-radius:3px; overflow:hidden; }
225
+ .tc-head { display:flex; align-items:center; gap:6px; padding:6px 10px; background:var(--bg-raised); cursor:pointer; font-size:11px; user-select:none; transition:background 0.1s; }
226
+ .tc-head:hover { background:var(--bg-surface); }
227
+ .tc-head .chv { color:var(--text-dim); font-size:9px; width:10px; transition:transform 0.15s; }
228
+ .tc-head.open .chv { transform:rotate(90deg); }
229
+ .tc-head .tc-name { font-weight:600; color:var(--phosphor-dim); }
230
+ .tc-head .tc-label { color:var(--text-dim); font-size:10px; }
231
+ .tc-body { display:none; border-top:1px solid var(--border); }
232
+ .tc-body.open { display:block; }
233
+ .tc-section { padding:8px 10px; }
234
+ .tc-section + .tc-section { border-top:1px solid var(--border); }
235
+ .tc-section-label { font-size:9px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-dim); margin-bottom:4px; }
236
+ .tc-section pre { font-family:var(--mono); font-size:11px; line-height:1.5; color:var(--text); white-space:pre-wrap; word-break:break-all; max-height:250px; overflow-y:auto; }
237
+ .tc-result { max-height:150px; overflow-y:auto; font-size:11px; color:var(--text-dim); white-space:pre-wrap; word-break:break-all; line-height:1.5; }
238
+
239
+ /* Delegation marker in detail */
240
+ .deleg-marker { display:flex; align-items:center; gap:6px; padding:6px 10px; margin:4px 0; border:1px dashed var(--amber-dim); border-radius:3px; background:rgba(255,170,51,0.03); font-size:11px; color:var(--amber); cursor:pointer; transition:all 0.15s; }
241
+ .deleg-marker:hover { background:rgba(255,170,51,0.06); border-color:var(--amber); }
242
+ .deleg-marker .dm-info { opacity:0.5; margin-left:auto; font-size:10px; }
243
+
244
+ /* ======== ANIMATIONS ======== */
245
+ @keyframes flicker { 0%,100%{opacity:1} 92%{opacity:1} 93%{opacity:0.8} 94%{opacity:1} 96%{opacity:0.9} 97%{opacity:1} }
246
+ @keyframes fadeIn { from{opacity:0;transform:translateY(3px)} to{opacity:1;transform:translateY(0)} }
247
+ </style>
248
+ </head>
249
+ <body>
250
+
251
+ <div id="drop-zone">
252
+ <div class="logo">Thread Explorer</div>
253
+ <div class="subtitle">PromptObjects</div>
254
+ <div class="drop-area" id="drop-area">
255
+ <div class="icon">{...}</div>
256
+ <p>Drop a thread export <span>JSON</span> here</p>
257
+ <p class="hint">or click to browse</p>
258
+ </div>
259
+ <input type="file" id="file-input" accept=".json,application/json">
260
+ <div class="sample-link" id="load-sample">load sample data</div>
261
+ </div>
262
+
263
+ <div id="app">
264
+ <div id="header">
265
+ <span class="brand-name">Thread Explorer</span>
266
+ <span class="brand-sep">/</span>
267
+ <span class="thread-name" id="h-name">---</span>
268
+ <div class="header-stats" id="h-stats"></div>
269
+ <button class="btn-open" id="btn-open">Open</button>
270
+ </div>
271
+ <div id="cost-bar">
272
+ <span class="label">Tokens</span>
273
+ <div class="bar" id="cost-bar-inner"></div>
274
+ <div class="bar-legend" id="cost-legend"></div>
275
+ </div>
276
+ <div id="main" class="split">
277
+ <div style="display:flex;flex-direction:column;overflow:hidden;">
278
+ <div id="toolbar">
279
+ <button class="tab active" data-view="sequence">Sequence</button>
280
+ <button class="tab" data-view="timeline">Timeline</button>
281
+ <div class="toolbar-sep"></div>
282
+ <input id="search" type="text" placeholder="Search messages...">
283
+ <div class="filter-btns" id="filter-btns"></div>
284
+ </div>
285
+ <div id="seq-view" style="flex:1;overflow:auto;"></div>
286
+ <div id="timeline-view" style="flex:1;overflow:auto;display:none;"></div>
287
+ </div>
288
+ <div id="detail-view">
289
+ <div class="detail-empty"><div class="icon">&larr;</div><div>Select an event</div></div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ <script>
295
+ // ========================================
296
+ // THREAD EXPLORER
297
+ // ========================================
298
+
299
+ // --- State ---
300
+ let treeData = null;
301
+ let events = []; // flattened event sequence
302
+ let actors = []; // ordered actor names
303
+ let poNames = new Set();
304
+ let actorColors = {};
305
+ let activeView = 'sequence';
306
+ let selectedEventIdx = null;
307
+ let selectedSessionId = null;
308
+ let searchQuery = '';
309
+ let activeFilters = new Set(); // PO name filters
310
+ let allNodes = {}; // id -> node for quick lookup
311
+
312
+ const COLORS = ['#33ff66','#ffaa33','#33ddff','#dd55ff','#ff6699','#33ffcc','#ffdd44','#88aaff','#ff8844','#99ff33'];
313
+ const STRUCTURAL_TOOLS = new Set(['create_capability','add_capability','request_capability','remove_capability','modify_prompt','create_po','delete_primitive','ask_human']);
314
+
315
+ // --- DOM refs ---
316
+ const dropZone = document.getElementById('drop-zone');
317
+ const dropArea = document.getElementById('drop-area');
318
+ const fileInput = document.getElementById('file-input');
319
+ const app = document.getElementById('app');
320
+
321
+ // --- File loading ---
322
+ dropArea.addEventListener('click', () => fileInput.click());
323
+ fileInput.addEventListener('change', e => { if(e.target.files[0]) loadFile(e.target.files[0]); });
324
+ dropArea.addEventListener('dragover', e => { e.preventDefault(); dropArea.classList.add('dragover'); });
325
+ dropArea.addEventListener('dragleave', () => dropArea.classList.remove('dragover'));
326
+ dropArea.addEventListener('drop', e => { e.preventDefault(); dropArea.classList.remove('dragover'); if(e.dataTransfer.files[0]) loadFile(e.dataTransfer.files[0]); });
327
+ document.getElementById('btn-open').addEventListener('click', () => { const i=document.createElement('input'); i.type='file'; i.accept='.json'; i.onchange=e=>{if(e.target.files[0])loadFile(e.target.files[0]);}; i.click(); });
328
+ document.getElementById('load-sample').addEventListener('click', () => loadData(generateSampleData()));
329
+
330
+ function loadFile(file) {
331
+ const r = new FileReader();
332
+ r.onload = e => { try { loadData(JSON.parse(e.target.result)); } catch(err) { alert('Parse error: '+err.message); } };
333
+ r.readAsText(file);
334
+ }
335
+
336
+ function loadData(data) {
337
+ treeData = data;
338
+ allNodes = {};
339
+ indexNodes(data);
340
+ poNames = extractPONames(data);
341
+ events = extractEvents(data);
342
+ actors = extractActors();
343
+ assignColors();
344
+ selectedEventIdx = null;
345
+ selectedSessionId = null;
346
+ activeFilters = new Set();
347
+ searchQuery = '';
348
+ document.getElementById('search').value = '';
349
+
350
+ dropZone.classList.add('hidden');
351
+ app.classList.add('visible');
352
+
353
+ renderAll();
354
+ // Auto-select first event
355
+ if (events.length > 0) selectEvent(0);
356
+ }
357
+
358
+ function indexNodes(node, parent) {
359
+ const id = node.session?.id || 'n-'+Object.keys(allNodes).length;
360
+ allNodes[id] = { ...node, _parent: parent || null };
361
+ (node.children||[]).forEach(c => indexNodes(c, node));
362
+ }
363
+
364
+ function extractPONames(node) {
365
+ const s = new Set();
366
+ s.add(node.session?.po_name);
367
+ (node.children||[]).forEach(c => extractPONames(c).forEach(n => s.add(n)));
368
+ return s;
369
+ }
370
+
371
+ function extractActors() {
372
+ const ordered = ['human'];
373
+ const seen = new Set(['human']);
374
+ events.forEach(e => {
375
+ [e.from, e.to].forEach(a => {
376
+ if (a && !seen.has(a) && (poNames.has(a) || a === 'human')) { seen.add(a); ordered.push(a); }
377
+ });
378
+ });
379
+ // Add "tools" pseudo-actor if any tool calls exist
380
+ if (events.some(e => e.type === 'tool_call' || e.type === 'tool_result')) {
381
+ ordered.push('[tools]');
382
+ }
383
+ return ordered;
384
+ }
385
+
386
+ function assignColors() {
387
+ actorColors = {};
388
+ let ci = 0;
389
+ actors.forEach(a => {
390
+ if (a === 'human') actorColors[a] = '#33ddff';
391
+ else if (a === '[tools]') actorColors[a] = '#667766';
392
+ else { actorColors[a] = COLORS[ci % COLORS.length]; ci++; }
393
+ });
394
+ }
395
+
396
+ // --- Event extraction ---
397
+ function extractEvents(node, depth) {
398
+ depth = depth || 0;
399
+ const evts = [];
400
+ const session = node.session || {};
401
+ const poName = session.po_name || 'unknown';
402
+ const messages = node.messages || [];
403
+
404
+ // Delegation children lookup
405
+ const delegKids = {};
406
+ (node.children||[]).forEach(c => {
407
+ const cp = c.session?.po_name;
408
+ if (c.session?.thread_type === 'delegation' && cp) {
409
+ if (!delegKids[cp]) delegKids[cp] = [];
410
+ delegKids[cp].push(c);
411
+ }
412
+ });
413
+ const delegCallIds = new Set();
414
+
415
+ for (const msg of messages) {
416
+ if (msg.role === 'user') {
417
+ evts.push({
418
+ type: 'message', from: msg.from_po || 'human', to: poName,
419
+ content: msg.content || '', summary: trunc(msg.content||'',80),
420
+ sessionId: session.id, depth, ts: msg.created_at, usage: null, isStructural: false
421
+ });
422
+ }
423
+ if (msg.role === 'assistant') {
424
+ if (msg.tool_calls) {
425
+ for (const tc of msg.tool_calls) {
426
+ const tcName = tc.name||'';
427
+ const tcId = tc.id||'';
428
+ const isDeleg = delegKids[tcName] && delegKids[tcName].length > 0;
429
+ if (isDeleg) {
430
+ delegCallIds.add(tcId);
431
+ evts.push({
432
+ type: 'delegation', from: poName, to: tcName,
433
+ content: JSON.stringify(tc.arguments||{}), summary: 'delegate \u2192 '+tcName,
434
+ sessionId: session.id, depth, ts: msg.created_at, usage: msg.usage, isStructural: false
435
+ });
436
+ const child = delegKids[tcName].shift();
437
+ evts.push(...extractEvents(child, depth+1));
438
+ evts.push({
439
+ type: 'delegation_return', from: tcName, to: poName,
440
+ content: '', summary: tcName+' \u2192 return',
441
+ sessionId: session.id, depth, ts: msg.created_at, usage: null, isStructural: false
442
+ });
443
+ } else {
444
+ const isSt = STRUCTURAL_TOOLS.has(tcName);
445
+ evts.push({
446
+ type: isSt ? 'structural' : 'tool_call', from: poName, to: isSt ? poName : '[tools]',
447
+ content: JSON.stringify(tc.arguments||{}), summary: tcName, toolName: tcName,
448
+ sessionId: session.id, depth, ts: msg.created_at, usage: msg.usage, isStructural: isSt,
449
+ structuralType: isSt ? tcName : null, toolCallId: tcId
450
+ });
451
+ }
452
+ }
453
+ }
454
+ if (msg.content) {
455
+ evts.push({
456
+ type: 'response', from: poName, to: session.parent_po || 'human',
457
+ content: msg.content, summary: trunc(msg.content,80),
458
+ sessionId: session.id, depth, ts: msg.created_at, usage: msg.usage, isStructural: false
459
+ });
460
+ }
461
+ }
462
+ if (msg.role === 'tool') {
463
+ for (const tr of (msg.tool_results||[])) {
464
+ if (delegCallIds.has(tr.tool_call_id)) continue;
465
+ evts.push({
466
+ type: 'tool_result', from: '[tools]', to: poName,
467
+ content: tr.content||'', summary: trunc(tr.content||'',60), toolName: tr.name,
468
+ sessionId: session.id, depth, ts: msg.created_at, usage: null, isStructural: false,
469
+ toolCallId: tr.tool_call_id
470
+ });
471
+ }
472
+ }
473
+ }
474
+ return evts;
475
+ }
476
+
477
+ // --- Rendering orchestration ---
478
+ function renderAll() {
479
+ renderHeader();
480
+ renderCostBar();
481
+ renderFilterBtns();
482
+ renderSequenceView();
483
+ renderTimelineView();
484
+ renderDetail();
485
+ }
486
+
487
+ function getFilteredEvents() {
488
+ let filtered = events;
489
+ if (activeFilters.size > 0) {
490
+ filtered = filtered.filter(e => activeFilters.has(e.from) || activeFilters.has(e.to));
491
+ }
492
+ if (searchQuery) {
493
+ const q = searchQuery.toLowerCase();
494
+ filtered = filtered.filter(e => (e.content||'').toLowerCase().includes(q) || (e.summary||'').toLowerCase().includes(q) || (e.toolName||'').toLowerCase().includes(q));
495
+ }
496
+ return filtered;
497
+ }
498
+
499
+ // --- Header ---
500
+ function renderHeader() {
501
+ const s = treeData.session||{};
502
+ document.getElementById('h-name').textContent = s.name || s.po_name || 'Untitled';
503
+ const usage = sumUsageTree(treeData);
504
+ const nEvents = events.length;
505
+ const nSessions = Object.keys(allNodes).length;
506
+ document.getElementById('h-stats').innerHTML =
507
+ `<span><span class="v">${nSessions}</span> sessions</span>`+
508
+ `<span><span class="v">${nEvents}</span> events</span>`+
509
+ `<span><span class="v">${fmtNum(usage.input)}</span> in</span>`+
510
+ `<span><span class="v">${fmtNum(usage.output)}</span> out</span>`;
511
+ }
512
+
513
+ // --- Cost bar ---
514
+ function renderCostBar() {
515
+ const perPo = {};
516
+ events.forEach(e => {
517
+ if (e.usage) {
518
+ const po = e.from;
519
+ if (!perPo[po]) perPo[po] = { input:0, output:0 };
520
+ perPo[po].input += e.usage.input_tokens||0;
521
+ perPo[po].output += e.usage.output_tokens||0;
522
+ }
523
+ });
524
+ const total = Object.values(perPo).reduce((s,v) => s+v.input+v.output, 0) || 1;
525
+ const bar = document.getElementById('cost-bar-inner');
526
+ const legend = document.getElementById('cost-legend');
527
+ bar.innerHTML = '';
528
+ legend.innerHTML = '';
529
+ Object.entries(perPo).forEach(([po, v]) => {
530
+ const pct = ((v.input+v.output)/total*100);
531
+ if (pct < 0.5) return;
532
+ const seg = document.createElement('div');
533
+ seg.className = 'bar-seg';
534
+ seg.style.width = pct+'%';
535
+ seg.style.background = actorColors[po] || '#667766';
536
+ seg.title = `${po}: ${fmtNum(v.input)} in, ${fmtNum(v.output)} out`;
537
+ seg.addEventListener('click', () => toggleFilter(po));
538
+ bar.appendChild(seg);
539
+ legend.innerHTML += `<span><span class="dot" style="background:${actorColors[po]||'#667'}"></span>${esc(po)}</span>`;
540
+ });
541
+ }
542
+
543
+ // --- Filter buttons ---
544
+ function renderFilterBtns() {
545
+ const container = document.getElementById('filter-btns');
546
+ container.innerHTML = '';
547
+ actors.filter(a => a !== '[tools]').forEach(a => {
548
+ const btn = document.createElement('button');
549
+ btn.className = 'filter-btn' + (activeFilters.has(a) ? ' active' : '');
550
+ btn.textContent = a;
551
+ btn.style.borderColor = activeFilters.has(a) ? (actorColors[a]||'') : '';
552
+ btn.style.color = activeFilters.has(a) ? (actorColors[a]||'') : '';
553
+ btn.addEventListener('click', () => toggleFilter(a));
554
+ container.appendChild(btn);
555
+ });
556
+ }
557
+
558
+ function toggleFilter(actor) {
559
+ if (activeFilters.has(actor)) activeFilters.delete(actor);
560
+ else activeFilters.add(actor);
561
+ renderFilterBtns();
562
+ renderSequenceView();
563
+ renderTimelineView();
564
+ }
565
+
566
+ // --- Search ---
567
+ document.getElementById('search').addEventListener('input', e => {
568
+ searchQuery = e.target.value;
569
+ renderSequenceView();
570
+ renderTimelineView();
571
+ });
572
+
573
+ // --- Tab switching ---
574
+ document.querySelectorAll('.tab').forEach(tab => {
575
+ tab.addEventListener('click', () => {
576
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
577
+ tab.classList.add('active');
578
+ activeView = tab.dataset.view;
579
+ document.getElementById('seq-view').style.display = activeView === 'sequence' ? 'block' : 'none';
580
+ document.getElementById('timeline-view').style.display = activeView === 'timeline' ? 'block' : 'none';
581
+ });
582
+ });
583
+
584
+ // ========================================
585
+ // SEQUENCE VIEW
586
+ // ========================================
587
+ function renderSequenceView() {
588
+ const container = document.getElementById('seq-view');
589
+ const filtered = getFilteredEvents();
590
+
591
+ // Actor header
592
+ let html = '<div class="seq-actors">';
593
+ actors.forEach(a => {
594
+ const col = actorColors[a]||'#667';
595
+ const cls = activeFilters.has(a) ? ' filtered' : '';
596
+ html += `<div class="seq-actor${cls}" onclick="toggleFilter('${esc(a)}')" style="color:${col}"><span class="actor-dot" style="background:${col}"></span>${esc(a)}</div>`;
597
+ });
598
+ html += '</div>';
599
+
600
+ // Events area
601
+ html += '<div class="seq-events">';
602
+ // Lifelines
603
+ html += '<div class="seq-lifelines">';
604
+ actors.forEach(() => { html += '<div class="seq-lifeline"></div>'; });
605
+ html += '</div>';
606
+
607
+ // Group consecutive tool_call/tool_result pairs by same PO
608
+ const groups = groupToolCalls(filtered);
609
+
610
+ groups.forEach(g => {
611
+ if (g.group) {
612
+ html += renderSeqGroup(g);
613
+ } else {
614
+ html += renderSeqRow(g.event, g.originalIdx);
615
+ }
616
+ });
617
+
618
+ html += '</div>';
619
+ container.innerHTML = html;
620
+ }
621
+
622
+ function groupToolCalls(evts) {
623
+ const result = [];
624
+ let i = 0;
625
+ while (i < evts.length) {
626
+ const e = evts[i];
627
+ if ((e.type === 'tool_call' || e.type === 'tool_result') && !e.isStructural) {
628
+ // Collect consecutive tool events from the same session
629
+ const groupEvents = [];
630
+ const sessionId = e.sessionId;
631
+ while (i < evts.length && (evts[i].type === 'tool_call' || evts[i].type === 'tool_result') && evts[i].sessionId === sessionId && !evts[i].isStructural) {
632
+ groupEvents.push({ event: evts[i], originalIdx: events.indexOf(evts[i]) });
633
+ i++;
634
+ }
635
+ if (groupEvents.length > 2) {
636
+ result.push({ group: true, events: groupEvents, sessionId });
637
+ } else {
638
+ groupEvents.forEach(ge => result.push(ge));
639
+ }
640
+ } else {
641
+ result.push({ event: e, originalIdx: events.indexOf(e) });
642
+ i++;
643
+ }
644
+ }
645
+ return result;
646
+ }
647
+
648
+ function renderSeqGroup(g) {
649
+ const toolNames = [...new Set(g.events.filter(e=>e.event.type==='tool_call').map(e=>e.event.toolName||e.event.summary))];
650
+ const count = g.events.filter(e=>e.event.type==='tool_call').length;
651
+ const firstEvt = g.events[0].event;
652
+ const fromIdx = actors.indexOf(firstEvt.from);
653
+ const col = actorColors[firstEvt.from]||'#667';
654
+
655
+ let html = `<div class="seq-group">`;
656
+ html += `<div class="seq-group-header" onclick="toggleSeqGroup(this)">`;
657
+ html += `<span class="chevron">&#9654;</span>`;
658
+ html += `<span style="color:${col}">${esc(firstEvt.from)}</span>`;
659
+ html += `<span class="group-label">${count} tool calls: ${esc(toolNames.slice(0,3).join(', '))}${toolNames.length>3?'...':''}</span>`;
660
+ html += `</div>`;
661
+ html += `<div class="seq-group-body">`;
662
+ g.events.forEach(ge => { html += renderSeqRow(ge.event, ge.originalIdx); });
663
+ html += `</div></div>`;
664
+ return html;
665
+ }
666
+
667
+ function renderSeqRow(evt, idx) {
668
+ const fromIdx = actors.indexOf(evt.from);
669
+ const toIdx = actors.indexOf(evt.to);
670
+ const isActive = idx === selectedEventIdx;
671
+
672
+ let cls = `seq-row type-${evt.type}${isActive?' active':''}${evt.isStructural?' structural':''}`;
673
+ let html = `<div class="${cls}" data-idx="${idx}" onclick="selectEvent(${idx})">`;
674
+ html += `<div class="row-content">`;
675
+
676
+ if (evt.isStructural) {
677
+ // Structural badge on actor's lifeline
678
+ const x = fromIdx >= 0 ? ((fromIdx + 0.5) / actors.length * 100) : 50;
679
+ const col = actorColors[evt.from]||'#667';
680
+ html += `<div class="seq-structural-badge" style="left:calc(${x}% + 12px);background:rgba(255,68,85,0.15);color:var(--red);">${esc(evt.structuralType||evt.summary)}</div>`;
681
+ } else if (fromIdx >= 0 && toIdx >= 0 && fromIdx !== toIdx) {
682
+ // Arrow between two different actors
683
+ const left = Math.min(fromIdx, toIdx);
684
+ const right = Math.max(fromIdx, toIdx);
685
+ const goesRight = toIdx > fromIdx;
686
+ const leftPct = ((left + 0.5) / actors.length * 100);
687
+ const rightPct = ((right + 0.5) / actors.length * 100);
688
+ const col = actorColors[evt.from]||'#667';
689
+ const labelText = evt.type === 'tool_result' ? (evt.toolName||'\u2190') : evt.summary;
690
+
691
+ html += `<div class="seq-arrow-container" style="left:${leftPct}%;width:${rightPct-leftPct}%;">`;
692
+ if (!goesRight) html += `<div class="seq-arrow-head left" style="border-right-color:${col}"></div>`;
693
+ html += `<div class="seq-arrow-line" style="background:${col}">`;
694
+ html += `<div class="seq-arrow-label ${goesRight?'above-right':'above-left'}" style="color:${col}">${esc(trunc(labelText,40))}</div>`;
695
+ html += `</div>`;
696
+ if (goesRight) html += `<div class="seq-arrow-head right" style="border-left-color:${col}"></div>`;
697
+ html += `</div>`;
698
+ } else if (fromIdx >= 0 && fromIdx === toIdx) {
699
+ // Self-arrow (rare)
700
+ const x = ((fromIdx + 0.5) / actors.length * 100);
701
+ const col = actorColors[evt.from]||'#667';
702
+ html += `<div class="seq-self-arrow" style="left:calc(${x}% + 4px);border-color:${col}"></div>`;
703
+ html += `<div class="seq-structural-badge" style="left:calc(${x}% + 40px);background:transparent;color:${col}">${esc(trunc(evt.summary,30))}</div>`;
704
+ }
705
+
706
+ html += `</div></div>`;
707
+ return html;
708
+ }
709
+
710
+ function toggleSeqGroup(header) {
711
+ header.classList.toggle('expanded');
712
+ header.nextElementSibling.classList.toggle('expanded');
713
+ }
714
+
715
+ // ========================================
716
+ // TIMELINE VIEW
717
+ // ========================================
718
+ function renderTimelineView() {
719
+ const container = document.getElementById('timeline-view');
720
+ const filtered = getFilteredEvents();
721
+
722
+ let html = '';
723
+ filtered.forEach((evt, fi) => {
724
+ const idx = events.indexOf(evt);
725
+ const isActive = idx === selectedEventIdx;
726
+ const col = actorColors[evt.from]||'#667';
727
+ const indent = evt.depth * 16;
728
+ const typeBadge = evt.type === 'delegation' || evt.type === 'delegation_return' ? '<span class="tl-type-badge delegation">deleg</span>'
729
+ : evt.isStructural ? '<span class="tl-type-badge structural">'+esc(evt.structuralType||'struct')+'</span>'
730
+ : (evt.type === 'tool_call' || evt.type === 'tool_result') ? '<span class="tl-type-badge tool">'+ esc(evt.toolName||'tool')+'</span>' : '';
731
+
732
+ html += `<div class="tl-event${isActive?' active':''}${evt.isStructural?' structural':''}" data-idx="${idx}" onclick="selectEvent(${idx})">`;
733
+ html += `<div class="tl-idx">${idx}</div>`;
734
+ html += `<div class="tl-time">${fmtTime(evt.ts)}</div>`;
735
+ html += `<div class="tl-actor-dot" style="background:${col};margin-left:${indent}px"></div>`;
736
+ html += `<div class="tl-body">`;
737
+ html += `<div class="tl-header-line">`;
738
+ html += `<span class="tl-from" style="color:${col}">${esc(evt.from)}</span>`;
739
+ html += `<span class="tl-arrow">\u2192</span>`;
740
+ html += `<span class="tl-to" style="color:${actorColors[evt.to]||'#667'}">${esc(evt.to)}</span>`;
741
+ html += typeBadge;
742
+ html += `</div>`;
743
+ if (evt.summary && evt.type !== 'tool_result') {
744
+ html += `<div class="tl-preview">${esc(evt.summary)}</div>`;
745
+ }
746
+ if (evt.usage) {
747
+ html += `<div class="tl-usage">${fmtNum(evt.usage.input_tokens||0)} in / ${fmtNum(evt.usage.output_tokens||0)} out</div>`;
748
+ }
749
+ html += `</div></div>`;
750
+ });
751
+
752
+ container.innerHTML = html || '<div class="detail-empty"><div>No events match</div></div>';
753
+ }
754
+
755
+ // ========================================
756
+ // DETAIL VIEW
757
+ // ========================================
758
+ function selectEvent(idx) {
759
+ selectedEventIdx = idx;
760
+ const evt = events[idx];
761
+ if (!evt) return;
762
+
763
+ // Determine which session to show
764
+ const sessionId = evt.sessionId;
765
+ selectedSessionId = sessionId;
766
+
767
+ // Highlight in sequence/timeline
768
+ document.querySelectorAll('.seq-row,.tl-event').forEach(el => {
769
+ el.classList.toggle('active', parseInt(el.dataset.idx) === idx);
770
+ });
771
+
772
+ renderDetail();
773
+
774
+ // Scroll the active row into view in the current view
775
+ const activeRow = document.querySelector(activeView === 'sequence' ? '.seq-row.active' : '.tl-event.active');
776
+ if (activeRow) activeRow.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
777
+ }
778
+
779
+ function renderDetail() {
780
+ const container = document.getElementById('detail-view');
781
+ if (!selectedSessionId) {
782
+ container.innerHTML = '<div class="detail-empty"><div class="icon">&larr;</div><div>Select an event</div></div>';
783
+ return;
784
+ }
785
+
786
+ const node = allNodes[selectedSessionId];
787
+ if (!node) { container.innerHTML = '<div class="detail-empty"><div>Session not found</div></div>'; return; }
788
+
789
+ const session = node.session||{};
790
+ const type = session.thread_type||'root';
791
+ const usage = sumUsageTree(node);
792
+ const messages = node.messages||[];
793
+
794
+ // Build breadcrumbs
795
+ const crumbs = buildBreadcrumbs(selectedSessionId);
796
+
797
+ let html = '';
798
+
799
+ // Breadcrumbs
800
+ if (crumbs.length > 1) {
801
+ html += '<div class="breadcrumbs">';
802
+ crumbs.forEach((c, i) => {
803
+ if (i > 0) html += '<span class="breadcrumb-sep">/</span>';
804
+ const isCurrent = c.id === selectedSessionId;
805
+ html += `<span class="breadcrumb${isCurrent?' current':''}" onclick="${isCurrent?'':`navigateToSession('${c.id}')`}">${esc(c.name)}</span>`;
806
+ });
807
+ html += '</div>';
808
+ }
809
+
810
+ // Header
811
+ html += `<div class="detail-header">`;
812
+ html += `<div style="display:flex;align-items:center">`;
813
+ html += `<span class="po-name">${esc(session.po_name||'?')}</span>`;
814
+ html += `<span class="type-badge badge-${type}">${type}</span>`;
815
+ if (session.parent_po) html += `<span class="parent-info">from <em>${esc(session.parent_po)}</em></span>`;
816
+ html += `</div>`;
817
+ html += `<div class="stats"><span><span class="v">${fmtNum(usage.input)}</span> in</span><span><span class="v">${fmtNum(usage.output)}</span> out</span></div>`;
818
+ html += `</div>`;
819
+
820
+ // Messages
821
+ html += '<div class="messages">';
822
+
823
+ // tool_call_id -> result lookup
824
+ const toolResults = {};
825
+ messages.forEach(m => {
826
+ if (m.role === 'tool' && m.tool_results) m.tool_results.forEach(tr => { toolResults[tr.tool_call_id] = tr; });
827
+ });
828
+
829
+ // delegation children lookup
830
+ const delegKids = {};
831
+ (node.children||[]).forEach(c => {
832
+ const cp = c.session?.po_name;
833
+ if (c.session?.thread_type === 'delegation' && cp) {
834
+ if (!delegKids[cp]) delegKids[cp] = [];
835
+ delegKids[cp].push(c);
836
+ }
837
+ });
838
+
839
+ for (const msg of messages) {
840
+ if (msg.role === 'tool') continue;
841
+
842
+ if (msg.role === 'user') {
843
+ const fromPo = msg.from_po && msg.from_po !== 'human';
844
+ html += `<div class="msg user${fromPo?' from-po':''}">`;
845
+ html += `<div class="msg-head"><span class="msg-sender ${fromPo?'delegator':'human'}">${esc(fromPo?msg.from_po:'human')}</span>`;
846
+ html += `<span class="msg-ts">${fmtTime(msg.created_at)}</span></div>`;
847
+ html += `<div class="msg-body">${esc(msg.content||'')}</div></div>`;
848
+ }
849
+
850
+ if (msg.role === 'assistant') {
851
+ if (msg.content) {
852
+ html += `<div class="msg assistant">`;
853
+ html += `<div class="msg-head"><span class="msg-sender assistant">${esc(session.po_name||'assistant')}</span>`;
854
+ html += `<span class="msg-ts">${fmtTime(msg.created_at)}</span>`;
855
+ if (msg.usage) html += `<span class="msg-tokens">${fmtNum(msg.usage.input_tokens||0)} in / ${fmtNum(msg.usage.output_tokens||0)} out</span>`;
856
+ html += `</div>`;
857
+ html += `<div class="msg-body">${esc(msg.content)}</div></div>`;
858
+ }
859
+
860
+ if (msg.tool_calls) {
861
+ for (const tc of msg.tool_calls) {
862
+ const tcName = tc.name||'';
863
+ const tcArgs = tc.arguments||{};
864
+ const tcId = tc.id||'';
865
+ const result = toolResults[tcId];
866
+ const isSt = STRUCTURAL_TOOLS.has(tcName);
867
+
868
+ html += `<div class="tc">`;
869
+ html += `<div class="tc-head" onclick="toggleTc(this)">`;
870
+ html += `<span class="chv">&#9654;</span>`;
871
+ html += `<span class="tc-name" ${isSt?'style="color:var(--red)"':''}>${esc(tcName)}</span>`;
872
+ html += `<span class="tc-label">${isSt?'structural':'tool call'}</span>`;
873
+ html += `</div>`;
874
+ html += `<div class="tc-body">`;
875
+ html += `<div class="tc-section"><div class="tc-section-label">Arguments</div><pre>${esc(JSON.stringify(tcArgs,null,2))}</pre></div>`;
876
+ if (result) html += `<div class="tc-section"><div class="tc-section-label">Result</div><div class="tc-result">${esc(result.content||'')}</div></div>`;
877
+ html += `</div></div>`;
878
+
879
+ // Delegation marker
880
+ if (delegKids[tcName] && delegKids[tcName].length > 0) {
881
+ const child = delegKids[tcName].shift();
882
+ if (child) {
883
+ const cid = child.session?.id;
884
+ const cMsgs = (child.messages||[]).filter(m=>m.role!=='tool').length;
885
+ html += `<div class="deleg-marker" onclick="navigateToSession('${cid}')">`;
886
+ html += `<span>\u2192</span> <strong>Delegated to ${esc(tcName)}</strong>`;
887
+ html += `<span class="dm-info">${cMsgs} messages</span>`;
888
+ html += `</div>`;
889
+ }
890
+ }
891
+ }
892
+ }
893
+ }
894
+ }
895
+
896
+ html += '</div>';
897
+ container.innerHTML = html;
898
+ container.scrollTop = 0;
899
+ }
900
+
901
+ function toggleTc(head) {
902
+ head.classList.toggle('open');
903
+ head.nextElementSibling.classList.toggle('open');
904
+ }
905
+
906
+ function navigateToSession(sessionId) {
907
+ selectedSessionId = sessionId;
908
+ // Find first event for this session
909
+ const idx = events.findIndex(e => e.sessionId === sessionId);
910
+ if (idx >= 0) selectedEventIdx = idx;
911
+ renderDetail();
912
+ // Highlight in views
913
+ document.querySelectorAll('.seq-row,.tl-event').forEach(el => {
914
+ el.classList.toggle('active', parseInt(el.dataset.idx) === selectedEventIdx);
915
+ });
916
+ }
917
+
918
+ function buildBreadcrumbs(sessionId) {
919
+ const crumbs = [];
920
+ let current = allNodes[sessionId];
921
+ while (current) {
922
+ const s = current.session||{};
923
+ crumbs.unshift({ id: s.id, name: s.po_name||'?' });
924
+ // Find parent
925
+ if (s.parent_session_id && allNodes[s.parent_session_id]) {
926
+ current = allNodes[s.parent_session_id];
927
+ } else if (current._parent) {
928
+ current = current._parent;
929
+ // Check if this is really the parent by matching session
930
+ const ps = current.session;
931
+ if (ps && ps.id && !crumbs.find(c=>c.id===ps.id)) {
932
+ continue;
933
+ } else break;
934
+ } else break;
935
+ }
936
+ return crumbs;
937
+ }
938
+
939
+ // --- Utility ---
940
+ function sumUsageTree(node) {
941
+ let input=0, output=0;
942
+ (node.messages||[]).forEach(m => { if(m.usage){ input+=m.usage.input_tokens||0; output+=m.usage.output_tokens||0; }});
943
+ (node.children||[]).forEach(c => { const u=sumUsageTree(c); input+=u.input; output+=u.output; });
944
+ return {input,output};
945
+ }
946
+ function fmtNum(n) { if(n>=1e6) return (n/1e6).toFixed(1)+'M'; if(n>=1e3) return (n/1e3).toFixed(1)+'K'; return ''+n; }
947
+ function fmtTime(iso) { if(!iso) return ''; try { return new Date(iso).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}); } catch{return '';} }
948
+ function trunc(s,n) { return s && s.length>n ? s.substring(0,n)+'...' : (s||''); }
949
+ function esc(s) { if(!s) return ''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
950
+
951
+ // ========================================
952
+ // SAMPLE DATA
953
+ // ========================================
954
+ function generateSampleData() {
955
+ const t = n => new Date(Date.now() - n*1000).toISOString();
956
+ return {
957
+ session: { id:"s1", po_name:"solver", name:"Analyze data pipeline", thread_type:"root", parent_po:null, created_at:t(600) },
958
+ messages: [
959
+ { role:"user", content:"Analyze the data pipeline in /src/pipeline and fix the ETL issue.", from_po:null, created_at:t(600) },
960
+ { role:"assistant", content:null,
961
+ tool_calls:[{id:"c1",name:"reader",arguments:{message:"Read and summarize all files in /src/pipeline/"}}],
962
+ usage:{input_tokens:1250,output_tokens:45,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(595) },
963
+ { role:"tool", tool_results:[{tool_call_id:"c1",name:"reader",content:"3 stages: extract (PostgreSQL), transform (cleaning), load (warehouse). Key issue: transform.py drops ~12% of rows with null timestamps."}], created_at:t(560) },
964
+ { role:"assistant", content:null,
965
+ tool_calls:[{id:"c2",name:"read_file",arguments:{path:"/src/pipeline/transform.py"}}],
966
+ usage:{input_tokens:1800,output_tokens:32,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(555) },
967
+ { role:"tool", tool_results:[{tool_call_id:"c2",name:"read_file",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df = df.dropna(subset=['created_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"}], created_at:t(553) },
968
+ { role:"assistant", content:null,
969
+ tool_calls:[{id:"c3",name:"write_file",arguments:{path:"/src/pipeline/transform.py",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df['created_at'] = df['created_at'].fillna(df['updated_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"}}],
970
+ usage:{input_tokens:2200,output_tokens:60,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(545) },
971
+ { role:"tool", tool_results:[{tool_call_id:"c3",name:"write_file",content:"File written successfully"}], created_at:t(543) },
972
+ { role:"assistant", content:null,
973
+ tool_calls:[{id:"c4",name:"validator",arguments:{message:"Run the test suite for /src/pipeline/ and verify the transform fix handles null timestamps correctly."}}],
974
+ usage:{input_tokens:2500,output_tokens:55,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(540) },
975
+ { role:"tool", tool_results:[{tool_call_id:"c4",name:"validator",content:"All 12 tests pass. Null timestamp handling verified: 0 rows dropped, fallback to updated_at working correctly."}], created_at:t(500) },
976
+ { role:"assistant", content:null,
977
+ tool_calls:[{id:"c5",name:"add_capability",arguments:{name:"solver",capability:"write_file"}}],
978
+ usage:{input_tokens:2800,output_tokens:30,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(495) },
979
+ { role:"tool", tool_results:[{tool_call_id:"c5",name:"add_capability",content:"Capability write_file added to solver"}], created_at:t(494) },
980
+ { role:"assistant", content:"Fixed the ETL pipeline issue.\n\n**Problem:** `transform.py` used `df.dropna(subset=['created_at'])` which silently dropped ~12% of records with null timestamps.\n\n**Fix:** Replaced with `df['created_at'].fillna(df['updated_at'])` to use the `updated_at` field as a fallback instead of discarding rows.\n\n**Verification:** All 12 tests pass. The validator confirmed 0 rows are now dropped and the fallback works correctly.\n\nI've also added `write_file` to my capabilities for future code fixes.",
981
+ usage:{input_tokens:3000,output_tokens:120,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(490) }
982
+ ],
983
+ children: [
984
+ {
985
+ session: { id:"s2", po_name:"reader", name:"Read pipeline files", thread_type:"delegation", parent_po:"solver", created_at:t(595) },
986
+ messages: [
987
+ { role:"user", content:"Read and summarize all files in /src/pipeline/", from_po:"solver", created_at:t(595) },
988
+ { role:"assistant", content:null,
989
+ tool_calls:[{id:"r1",name:"list_files",arguments:{path:"/src/pipeline/"}}],
990
+ usage:{input_tokens:420,output_tokens:18,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(593) },
991
+ { role:"tool", tool_results:[{tool_call_id:"r1",name:"list_files",content:"extract.py\ntransform.py\nload.py\nconfig.yml\n__init__.py"}], created_at:t(592) },
992
+ { role:"assistant", content:null,
993
+ tool_calls:[
994
+ {id:"r2",name:"read_file",arguments:{path:"/src/pipeline/extract.py"}},
995
+ {id:"r3",name:"read_file",arguments:{path:"/src/pipeline/transform.py"}},
996
+ {id:"r4",name:"read_file",arguments:{path:"/src/pipeline/load.py"}}
997
+ ],
998
+ usage:{input_tokens:680,output_tokens:42,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(590) },
999
+ { role:"tool", tool_results:[
1000
+ {tool_call_id:"r2",name:"read_file",content:"import psycopg2\n\ndef extract(config):\n conn = psycopg2.connect(**config['db'])\n cursor = conn.cursor()\n cursor.execute('SELECT * FROM events ORDER BY id')\n while True:\n rows = cursor.fetchmany(1000)\n if not rows: break\n yield rows"},
1001
+ {tool_call_id:"r3",name:"read_file",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df = df.dropna(subset=['created_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"},
1002
+ {tool_call_id:"r4",name:"read_file",content:"from sqlalchemy import create_engine\n\ndef load(df, config):\n engine = create_engine(config['warehouse_url'])\n df.to_sql('events_clean', engine, if_exists='append', index=False, method='multi', chunksize=500)"}
1003
+ ], created_at:t(585) },
1004
+ { role:"assistant",
1005
+ content:"3 stages: extract (PostgreSQL), transform (cleaning), load (warehouse). Key issue: transform.py drops ~12% of rows with null timestamps.",
1006
+ usage:{input_tokens:1400,output_tokens:95,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(565) }
1007
+ ],
1008
+ children: []
1009
+ },
1010
+ {
1011
+ session: { id:"s3", po_name:"validator", name:"Validate fix", thread_type:"delegation", parent_po:"solver", created_at:t(540) },
1012
+ messages: [
1013
+ { role:"user", content:"Run the test suite for /src/pipeline/ and verify the transform fix handles null timestamps correctly.", from_po:"solver", created_at:t(540) },
1014
+ { role:"assistant", content:null,
1015
+ tool_calls:[{id:"v1",name:"list_files",arguments:{path:"/src/pipeline/tests/"}}],
1016
+ usage:{input_tokens:350,output_tokens:15,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(538) },
1017
+ { role:"tool", tool_results:[{tool_call_id:"v1",name:"list_files",content:"test_extract.py\ntest_transform.py\ntest_load.py\nconftest.py"}], created_at:t(537) },
1018
+ { role:"assistant", content:null,
1019
+ tool_calls:[{id:"v2",name:"read_file",arguments:{path:"/src/pipeline/tests/test_transform.py"}}],
1020
+ usage:{input_tokens:500,output_tokens:20,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(535) },
1021
+ { role:"tool", tool_results:[{tool_call_id:"v2",name:"read_file",content:"import pytest\nimport pandas as pd\nfrom pipeline.transform import transform\n\ndef test_null_timestamps():\n df = pd.DataFrame({'created_at': [None, '2024-01-01'], 'updated_at': ['2024-02-01', '2024-01-01'], 'amount': [100, 200], 'status': ['ACTIVE', 'inactive']})\n result = transform(df)\n assert len(result) == 2\n assert result.iloc[0]['created_at'] == pd.Timestamp('2024-02-01')"}], created_at:t(533) },
1022
+ { role:"assistant", content:null,
1023
+ tool_calls:[
1024
+ {id:"v3",name:"read_file",arguments:{path:"/src/pipeline/transform.py"}},
1025
+ {id:"v4",name:"read_file",arguments:{path:"/src/pipeline/tests/conftest.py"}}
1026
+ ],
1027
+ usage:{input_tokens:900,output_tokens:28,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(520) },
1028
+ { role:"tool", tool_results:[
1029
+ {tool_call_id:"v3",name:"read_file",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df['created_at'] = df['created_at'].fillna(df['updated_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"},
1030
+ {tool_call_id:"v4",name:"read_file",content:"import pytest\n\n@pytest.fixture\ndef sample_data():\n return {'events': [{'id': 1, 'created_at': None, 'updated_at': '2024-01-15'}]}"}
1031
+ ], created_at:t(515) },
1032
+ { role:"assistant",
1033
+ content:"All 12 tests pass. Null timestamp handling verified: 0 rows dropped, fallback to updated_at working correctly.",
1034
+ usage:{input_tokens:1200,output_tokens:40,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(505) }
1035
+ ],
1036
+ children: []
1037
+ }
1038
+ ]
1039
+ };
1040
+ }
1041
+ </script>
1042
+ </body>
1043
+ </html>