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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/exe/prompt_objects +548 -1
- data/frontend/src/App.tsx +11 -3
- data/frontend/src/components/ContextMenu.tsx +67 -0
- data/frontend/src/components/MessageBus.tsx +4 -3
- data/frontend/src/components/ModelSelector.tsx +5 -1
- data/frontend/src/components/ThreadsSidebar.tsx +46 -2
- data/frontend/src/components/UsagePanel.tsx +105 -0
- data/frontend/src/hooks/useWebSocket.ts +53 -0
- data/frontend/src/store/index.ts +10 -0
- data/frontend/src/types/index.ts +4 -1
- data/lib/prompt_objects/cli.rb +1 -0
- data/lib/prompt_objects/connectors/mcp.rb +1 -0
- data/lib/prompt_objects/environment.rb +24 -1
- data/lib/prompt_objects/llm/anthropic_adapter.rb +15 -1
- data/lib/prompt_objects/llm/factory.rb +93 -6
- data/lib/prompt_objects/llm/gemini_adapter.rb +13 -1
- data/lib/prompt_objects/llm/openai_adapter.rb +21 -4
- data/lib/prompt_objects/llm/pricing.rb +49 -0
- data/lib/prompt_objects/llm/response.rb +3 -2
- data/lib/prompt_objects/mcp/server.rb +1 -0
- data/lib/prompt_objects/message_bus.rb +27 -8
- data/lib/prompt_objects/prompt_object.rb +6 -4
- data/lib/prompt_objects/server/api/routes.rb +186 -29
- data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +1 -0
- data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +77 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/lib/prompt_objects/server/websocket_handler.rb +93 -9
- data/lib/prompt_objects/server.rb +54 -0
- data/lib/prompt_objects/session/store.rb +399 -4
- data/lib/prompt_objects.rb +1 -0
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/manifest.yml +22 -0
- data/templates/arc-agi-1/objects/data_manager.md +42 -0
- data/templates/arc-agi-1/objects/observer.md +100 -0
- data/templates/arc-agi-1/objects/solver.md +118 -0
- data/templates/arc-agi-1/objects/verifier.md +79 -0
- data/templates/arc-agi-1/primitives/check_arc_data.rb +53 -0
- data/templates/arc-agi-1/primitives/find_objects.rb +72 -0
- data/templates/arc-agi-1/primitives/grid_diff.rb +70 -0
- data/templates/arc-agi-1/primitives/grid_info.rb +42 -0
- data/templates/arc-agi-1/primitives/grid_transform.rb +50 -0
- data/templates/arc-agi-1/primitives/load_arc_task.rb +68 -0
- data/templates/arc-agi-1/primitives/render_grid.rb +78 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +131 -0
- data/tools/thread-explorer.html +1043 -0
- metadata +21 -3
- data/lib/prompt_objects/server/public/assets/index-CeNJvqLG.js +0 -77
- 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">←</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">▶</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">←</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">▶</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>
|