context-lens 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +120 -25
  2. package/dist/cli-utils.d.ts +2 -1
  3. package/dist/cli-utils.d.ts.map +1 -1
  4. package/dist/cli-utils.js +40 -13
  5. package/dist/cli-utils.js.map +1 -1
  6. package/dist/cli.js +184 -68
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/conversation.d.ts +63 -0
  9. package/dist/core/conversation.d.ts.map +1 -0
  10. package/dist/core/conversation.js +305 -0
  11. package/dist/core/conversation.js.map +1 -0
  12. package/dist/core/health.d.ts +7 -0
  13. package/dist/core/health.d.ts.map +1 -0
  14. package/dist/core/health.js +311 -0
  15. package/dist/core/health.js.map +1 -0
  16. package/dist/core/models.d.ts +36 -0
  17. package/dist/core/models.d.ts.map +1 -0
  18. package/dist/core/models.js +111 -0
  19. package/dist/core/models.js.map +1 -0
  20. package/dist/core/parse.d.ts +17 -0
  21. package/dist/core/parse.d.ts.map +1 -0
  22. package/dist/core/parse.js +349 -0
  23. package/dist/core/parse.js.map +1 -0
  24. package/dist/core/routing.d.ts +47 -0
  25. package/dist/core/routing.d.ts.map +1 -0
  26. package/dist/core/routing.js +132 -0
  27. package/dist/core/routing.js.map +1 -0
  28. package/dist/core/security.d.ts +8 -0
  29. package/dist/core/security.d.ts.map +1 -0
  30. package/dist/core/security.js +222 -0
  31. package/dist/core/security.js.map +1 -0
  32. package/dist/core/source.d.ts +22 -0
  33. package/dist/core/source.d.ts.map +1 -0
  34. package/dist/core/source.js +56 -0
  35. package/dist/core/source.js.map +1 -0
  36. package/dist/core/tokens.d.ts +29 -0
  37. package/dist/core/tokens.d.ts.map +1 -0
  38. package/dist/core/tokens.js +163 -0
  39. package/dist/core/tokens.js.map +1 -0
  40. package/dist/core.d.ts +14 -22
  41. package/dist/core.d.ts.map +1 -1
  42. package/dist/core.js +14 -471
  43. package/dist/core.js.map +1 -1
  44. package/dist/http/headers.d.ts +25 -0
  45. package/dist/http/headers.d.ts.map +1 -0
  46. package/dist/http/headers.js +54 -0
  47. package/dist/http/headers.js.map +1 -0
  48. package/dist/lhar/composition.d.ts +12 -0
  49. package/dist/lhar/composition.d.ts.map +1 -0
  50. package/dist/lhar/composition.js +258 -0
  51. package/dist/lhar/composition.js.map +1 -0
  52. package/dist/lhar/export.d.ts +5 -0
  53. package/dist/lhar/export.d.ts.map +1 -0
  54. package/dist/lhar/export.js +59 -0
  55. package/dist/lhar/export.js.map +1 -0
  56. package/dist/lhar/record.d.ts +6 -0
  57. package/dist/lhar/record.d.ts.map +1 -0
  58. package/dist/lhar/record.js +216 -0
  59. package/dist/lhar/record.js.map +1 -0
  60. package/dist/lhar/response.d.ts +11 -0
  61. package/dist/lhar/response.d.ts.map +1 -0
  62. package/dist/lhar/response.js +132 -0
  63. package/dist/lhar/response.js.map +1 -0
  64. package/dist/lhar-types.generated.d.ts +24 -3
  65. package/dist/lhar-types.generated.d.ts.map +1 -1
  66. package/dist/lhar.d.ts +12 -19
  67. package/dist/lhar.d.ts.map +1 -1
  68. package/dist/lhar.js +16 -473
  69. package/dist/lhar.js.map +1 -1
  70. package/dist/server/api.d.ts +8 -0
  71. package/dist/server/api.d.ts.map +1 -0
  72. package/dist/server/api.js +292 -0
  73. package/dist/server/api.js.map +1 -0
  74. package/dist/server/config.d.ts +13 -0
  75. package/dist/server/config.d.ts.map +1 -0
  76. package/dist/server/config.js +36 -0
  77. package/dist/server/config.js.map +1 -0
  78. package/dist/server/projection.d.ts +9 -0
  79. package/dist/server/projection.d.ts.map +1 -0
  80. package/dist/server/projection.js +47 -0
  81. package/dist/server/projection.js.map +1 -0
  82. package/dist/server/proxy.d.ts +13 -0
  83. package/dist/server/proxy.d.ts.map +1 -0
  84. package/dist/server/proxy.js +218 -0
  85. package/dist/server/proxy.js.map +1 -0
  86. package/dist/server/static.d.ts +9 -0
  87. package/dist/server/static.d.ts.map +1 -0
  88. package/dist/server/static.js +78 -0
  89. package/dist/server/static.js.map +1 -0
  90. package/dist/server/store.d.ts +81 -0
  91. package/dist/server/store.d.ts.map +1 -0
  92. package/dist/server/store.js +632 -0
  93. package/dist/server/store.js.map +1 -0
  94. package/dist/server/webui.d.ts +5 -0
  95. package/dist/server/webui.d.ts.map +1 -0
  96. package/dist/server/webui.js +42 -0
  97. package/dist/server/webui.js.map +1 -0
  98. package/dist/server-utils.d.ts +2 -2
  99. package/dist/server-utils.d.ts.map +1 -1
  100. package/dist/server-utils.js +12 -21
  101. package/dist/server-utils.js.map +1 -1
  102. package/dist/server.js +31 -697
  103. package/dist/server.js.map +1 -1
  104. package/dist/types.d.ts +94 -10
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/version.generated.d.ts +2 -0
  107. package/dist/version.generated.d.ts.map +1 -0
  108. package/dist/version.generated.js +2 -0
  109. package/dist/version.generated.js.map +1 -0
  110. package/findings-screenshot.png +0 -0
  111. package/messages-screenshot.png +0 -0
  112. package/package.json +23 -10
  113. package/schema/lhar.schema.json +58 -4
  114. package/screenshot-overview.png +0 -0
  115. package/sessions-screenshot.png +0 -0
  116. package/timeline-screenshot.png +0 -0
  117. package/diff.png +0 -0
  118. package/overview-sidebar.png +0 -0
  119. package/public/index.html +0 -2804
package/public/index.html DELETED
@@ -1,2804 +0,0 @@
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>Context Lens</title>
7
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='14' fill='none' stroke='%2360a5fa' stroke-width='2'/><circle cx='16' cy='16' r='6' fill='%2360a5fa' opacity='.6'/></svg>">
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
11
- <style>
12
- :root {
13
- --font-mono: 'IBM Plex Mono', 'SF Mono', 'Fira Code', Consolas, monospace;
14
- --font-sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
15
- --bg-deep: #0a0d12;
16
- --bg-field: #0f1318;
17
- --bg-surface: #14191f;
18
- --bg-raised: #1c222b;
19
- --bg-hover: #222a35;
20
- --border-dim: #252d3a;
21
- --border-mid: #313b4a;
22
- --border-bright: #3e4b5e;
23
- --text-primary: #e8ecf1;
24
- --text-secondary: #9aa8b8;
25
- --text-dim: #8293a5;
26
- --text-muted: #5e7085;
27
- --accent-blue: #5b9cf5;
28
- --accent-green: #3dd68c;
29
- --accent-amber: #f0b429;
30
- --accent-red: #f06060;
31
- --accent-purple: #a78bfa;
32
- --accent-pink: #f472b6;
33
- --accent-cyan: #22d3ee;
34
- }
35
-
36
- * { margin: 0; padding: 0; box-sizing: border-box; }
37
- body {
38
- font-family: var(--font-sans);
39
- background: var(--bg-deep);
40
- color: var(--text-primary);
41
- font-size: 13px;
42
- overflow: hidden;
43
- }
44
-
45
- /* Grain texture overlay */
46
- body::after {
47
- content: '';
48
- position: fixed;
49
- inset: 0;
50
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.025'/%3E%3C/svg%3E");
51
- pointer-events: none;
52
- z-index: 9999;
53
- }
54
-
55
- .app { display: grid; grid-template-columns: 280px 1fr; grid-template-rows: 48px 1fr; height: 100vh; }
56
-
57
- /* Header */
58
- .header {
59
- grid-column: 1 / -1;
60
- background: var(--bg-surface);
61
- border-bottom: 1px solid var(--border-mid);
62
- display: flex; align-items: center; padding: 0 16px; gap: 14px;
63
- }
64
- .header h1 {
65
- font-family: var(--font-mono);
66
- font-size: 13px; font-weight: 600;
67
- letter-spacing: 0.04em;
68
- color: var(--text-primary);
69
- }
70
- .header h1 .lens-dot {
71
- display: inline-block;
72
- width: 7px; height: 7px;
73
- background: var(--accent-blue);
74
- border-radius: 50%;
75
- margin-right: 6px;
76
- box-shadow: 0 0 6px var(--accent-blue);
77
- animation: pulse-dot 3s ease-in-out infinite;
78
- }
79
- @keyframes pulse-dot {
80
- 0%, 100% { opacity: 0.7; box-shadow: 0 0 4px var(--accent-blue); }
81
- 50% { opacity: 1; box-shadow: 0 0 8px var(--accent-blue); }
82
- }
83
- .header-right { margin-left: auto; display: flex; gap: 14px; align-items: center; font-size: 12px; }
84
- .header select, .header button {
85
- font-family: var(--font-mono);
86
- background: var(--bg-raised);
87
- border: 1px solid var(--border-mid);
88
- color: var(--text-secondary);
89
- padding: 5px 12px;
90
- border-radius: 4px;
91
- font-size: 11px;
92
- cursor: pointer;
93
- transition: border-color 0.2s, color 0.2s, background 0.2s;
94
- }
95
- .header select:hover, .header button:hover {
96
- border-color: var(--accent-blue);
97
- color: var(--text-primary);
98
- background: var(--bg-hover);
99
- }
100
-
101
- /* Sidebar */
102
- .sidebar {
103
- background: var(--bg-field);
104
- border-right: 1px solid var(--border-dim);
105
- overflow-y: auto; display: flex; flex-direction: column; min-height: 0;
106
- scrollbar-width: thin;
107
- scrollbar-color: var(--border-mid) transparent;
108
- }
109
- .sidebar-section {
110
- padding: 12px 14px 6px;
111
- font-size: 10px; color: var(--text-dim);
112
- text-transform: uppercase; letter-spacing: 0.1em;
113
- font-weight: 600;
114
- border-bottom: 1px solid var(--border-dim);
115
- }
116
- .session-item {
117
- padding: 10px 14px;
118
- cursor: pointer;
119
- border-left: 2px solid transparent;
120
- transition: background 0.15s, border-color 0.15s;
121
- }
122
- .session-item:hover { background: var(--bg-surface); }
123
- .session-item.active {
124
- background: rgba(91, 156, 245, 0.06);
125
- border-left-color: var(--accent-blue);
126
- }
127
- .session-head { display: flex; align-items: center; gap: 8px; position: relative; }
128
- .session-delete {
129
- margin-left: auto;
130
- font-family: var(--font-mono);
131
- font-size: 12px; line-height: 1;
132
- color: var(--text-muted);
133
- cursor: pointer;
134
- opacity: 0;
135
- transition: opacity 0.15s, color 0.15s;
136
- padding: 2px 4px;
137
- }
138
- .session-item:hover .session-delete { opacity: 1; }
139
- .session-delete:hover { color: var(--accent-red); }
140
- .session-meta {
141
- display: flex; gap: 12px; margin-top: 5px; padding-left: 14px;
142
- font-family: var(--font-mono); font-size: 11px; color: var(--text-dim);
143
- }
144
- .badge {
145
- font-size: 10px; padding: 2px 6px; border-radius: 3px;
146
- font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em;
147
- }
148
- .badge-claude { background: rgba(251, 146, 60, 0.12); color: #fb923c; }
149
- .badge-codex { background: rgba(52, 211, 153, 0.12); color: #34d399; }
150
- .badge-aider { background: rgba(96, 165, 250, 0.12); color: #60a5fa; }
151
- .badge-kimi { background: rgba(167, 139, 250, 0.12); color: #a78bfa; }
152
- .badge-unknown { background: var(--bg-raised); color: var(--text-dim); }
153
- .dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
154
- .dot-live { background: var(--accent-blue); box-shadow: 0 0 6px var(--accent-blue); animation: pulse-dot 3s ease-in-out infinite; }
155
- .dot-done { background: var(--text-muted); }
156
- .turn-group {
157
- margin-left: 14px; border-left: 1px solid var(--border-dim);
158
- max-height: 0; overflow: hidden;
159
- transition: max-height 0.25s ease-out;
160
- }
161
- .turn-group.expanded {
162
- max-height: 280px;
163
- overflow-y: auto;
164
- scrollbar-width: thin;
165
- scrollbar-color: var(--border-mid) transparent;
166
- transition: max-height 0.3s ease-in;
167
- }
168
- .tg-item {
169
- padding: 7px 12px; cursor: pointer;
170
- display: flex; align-items: center; gap: 6px;
171
- font-size: 12px;
172
- transition: background 0.12s;
173
- min-height: 32px;
174
- }
175
- .tg-item:hover { background: var(--bg-surface); }
176
- .tg-item.active { background: rgba(91, 156, 245, 0.08); }
177
- .tg-num {
178
- font-family: var(--font-mono);
179
- color: var(--text-dim); width: 16px; text-align: right; font-size: 11px;
180
- }
181
- .tg-model {
182
- font-family: var(--font-mono);
183
- font-size: 9px; padding: 2px 5px; border-radius: 2px;
184
- background: var(--bg-raised);
185
- }
186
- .tg-desc { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
187
- .tg-tokens {
188
- font-family: var(--font-mono);
189
- margin-left: auto; color: var(--text-dim); font-size: 11px; white-space: nowrap;
190
- }
191
- .sub-dots {
192
- display: flex; gap: 5px; padding: 4px 12px 4px 40px;
193
- font-size: 11px; color: var(--text-muted); align-items: center;
194
- }
195
- .sub-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent-purple); opacity: 0.7; }
196
- .sidebar-footer {
197
- padding: 12px 14px; border-top: 1px solid var(--border-mid);
198
- margin-top: auto; font-size: 13px; color: var(--text-primary);
199
- background: var(--bg-surface);
200
- }
201
- .sidebar-footer div {
202
- display: flex; justify-content: space-between; padding: 5px 0;
203
- font-family: var(--font-mono); font-size: 12px;
204
- }
205
- .sidebar-footer div span:first-child { color: var(--text-secondary); font-weight: 500; }
206
- .sidebar-footer div span:last-child { color: var(--text-primary); font-weight: 600; }
207
-
208
- /* Main */
209
- .main { overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 14px;
210
- min-height: 0; scrollbar-width: thin; scrollbar-color: var(--border-mid) transparent;
211
- }
212
-
213
- /* Panels — three tiers of visual weight */
214
- .panel {
215
- background: var(--bg-surface);
216
- border: 1px solid var(--border-dim);
217
- border-radius: 6px;
218
- overflow: hidden;
219
- flex-shrink: 0;
220
- transition: border-color 0.2s;
221
- }
222
- .panel-hero { border-color: var(--border-mid); }
223
- .panel-header {
224
- padding: 10px 16px;
225
- font-size: 11px; font-weight: 600;
226
- color: var(--text-primary);
227
- text-transform: uppercase; letter-spacing: 0.04em;
228
- border-bottom: 1px solid var(--border-dim);
229
- display: flex; justify-content: space-between; align-items: center;
230
- }
231
- .panel-body { padding: 16px; }
232
-
233
- /* Stats row */
234
- .stats-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; flex-shrink: 0; }
235
- .stat-card {
236
- text-align: center; padding: 4px 4px;
237
- border-radius: 4px;
238
- position: relative;
239
- }
240
- .stat-card::before {
241
- content: '';
242
- position: absolute; inset: 0;
243
- border-radius: 4px;
244
- opacity: 0;
245
- transition: opacity 0.3s;
246
- }
247
- .stat-val {
248
- font-family: var(--font-mono);
249
- font-size: 17px; font-weight: 600;
250
- letter-spacing: -0.02em;
251
- }
252
- .stat-lbl {
253
- font-size: 10px; color: var(--text-secondary); margin-top: 3px;
254
- font-family: var(--font-mono);
255
- }
256
- .blue { color: var(--accent-blue); }
257
- .green { color: var(--accent-green); }
258
- .amber { color: var(--accent-amber); }
259
- .white { color: var(--text-primary); }
260
- .red { color: var(--accent-red); }
261
- /* Stat severity tints */
262
- .stat-tint-blue { background: rgba(91, 156, 245, 0.04); }
263
- .stat-tint-green { background: rgba(61, 214, 140, 0.04); }
264
- .stat-tint-amber { background: rgba(240, 180, 41, 0.06); }
265
- .stat-tint-red { background: rgba(240, 96, 96, 0.08); }
266
-
267
- /* Treemap */
268
- .treemap { display: flex; height: 64px; border-radius: 4px; overflow: hidden; gap: 2px; }
269
- .treemap-block {
270
- display: flex; align-items: center; justify-content: center;
271
- font-family: var(--font-mono);
272
- font-size: 10px; font-weight: 500;
273
- color: rgba(255,255,255,0.92);
274
- flex-direction: column; gap: 1px;
275
- cursor: pointer;
276
- transition: filter 0.2s, flex 0.5s ease;
277
- position: relative;
278
- }
279
- .treemap-block:hover { filter: brightness(1.2); }
280
- .treemap-block span { font-size: 8px; opacity: 0.7; }
281
- /* Higher-saturation treemap colors */
282
- .tm-system_prompt { background: #2563eb; }
283
- .tm-tool_definitions { background: #db2777; }
284
- .tm-tool_results { background: #059669; }
285
- .tm-system_injections { background: #6366f1; }
286
- .tm-thinking { background: #8b5cf6; }
287
- .tm-assistant_text { background: #d97706; }
288
- .tm-user_text { background: #10b981; }
289
- .tm-tool_calls { background: #ec4899; }
290
- .tm-images { background: #4b5563; }
291
- .tm-cache_markers { background: #6b7280; }
292
- .tm-other { background: #4b5563; }
293
- .treemap-legend { display: flex; gap: 14px; margin-top: 10px; flex-wrap: wrap; }
294
- .treemap-legend-item {
295
- display: flex; align-items: center; gap: 5px;
296
- font-size: 11px; color: var(--text-secondary);
297
- }
298
- .treemap-legend-dot { width: 10px; height: 10px; border-radius: 2px; }
299
-
300
- /* Agent breakdown */
301
- .agent-row {
302
- display: flex; align-items: center; gap: 8px; padding: 5px 0;
303
- font-size: 12px;
304
- }
305
- .agent-bar-wrap { flex: 1; height: 6px; background: var(--bg-deep); border-radius: 3px; overflow: hidden; }
306
- .agent-bar { height: 100%; border-radius: 3px; transition: width 0.4s ease; }
307
- .agent-lbl { width: 140px; color: var(--text-secondary); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
308
- .agent-val { font-family: var(--font-mono); width: 120px; text-align: right; color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
309
-
310
- /* Timeline */
311
- .timeline-wrap {
312
- overflow-x: auto;
313
- scrollbar-width: thin;
314
- scrollbar-color: var(--border-mid) transparent;
315
- }
316
- .tl-container { display: flex; gap: 0; }
317
- .tl-y-axis {
318
- display: flex; flex-direction: column; justify-content: space-between;
319
- height: 160px; flex-shrink: 0; padding-right: 6px;
320
- font-family: var(--font-mono); font-size: 9px; color: var(--text-muted);
321
- text-align: right; min-width: 36px;
322
- }
323
- .tl-y-axis span { line-height: 1; }
324
- .timeline-chart { display: flex; align-items: flex-end; gap: 2px; height: 160px; min-width: min-content; }
325
- .tl-bar {
326
- flex: 0 0 auto; width: 12px;
327
- border-radius: 2px 2px 0 0;
328
- cursor: pointer;
329
- transition: filter 0.15s, height 0.3s ease;
330
- }
331
- /* Wider bars when few entries */
332
- .timeline-chart.tl-sparse .tl-bar { flex: 1; width: auto; min-width: 8px; max-width: 28px; }
333
- .tl-bar:hover { filter: brightness(1.2); }
334
- .tl-bar.active { box-shadow: 0 0 0 1.5px var(--accent-blue), 0 0 8px rgba(91, 156, 245, 0.3); }
335
- .tl-legend { display: flex; gap: 16px; margin-top: 10px; }
336
- .tl-legend-item {
337
- display: flex; align-items: center; gap: 5px;
338
- font-size: 11px; color: var(--text-secondary);
339
- }
340
- .tl-legend-dot { width: 8px; height: 8px; border-radius: 2px; }
341
- .tl-labels { display: flex; gap: 2px; min-width: min-content; }
342
- .tl-label {
343
- flex: 0 0 auto; width: 12px;
344
- text-align: center; font-size: 9px; color: var(--text-muted);
345
- font-family: var(--font-mono);
346
- padding-top: 3px;
347
- }
348
- .timeline-chart.tl-sparse + .tl-labels .tl-label { flex: 1; width: auto; min-width: 8px; max-width: 28px; }
349
-
350
- /* Toggle */
351
- .toggle-group { display: flex; }
352
- .toggle-btn {
353
- font-size: 11px; padding: 4px 10px;
354
- background: var(--bg-raised);
355
- border: 1px solid var(--border-dim);
356
- color: var(--text-dim);
357
- cursor: pointer;
358
- transition: background 0.15s, color 0.15s, border-color 0.15s;
359
- letter-spacing: 0.02em;
360
- }
361
- .toggle-btn:first-child { border-radius: 4px 0 0 4px; }
362
- .toggle-btn:last-child { border-radius: 0 4px 4px 0; }
363
- .toggle-btn:hover { color: var(--text-secondary); background: var(--bg-hover); }
364
- .toggle-btn.active {
365
- background: rgba(91, 156, 245, 0.12);
366
- border-color: rgba(91, 156, 245, 0.4);
367
- color: var(--accent-blue);
368
- }
369
-
370
- /* Empty state */
371
- .empty-main {
372
- display: flex; align-items: center; justify-content: center;
373
- height: 100%; color: var(--text-dim);
374
- flex-direction: column; gap: 10px;
375
- }
376
- .empty-main .empty-icon {
377
- width: 48px; height: 48px;
378
- border: 2px solid var(--border-mid);
379
- border-radius: 50%;
380
- display: flex; align-items: center; justify-content: center;
381
- margin-bottom: 4px;
382
- animation: pulse-ring 4s ease-in-out infinite;
383
- }
384
- @keyframes pulse-ring {
385
- 0%, 100% { border-color: var(--border-mid); }
386
- 50% { border-color: var(--border-bright); }
387
- }
388
-
389
- /* Working dir */
390
- .working-dir {
391
- font-family: var(--font-mono);
392
- font-size: 10px; color: var(--text-dim);
393
- margin-top: 3px; padding-left: 14px;
394
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
395
- }
396
-
397
- /* Model color helpers */
398
- .model-opus { color: #fb923c; }
399
- .model-sonnet { color: #60a5fa; }
400
- .model-haiku { color: #a78bfa; }
401
- .model-gpt { color: #10b981; }
402
- .model-default { color: var(--text-dim); }
403
-
404
- /* Turn detail */
405
- .turn-detail-header {
406
- display: flex; gap: 8px; align-items: center;
407
- margin-bottom: 12px; font-size: 11px; color: var(--text-secondary);
408
- }
409
- .call-row {
410
- display: flex; align-items: center; gap: 8px;
411
- padding: 6px 8px; border-radius: 4px;
412
- font-size: 12px; margin-bottom: 2px;
413
- transition: background 0.12s;
414
- }
415
- .call-row:hover { background: var(--bg-hover); }
416
- .call-row.main-call { background: rgba(91, 156, 245, 0.06); }
417
- .call-model {
418
- font-family: var(--font-mono);
419
- font-size: 9px; padding: 2px 5px; border-radius: 2px;
420
- width: 48px; text-align: center; background: var(--bg-raised);
421
- }
422
- .call-desc { color: var(--text-secondary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
423
- .call-desc.main { color: var(--text-primary); }
424
- .call-tok { font-family: var(--font-mono); color: var(--text-dim); font-size: 11px; width: 70px; text-align: right; }
425
- .call-cost { font-family: var(--font-mono); color: var(--accent-green); font-size: 11px; width: 50px; text-align: right; }
426
-
427
- /* Message categories */
428
- .msg-section { margin-top: 14px; border-top: 1px solid var(--border-dim); padding-top: 14px; }
429
- .msg-section-title {
430
- font-size: 11px; color: var(--text-secondary); margin-bottom: 10px;
431
- display: flex; justify-content: space-between;
432
- font-weight: 600;
433
- }
434
- .msg-category { margin-bottom: 6px; }
435
- .msg-cat-header {
436
- display: flex; align-items: center; gap: 6px;
437
- padding: 5px 0; cursor: pointer; font-size: 12px;
438
- transition: color 0.12s;
439
- }
440
- .msg-cat-header:hover { color: var(--text-primary); }
441
- .msg-cat-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
442
- .msg-cat-name { font-weight: 500; }
443
- .msg-cat-stats { font-family: var(--font-mono); margin-left: auto; color: var(--text-dim); font-size: 11px; }
444
- .msg-cat-bar-wrap { width: 60px; height: 4px; background: var(--bg-deep); border-radius: 2px; overflow: hidden; margin-left: 8px; }
445
- .msg-cat-bar { height: 100%; border-radius: 2px; transition: width 0.3s ease; }
446
- .msg-items {
447
- padding-left: 20px; border-left: 1px solid var(--border-dim);
448
- margin-left: 3px;
449
- max-height: 0; overflow: hidden;
450
- transition: max-height 0.25s ease-out;
451
- }
452
- .msg-items.expanded { max-height: 5000px; transition: max-height 0.4s ease-in; }
453
- .msg-line {
454
- display: flex; align-items: center; gap: 6px; padding: 4px 0;
455
- font-size: 11px; color: var(--text-secondary);
456
- border-bottom: 1px solid rgba(26, 34, 51, 0.5);
457
- transition: color 0.12s;
458
- }
459
- .msg-line-icon {
460
- font-family: var(--font-mono);
461
- color: var(--text-muted); width: 14px; font-size: 10px;
462
- }
463
- .msg-line-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
464
- .msg-line-tok { font-family: var(--font-mono); color: var(--text-muted); font-size: 10px; white-space: nowrap; }
465
- .msg-line-tok.large { color: var(--accent-amber); }
466
-
467
- /* Context diff */
468
- .diff-section { margin-top: 14px; border-top: 1px solid var(--border-dim); padding-top: 14px; }
469
- .diff-header {
470
- font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;
471
- display: flex; justify-content: space-between;
472
- }
473
- .diff-line {
474
- font-family: var(--font-mono);
475
- font-size: 11px; padding: 3px 8px; border-radius: 2px; margin-bottom: 1px;
476
- }
477
- .diff-add { background: rgba(16, 185, 129, 0.08); color: #4ade80; }
478
- .diff-remove { background: rgba(240, 96, 96, 0.08); color: #f87171; }
479
- .diff-same { color: var(--text-muted); }
480
-
481
- /* Recommendations */
482
- .rec-item {
483
- display: flex; align-items: flex-start; gap: 10px;
484
- padding: 10px 0; font-size: 12px;
485
- border-bottom: 1px solid var(--border-dim);
486
- transition: background 0.12s;
487
- }
488
- .rec-item:last-child { border-bottom: none; }
489
- .rec-severity {
490
- width: 8px; height: 8px; border-radius: 50%;
491
- margin-top: 4px; flex-shrink: 0;
492
- box-shadow: 0 0 4px currentColor;
493
- }
494
- .rec-severity.high { background: var(--accent-red); color: var(--accent-red); }
495
- .rec-severity.med { background: var(--accent-amber); color: var(--accent-amber); }
496
- .rec-severity.low { background: var(--accent-green); color: var(--accent-green); }
497
- .rec-body { flex: 1; }
498
- .rec-title { font-weight: 500; }
499
- .rec-detail { font-size: 11px; color: var(--text-dim); margin-top: 3px; line-height: 1.4; }
500
- .rec-impact {
501
- font-family: var(--font-mono);
502
- font-size: 11px; padding: 2px 8px; border-radius: 3px; white-space: nowrap;
503
- }
504
- .rec-impact.high { background: rgba(240, 96, 96, 0.1); color: var(--accent-red); }
505
- .rec-impact.med { background: rgba(240, 180, 41, 0.1); color: var(--accent-amber); }
506
- .rec-impact.low { background: rgba(61, 214, 140, 0.1); color: var(--accent-green); }
507
-
508
- /* Polling indicator */
509
- .poll-indicator {
510
- width: 6px; height: 6px; border-radius: 50%;
511
- background: var(--accent-green);
512
- opacity: 0;
513
- transition: opacity 0.15s;
514
- }
515
- .poll-indicator.active { opacity: 1; }
516
-
517
- /* Treemap click flash */
518
- @keyframes flash-highlight { 0% { background: rgba(91,156,245,0.2); } 100% { background: transparent; } }
519
- .msg-category.flash-highlight { animation: flash-highlight 0.8s ease-out; }
520
-
521
- /* Input tools line below call rows */
522
- .call-tools { font-family: var(--font-mono); font-size: 10px; color: var(--text-dim);
523
- padding: 0 8px 4px 62px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
524
-
525
- /* Clickable msg-lines */
526
- .msg-line { cursor: pointer; }
527
- .msg-line:hover { background: var(--bg-hover); }
528
- .msg-line.selected { background: rgba(91, 156, 245, 0.1); color: var(--text-primary); }
529
-
530
- /* Detail panel — slide-in from right */
531
- .detail-overlay {
532
- position: fixed; inset: 0;
533
- z-index: 1000;
534
- display: none;
535
- pointer-events: none;
536
- }
537
- .detail-overlay.open { display: flex; pointer-events: auto; }
538
-
539
- .detail-backdrop {
540
- flex: 1; min-width: 0;
541
- background: rgba(0, 0, 0, 0.3);
542
- cursor: pointer;
543
- }
544
-
545
- .detail-panel {
546
- width: 520px; min-width: 320px; max-width: 85vw;
547
- height: 100vh;
548
- background: var(--bg-field);
549
- border-left: 1px solid var(--border-mid);
550
- display: flex; flex-direction: column;
551
- transform: translateX(100%);
552
- transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
553
- box-shadow: -8px 0 30px rgba(0, 0, 0, 0.4);
554
- position: relative;
555
- }
556
- .detail-overlay.open .detail-panel {
557
- transform: translateX(0);
558
- }
559
-
560
- /* Resize handle */
561
- .detail-resize {
562
- position: absolute; left: -3px; top: 0; bottom: 0;
563
- width: 6px; cursor: col-resize; z-index: 10;
564
- background: transparent;
565
- transition: background 0.15s;
566
- }
567
- .detail-resize:hover,
568
- .detail-resize.dragging {
569
- background: var(--accent-blue);
570
- opacity: 0.5;
571
- }
572
-
573
- /* Panel header */
574
- .detail-head {
575
- padding: 12px 16px;
576
- border-bottom: 1px solid var(--border-dim);
577
- flex-shrink: 0;
578
- }
579
- .detail-head-row {
580
- display: flex; align-items: center; gap: 10px;
581
- }
582
- .detail-close {
583
- font-family: var(--font-mono);
584
- font-size: 16px; line-height: 1;
585
- color: var(--text-muted);
586
- cursor: pointer;
587
- padding: 2px 6px;
588
- border-radius: 3px;
589
- transition: color 0.15s, background 0.15s;
590
- flex-shrink: 0;
591
- }
592
- .detail-close:hover { color: var(--text-primary); background: var(--bg-hover); }
593
- .detail-title {
594
- font-family: var(--font-mono);
595
- font-size: 12px; font-weight: 600;
596
- color: var(--text-primary);
597
- flex: 1;
598
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
599
- }
600
- .detail-subtitle {
601
- font-family: var(--font-mono);
602
- font-size: 11px; color: var(--text-dim);
603
- margin-top: 6px;
604
- display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
605
- }
606
- .detail-subtitle span { white-space: nowrap; }
607
-
608
- /* Copy button */
609
- .detail-copy {
610
- font-family: var(--font-mono);
611
- font-size: 10px;
612
- color: var(--text-muted);
613
- background: var(--bg-raised);
614
- border: 1px solid var(--border-dim);
615
- padding: 3px 10px;
616
- border-radius: 3px;
617
- cursor: pointer;
618
- transition: color 0.15s, border-color 0.15s, background 0.15s;
619
- flex-shrink: 0;
620
- }
621
- .detail-copy:hover { color: var(--accent-blue); border-color: var(--accent-blue); background: var(--bg-hover); }
622
- .detail-copy.copied { color: var(--accent-green); border-color: var(--accent-green); }
623
-
624
- /* Nav buttons */
625
- .detail-nav {
626
- display: flex; gap: 4px; flex-shrink: 0;
627
- }
628
- .detail-nav-btn {
629
- font-family: var(--font-mono);
630
- font-size: 11px;
631
- color: var(--text-muted);
632
- background: var(--bg-raised);
633
- border: 1px solid var(--border-dim);
634
- padding: 3px 8px;
635
- border-radius: 3px;
636
- cursor: pointer;
637
- transition: color 0.15s, border-color 0.15s;
638
- }
639
- .detail-nav-btn:hover:not(:disabled) { color: var(--accent-blue); border-color: var(--accent-blue); }
640
- .detail-nav-btn:disabled { opacity: 0.3; cursor: default; }
641
-
642
- /* Tabs */
643
- .detail-tabs {
644
- display: flex;
645
- border-bottom: 1px solid var(--border-dim);
646
- flex-shrink: 0;
647
- }
648
- .detail-tab {
649
- font-family: var(--font-mono);
650
- font-size: 11px;
651
- color: var(--text-muted);
652
- padding: 8px 16px;
653
- cursor: pointer;
654
- border-bottom: 2px solid transparent;
655
- transition: color 0.15s, border-color 0.15s;
656
- }
657
- .detail-tab:hover { color: var(--text-secondary); }
658
- .detail-tab.active {
659
- color: var(--accent-blue);
660
- border-bottom-color: var(--accent-blue);
661
- }
662
-
663
- /* Content area */
664
- .detail-content {
665
- flex: 1; overflow-y: auto;
666
- padding: 16px;
667
- scrollbar-width: thin;
668
- scrollbar-color: var(--border-mid) transparent;
669
- }
670
-
671
- /* Metadata section */
672
- .detail-meta {
673
- border-top: 1px solid var(--border-dim);
674
- padding: 12px 16px;
675
- flex-shrink: 0;
676
- display: grid; grid-template-columns: 1fr 1fr;
677
- gap: 4px 16px;
678
- font-family: var(--font-mono);
679
- font-size: 10px;
680
- }
681
- .detail-meta-label { color: var(--text-dim); }
682
- .detail-meta-val { color: var(--text-secondary); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
683
-
684
- /* Syntax highlighting — rendered content */
685
- .detail-pre {
686
- font-family: var(--font-mono);
687
- font-size: 11px;
688
- line-height: 1.55;
689
- white-space: pre-wrap;
690
- word-break: break-word;
691
- color: var(--text-secondary);
692
- margin: 0;
693
- }
694
- /* JSON syntax */
695
- .syn-key { color: #7dd3fc; }
696
- .syn-str { color: #86efac; }
697
- .syn-num { color: #fbbf24; }
698
- .syn-bool { color: #c084fc; }
699
- .syn-null { color: #94a3b8; font-style: italic; }
700
- .syn-brace { color: #64748b; }
701
- /* Diff syntax */
702
- .syn-diff-add { color: #4ade80; background: rgba(16, 185, 129, 0.06); display: inline-block; width: 100%; }
703
- .syn-diff-del { color: #f87171; background: rgba(240, 96, 96, 0.06); display: inline-block; width: 100%; }
704
- .syn-diff-hunk { color: #818cf8; font-weight: 600; }
705
- .syn-diff-file { color: #fbbf24; }
706
- /* Bash/command output */
707
- .syn-prompt { color: #22d3ee; }
708
- .syn-comment { color: #64748b; font-style: italic; }
709
- /* Content block labels */
710
- .detail-block-label {
711
- display: inline-block;
712
- font-family: var(--font-mono);
713
- font-size: 9px;
714
- font-weight: 600;
715
- text-transform: uppercase;
716
- letter-spacing: 0.06em;
717
- padding: 2px 7px;
718
- border-radius: 3px;
719
- margin-bottom: 8px;
720
- }
721
- .detail-block-label.tool-use { background: rgba(244, 114, 182, 0.12); color: #f472b6; }
722
- .detail-block-label.tool-result { background: rgba(16, 185, 129, 0.12); color: #10b981; }
723
- .detail-block-label.thinking { background: rgba(167, 139, 250, 0.12); color: #a78bfa; }
724
- .detail-block-label.text { background: rgba(251, 191, 36, 0.12); color: #fbbf24; }
725
- .detail-block-label.image { background: rgba(75, 85, 99, 0.2); color: #9ca3af; }
726
- .detail-block-sep {
727
- border: none;
728
- border-top: 1px solid var(--border-dim);
729
- margin: 14px 0;
730
- }
731
- /* Truncation */
732
- .detail-truncated {
733
- font-family: var(--font-mono);
734
- font-size: 11px;
735
- color: var(--accent-blue);
736
- cursor: pointer;
737
- padding: 6px 0;
738
- transition: color 0.15s;
739
- }
740
- .detail-truncated:hover { color: var(--text-primary); }
741
-
742
- /* Minimal markdown rendering */
743
- .md-rendered { font-family: var(--font-sans); font-size: 12px; line-height: 1.6; color: var(--text-secondary); }
744
- .md-rendered h1, .md-rendered h2, .md-rendered h3 { color: var(--text-primary); font-weight: 600; margin: 10px 0 4px; }
745
- .md-rendered h1 { font-size: 15px; }
746
- .md-rendered h2 { font-size: 13px; }
747
- .md-rendered h3 { font-size: 12px; }
748
- .md-rendered code { font-family: var(--font-mono); font-size: 11px; background: var(--bg-raised); padding: 1px 5px; border-radius: 3px; color: var(--accent-cyan); }
749
- .md-rendered pre { font-family: var(--font-mono); font-size: 11px; background: var(--bg-deep); padding: 8px 10px; border-radius: 4px; overflow-x: auto; margin: 6px 0; line-height: 1.5; color: var(--text-secondary); white-space: pre-wrap; word-break: break-word; }
750
- .md-rendered pre code { background: none; padding: 0; color: inherit; }
751
- .md-rendered ul { padding-left: 18px; margin: 4px 0; }
752
- .md-rendered li { margin: 2px 0; }
753
- .md-rendered b { color: var(--text-primary); }
754
- .md-rendered p { margin: 4px 0; }
755
-
756
- /* Tool use compact params */
757
- .tool-params { margin: 0; padding: 0; list-style: none; }
758
- .tool-params li { display: flex; gap: 8px; padding: 3px 0; border-bottom: 1px solid var(--border-dim); font-size: 11px; }
759
- .tool-params li:last-child { border-bottom: none; }
760
- .tool-param-key { font-family: var(--font-mono); color: #7dd3fc; min-width: 80px; flex-shrink: 0; }
761
- .tool-param-val { font-family: var(--font-mono); color: var(--text-secondary); word-break: break-all; white-space: pre-wrap; }
762
- </style>
763
- </head>
764
- <body>
765
- <div class="app">
766
- <div class="header">
767
- <h1><span class="lens-dot"></span>Context Lens</h1>
768
- <div class="header-right">
769
- <span class="poll-indicator" id="poll-indicator"></span>
770
- <span id="hdr-cost" style="font-family:var(--font-mono);color:var(--accent-green);font-weight:600;font-size:12px;"></span>
771
- <span id="hdr-reqs" style="font-family:var(--font-mono);color:var(--text-dim);font-size:11px;"></span>
772
- <select id="hdr-source-filter"><option value="">All sources</option></select>
773
- <button id="export-btn">Export LHAR</button>
774
- <button id="reset-btn">Reset</button>
775
- </div>
776
- </div>
777
- <div class="sidebar" id="sidebar"></div>
778
- <div class="main" id="main-panel">
779
- <div class="empty-main" id="empty-state">
780
- <div class="empty-icon"><span class="lens-dot" style="margin:0;"></span></div>
781
- <div style="font-family:var(--font-mono);font-size:13px;color:var(--text-dim);">No requests captured</div>
782
- <div style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);">Point API calls to port 4040</div>
783
- </div>
784
- </div>
785
- </div>
786
-
787
- <!-- Detail Panel -->
788
- <div class="detail-overlay" id="detail-overlay">
789
- <div class="detail-backdrop" id="detail-backdrop"></div>
790
- <div class="detail-panel" id="detail-panel">
791
- <div class="detail-resize" id="detail-resize"></div>
792
- <div class="detail-head">
793
- <div class="detail-head-row">
794
- <span class="detail-close" id="detail-close">&times;</span>
795
- <span class="detail-title" id="detail-title"></span>
796
- <div class="detail-nav">
797
- <button class="detail-nav-btn" id="detail-prev" title="Previous message">&uarr;</button>
798
- <button class="detail-nav-btn" id="detail-next" title="Next message">&darr;</button>
799
- </div>
800
- <button class="detail-copy" id="detail-copy">Copy</button>
801
- </div>
802
- <div class="detail-subtitle" id="detail-subtitle"></div>
803
- </div>
804
- <div class="detail-tabs" id="detail-tabs">
805
- <div class="detail-tab active" data-dtab="rendered">Rendered</div>
806
- <div class="detail-tab" data-dtab="raw">Raw JSON</div>
807
- </div>
808
- <div class="detail-content" id="detail-content"></div>
809
- <div class="detail-meta" id="detail-meta"></div>
810
- </div>
811
- </div>
812
-
813
- <script>
814
- (function() {
815
- 'use strict';
816
-
817
- // --- State ---
818
- let currentData = null;
819
- let currentRevision = -1;
820
- let selectedSessionId = null;
821
- let selectedTurnIndex = -1; // -1 = latest
822
- let expandedSessions = new Set();
823
- let treemapMode = 'detailed';
824
- let timelineMode = 'all';
825
- let turnDetailMode = 'calls'; // 'calls' or 'diff'
826
- let expandedCategories = new Set();
827
- let expandedAgentGroups = new Set();
828
- let turnCallsExpanded = false;
829
- let sourceFilter = '';
830
-
831
- // Preserve scroll positions across re-renders
832
- function saveScroll(id) { var el = document.getElementById(id); return el ? el.scrollTop : 0; }
833
- function restoreScroll(id, pos) { var el = document.getElementById(id); if (el) el.scrollTop = pos; }
834
-
835
- // --- Helpers ---
836
- function esc(s) { return s ? String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : ''; }
837
- function fmtK(n) { return n >= 1000 ? (n / 1000).toFixed(1) + 'K' : String(n); }
838
- function fmtCost(c) { return c != null ? '$' + c.toFixed(c < 0.01 ? 4 : 2) : '--'; }
839
- function fmtTime(iso) { return new Date(iso).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); }
840
- function computeYTicks(max, count) {
841
- if (max <= 0) return [0];
842
- // Find nice step: 1, 2, 5, 10, 20, 50, 100, ...
843
- var rough = max / count;
844
- var mag = Math.pow(10, Math.floor(Math.log10(rough)));
845
- var residual = rough / mag;
846
- var nice = residual <= 1.5 ? 1 : residual <= 3.5 ? 2 : 5;
847
- var step = nice * mag;
848
- var ticks = [];
849
- for (var v = 0; v <= max + step * 0.01; v += step) {
850
- ticks.push(Math.round(v));
851
- }
852
- return ticks;
853
- }
854
-
855
- function modelClass(m) {
856
- if (!m) return 'model-default';
857
- if (m.includes('opus')) return 'model-opus';
858
- if (m.includes('sonnet')) return 'model-sonnet';
859
- if (m.includes('haiku')) return 'model-haiku';
860
- if (m.includes('gpt') || m.includes('o1') || m.includes('o3') || m.includes('o4')) return 'model-gpt';
861
- return 'model-default';
862
- }
863
-
864
- function modelBarColor(m) {
865
- if (!m) return '#888';
866
- if (m.includes('opus')) return '#fb923c';
867
- if (m.includes('sonnet')) return '#60a5fa';
868
- if (m.includes('haiku')) return '#a78bfa';
869
- if (m.includes('gpt') || m.includes('o1') || m.includes('o3') || m.includes('o4')) return '#10b981';
870
- return '#888';
871
- }
872
-
873
- function modelBarGradient(m, isMain) {
874
- if (isMain === false) {
875
- // Sub-agent: desaturated, lower-contrast gradient
876
- if (!m) return 'linear-gradient(to top, #222, #444)';
877
- if (m.includes('opus')) return 'linear-gradient(to top, #3d2510, #9a6a3a)';
878
- if (m.includes('sonnet')) return 'linear-gradient(to top, #152a42, #4070a0)';
879
- if (m.includes('haiku')) return 'linear-gradient(to top, #2e1660, #7060a0)';
880
- if (m.includes('gpt') || m.includes('o1') || m.includes('o3') || m.includes('o4')) return 'linear-gradient(to top, #0a3025, #1a7050)';
881
- return 'linear-gradient(to top, #222, #444)';
882
- }
883
- if (!m) return 'linear-gradient(to top, #333, #666)';
884
- if (m.includes('opus')) return 'linear-gradient(to top, #78350f, #fb923c)';
885
- if (m.includes('sonnet')) return 'linear-gradient(to top, #1e3a5f, #60a5fa)';
886
- if (m.includes('haiku')) return 'linear-gradient(to top, #4c1d95, #a78bfa)';
887
- if (m.includes('gpt') || m.includes('o1') || m.includes('o3') || m.includes('o4')) return 'linear-gradient(to top, #064e3b, #10b981)';
888
- return 'linear-gradient(to top, #333, #666)';
889
- }
890
-
891
- function sourceBadgeClass(src) {
892
- if (src === 'claude') return 'badge-claude';
893
- if (src === 'codex') return 'badge-codex';
894
- if (src === 'aider') return 'badge-aider';
895
- if (src === 'kimi') return 'badge-kimi';
896
- return 'badge-unknown';
897
- }
898
-
899
- function shortModel(m) {
900
- if (!m) return '?';
901
- if (m.includes('opus')) return 'opus';
902
- if (m.includes('sonnet')) return 'sonnet';
903
- if (m.includes('haiku')) return 'haiku';
904
- if (m.includes('gpt-4o-mini')) return '4o-mini';
905
- if (m.includes('gpt-4o')) return '4o';
906
- if (m.includes('gpt-4')) return 'gpt4';
907
- if (m.includes('o4-mini')) return 'o4m';
908
- if (m.includes('o3-mini')) return 'o3m';
909
- if (m.includes('o3')) return 'o3';
910
- if (m.includes('o1')) return 'o1';
911
- return m.length > 10 ? m.slice(0, 8) : m;
912
- }
913
-
914
- // Category display names and colors — more saturated for the treemap
915
- var CATEGORY_META = {
916
- system_prompt: { label: 'System prompt', color: '#2563eb' },
917
- tool_definitions: { label: 'Tool definitions', color: '#db2777' },
918
- tool_results: { label: 'Tool results', color: '#059669' },
919
- tool_calls: { label: 'Tool calls', color: '#ec4899' },
920
- system_injections: { label: 'System injections', color: '#6366f1' },
921
- thinking: { label: 'Thinking', color: '#8b5cf6' },
922
- assistant_text: { label: 'Assistant', color: '#d97706' },
923
- user_text: { label: 'User', color: '#10b981' },
924
- images: { label: 'Images', color: '#4b5563' },
925
- cache_markers: { label: 'Cache markers', color: '#6b7280' },
926
- other: { label: 'Other', color: '#4b5563' },
927
- };
928
-
929
- // Simple mode groups: system, tools, messages
930
- var SIMPLE_GROUPS = {
931
- system: ['system_prompt', 'system_injections'],
932
- tools: ['tool_definitions', 'tool_calls'],
933
- messages: ['tool_results', 'thinking', 'assistant_text', 'user_text', 'images', 'cache_markers', 'other'],
934
- };
935
- var SIMPLE_META = {
936
- system: { label: 'System', color: '#2563eb' },
937
- tools: { label: 'Tools', color: '#db2777' },
938
- messages: { label: 'Messages', color: '#10b981' },
939
- };
940
-
941
- // --- Data helpers ---
942
- function getConversations() {
943
- if (!currentData) return [];
944
- var convos = currentData.conversations || [];
945
- if (sourceFilter) convos = convos.filter(function(c) { return c.source === sourceFilter; });
946
- return convos;
947
- }
948
-
949
- function getSelectedSession() {
950
- if (!selectedSessionId) return null;
951
- var convos = getConversations();
952
- return convos.find(function(c) { return c.id === selectedSessionId; }) || null;
953
- }
954
-
955
- function getSessionEntries(session) {
956
- if (!session) return [];
957
- return session.entries.slice().reverse();
958
- }
959
-
960
- function getSelectedEntry(session) {
961
- if (!session) return null;
962
- var entries = getSessionEntries(session);
963
- if (entries.length === 0) return null;
964
- var idx = selectedTurnIndex >= 0 ? selectedTurnIndex : entries.length - 1;
965
- return entries[Math.min(idx, entries.length - 1)];
966
- }
967
-
968
- function sessionTotalCost(session) {
969
- return (session.entries || []).reduce(function(s, e) { return s + (e.costUsd || 0); }, 0);
970
- }
971
-
972
- function sessionTotalTokens(session) {
973
- return (session.entries || []).reduce(function(s, e) { return s + (e.contextInfo.totalTokens || 0); }, 0);
974
- }
975
-
976
- function allSources() {
977
- if (!currentData) return [];
978
- var set = {};
979
- (currentData.conversations || []).forEach(function(c) { if (c.source) set[c.source] = true; });
980
- return Object.keys(set).sort();
981
- }
982
-
983
- function classifyEntries(entries) {
984
- var agentTotals = {};
985
- entries.forEach(function(e) {
986
- var ak = e.agentKey || '_main';
987
- agentTotals[ak] = (agentTotals[ak] || 0) + e.contextInfo.totalTokens;
988
- });
989
- var mainAgent = null;
990
- var maxTok = 0;
991
- for (var ak in agentTotals) {
992
- if (agentTotals[ak] > maxTok) { maxTok = agentTotals[ak]; mainAgent = ak; }
993
- }
994
- return entries.map(function(e) {
995
- var ak = e.agentKey || '_main';
996
- return { entry: e, isMain: ak === mainAgent };
997
- });
998
- }
999
-
1000
- // --- Render: Header ---
1001
- function renderHeader() {
1002
- var totalCost = 0;
1003
- var totalReqs = 0;
1004
- (currentData ? currentData.conversations || [] : []).forEach(function(c) {
1005
- totalCost += sessionTotalCost(c);
1006
- totalReqs += (c.entries || []).length;
1007
- });
1008
- var ungrouped = currentData ? currentData.ungrouped || [] : [];
1009
- totalReqs += ungrouped.length;
1010
- ungrouped.forEach(function(e) { totalCost += e.costUsd || 0; });
1011
-
1012
- document.getElementById('hdr-cost').textContent = fmtCost(totalCost);
1013
- document.getElementById('hdr-reqs').textContent = totalReqs + ' req';
1014
-
1015
- var sel = document.getElementById('hdr-source-filter');
1016
- var sources = allSources();
1017
- var oldVal = sel.value;
1018
- sel.innerHTML = '<option value="">All sources</option>';
1019
- sources.forEach(function(s) {
1020
- sel.innerHTML += '<option value="' + esc(s) + '"' + (s === sourceFilter ? ' selected' : '') + '>' + esc(s) + '</option>';
1021
- });
1022
- if (oldVal && !sources.includes(oldVal)) sel.value = '';
1023
- }
1024
-
1025
- // --- Render: Sidebar ---
1026
- function renderSidebar() {
1027
- var scrollPos = saveScroll('sidebar');
1028
- var convos = getConversations();
1029
- var sb = document.getElementById('sidebar');
1030
-
1031
- if (convos.length === 0) {
1032
- sb.innerHTML = '<div class="sidebar-section">Sessions</div>'
1033
- + '<div style="padding:20px 14px;color:var(--text-muted);font-family:var(--font-mono);font-size:11px;">Waiting for data...</div>';
1034
- return;
1035
- }
1036
-
1037
- var html = '<div class="sidebar-section">Sessions</div>';
1038
- var mainCalls = 0, subCalls = 0, totalCost = 0;
1039
-
1040
- convos.forEach(function(c) {
1041
- var isActive = c.id === selectedSessionId;
1042
- var entries = getSessionEntries(c);
1043
- var classified = classifyEntries(entries);
1044
- var mainCount = classified.filter(function(x) { return x.isMain; }).length;
1045
- var subCount = classified.filter(function(x) { return !x.isMain; }).length;
1046
- mainCalls += mainCount;
1047
- subCalls += subCount;
1048
- var cost = sessionTotalCost(c);
1049
- totalCost += cost;
1050
- var latestEntry = entries[entries.length - 1];
1051
- var model = latestEntry ? latestEntry.contextInfo.model : 'unknown';
1052
- var isExpanded = isActive && expandedSessions.has(c.id);
1053
-
1054
- html += '<div class="session-item' + (isActive ? ' active' : '') + '" data-session="' + esc(c.id) + '">';
1055
- html += '<div class="session-head">';
1056
- html += '<span class="dot ' + (isActive ? 'dot-live' : 'dot-done') + '"></span>';
1057
- html += '<span class="badge ' + sourceBadgeClass(c.source) + '">' + esc(c.source || '?') + '</span>';
1058
- html += '<span style="font-family:var(--font-mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary);">' + esc(shortModel(model)) + '</span>';
1059
- html += '<span class="session-delete" data-delete="' + esc(c.id) + '">&times;</span>';
1060
- html += '</div>';
1061
- var latestTime = latestEntry && latestEntry.timestamp ? fmtTime(latestEntry.timestamp) : '';
1062
- html += '<div class="session-meta"><span>' + entries.length + ' turns</span><span>' + fmtK(sessionTotalTokens(c)) + ' tok</span><span style="color:var(--accent-green);">' + fmtCost(cost) + '</span>' + (latestTime ? '<span>' + esc(latestTime) + '</span>' : '') + '</div>';
1063
- if (c.workingDirectory) html += '<div class="working-dir">' + esc(c.workingDirectory) + '</div>';
1064
- html += '</div>';
1065
-
1066
- html += '<div class="turn-group' + (isExpanded ? ' expanded' : '') + '" data-turns="' + esc(c.id) + '">';
1067
-
1068
- var mainEntries = classified.filter(function(x) { return x.isMain; });
1069
-
1070
- // Build turn data array
1071
- var turnData = [];
1072
- var turnNum = 0;
1073
- classified.forEach(function(item, idx) {
1074
- if (!item.isMain) return;
1075
- turnNum++;
1076
- var e = item.entry;
1077
- var nextMainIdx = classified.length;
1078
- for (var ni = idx + 1; ni < classified.length; ni++) {
1079
- if (classified[ni].isMain) { nextMainIdx = ni; break; }
1080
- }
1081
- var subsAfter = [];
1082
- for (var k = idx + 1; k < classified.length; k++) {
1083
- if (classified[k].isMain) break;
1084
- subsAfter.push(classified[k]);
1085
- }
1086
- turnData.push({
1087
- item: item, idx: idx, turnNum: turnNum, nextMainIdx: nextMainIdx,
1088
- subsAfter: subsAfter, e: e
1089
- });
1090
- });
1091
-
1092
- // Reverse to show latest first
1093
- turnData.reverse();
1094
-
1095
- // Render turns in reverse order (latest on top)
1096
- turnData.forEach(function(td) {
1097
- var turnActive = isActive && (selectedTurnIndex === -1
1098
- ? td.turnNum === mainEntries.length
1099
- : (selectedTurnIndex >= td.idx && selectedTurnIndex < td.nextMainIdx));
1100
- var desc = esc(extractCallSummary(td.e) || td.e.agentLabel || '').slice(0, 40) || 'Turn ' + td.turnNum;
1101
-
1102
- html += '<div class="tg-item' + (turnActive ? ' active' : '') + '" data-session="' + esc(c.id) + '" data-turn="' + td.idx + '">';
1103
- html += '<span class="tg-num">' + td.turnNum + '</span>';
1104
- html += '<span class="tg-model ' + modelClass(td.e.contextInfo.model) + '">' + esc(shortModel(td.e.contextInfo.model)) + '</span>';
1105
- html += '<span class="tg-desc">' + desc + '</span>';
1106
- html += '<span class="tg-tokens">' + fmtK(td.e.contextInfo.totalTokens) + '</span>';
1107
- html += '</div>';
1108
-
1109
- if (td.subsAfter.length > 0) {
1110
- html += '<div class="sub-dots">';
1111
- td.subsAfter.forEach(function() { html += '<span class="sub-dot"></span>'; });
1112
- var subModel = shortModel(td.subsAfter[0].entry.contextInfo.model);
1113
- html += ' ' + esc(subModel) + ' x' + td.subsAfter.length;
1114
- html += '</div>';
1115
- }
1116
- });
1117
-
1118
- html += '</div>';
1119
- });
1120
-
1121
- html += '<div class="sidebar-footer">';
1122
- html += '<div><span>Main calls</span><span style="color:#fb923c;">' + mainCalls + '</span></div>';
1123
- html += '<div><span>Sub calls</span><span style="color:var(--accent-purple);">' + subCalls + '</span></div>';
1124
- html += '<div><span>Total cost</span><span style="color:var(--accent-green);">' + fmtCost(totalCost) + '</span></div>';
1125
- html += '</div>';
1126
-
1127
- sb.innerHTML = html;
1128
-
1129
- sb.querySelectorAll('.session-item').forEach(function(el) {
1130
- el.addEventListener('click', function() {
1131
- var sid = el.getAttribute('data-session');
1132
- selectedSessionId = sid;
1133
- selectedTurnIndex = -1; // Always select latest turn
1134
- // Toggle expansion
1135
- if (expandedSessions.has(sid)) {
1136
- expandedSessions.delete(sid);
1137
- } else {
1138
- expandedSessions.add(sid);
1139
- }
1140
- renderSidebar();
1141
- renderMain();
1142
- });
1143
- });
1144
-
1145
- sb.querySelectorAll('.tg-item').forEach(function(el) {
1146
- el.addEventListener('click', function(ev) {
1147
- ev.stopPropagation();
1148
- var sid = el.getAttribute('data-session');
1149
- var turn = parseInt(el.getAttribute('data-turn'), 10);
1150
- selectedSessionId = sid;
1151
- selectedTurnIndex = turn;
1152
- renderSidebar();
1153
- renderMain();
1154
- });
1155
- });
1156
-
1157
- sb.querySelectorAll('.session-delete').forEach(function(el) {
1158
- el.addEventListener('click', function(ev) {
1159
- ev.stopPropagation();
1160
- var sid = el.getAttribute('data-delete');
1161
- fetch('/api/conversations/' + encodeURIComponent(sid), { method: 'DELETE' }).then(function() {
1162
- if (selectedSessionId === sid) {
1163
- selectedSessionId = null;
1164
- selectedTurnIndex = -1;
1165
- }
1166
- expandedSessions.delete(sid);
1167
- fetchAndRender();
1168
- });
1169
- });
1170
- });
1171
-
1172
- restoreScroll('sidebar', scrollPos);
1173
-
1174
- // Auto-scroll expanded turn group so active turn is visible
1175
- var activeTg = sb.querySelector('.tg-item.active');
1176
- if (activeTg) {
1177
- var group = activeTg.closest('.turn-group');
1178
- if (group && group.classList.contains('expanded')) {
1179
- // Defer to after max-height transition starts
1180
- setTimeout(function() {
1181
- var itemTop = activeTg.offsetTop - group.offsetTop;
1182
- var groupH = group.clientHeight;
1183
- if (itemTop > groupH - 40 || itemTop < group.scrollTop) {
1184
- group.scrollTop = Math.max(0, itemTop - groupH / 2 + 16);
1185
- }
1186
- }, 50);
1187
- }
1188
- }
1189
- }
1190
-
1191
- // --- Render: Main Panel ---
1192
- function renderMain() {
1193
- var scrollPos = saveScroll('main-panel');
1194
- var main = document.getElementById('main-panel');
1195
- var session = getSelectedSession();
1196
-
1197
- if (!session) {
1198
- var convos = getConversations();
1199
- if (convos.length === 0) {
1200
- main.innerHTML = '<div class="empty-main">'
1201
- + '<div class="empty-icon"><span class="lens-dot" style="margin:0;"></span></div>'
1202
- + '<div style="font-family:var(--font-mono);font-size:13px;color:var(--text-dim);">No requests captured</div>'
1203
- + '<div style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);">Point API calls to port 4040</div></div>';
1204
- } else {
1205
- main.innerHTML = '<div class="empty-main"><div style="font-family:var(--font-mono);font-size:12px;color:var(--text-dim);">Select a session</div></div>';
1206
- }
1207
- return;
1208
- }
1209
-
1210
- var entries = getSessionEntries(session);
1211
- var classified = classifyEntries(entries);
1212
- var entry = getSelectedEntry(session);
1213
- if (!entry) return;
1214
-
1215
- var ci = entry.contextInfo;
1216
- var utilPct = entry.contextLimit > 0 ? Math.round((ci.totalTokens / entry.contextLimit) * 100) : 0;
1217
- var turnCost = entry.costUsd;
1218
- var sessionCost = sessionTotalCost(session);
1219
- var msgCount = ci.messages ? ci.messages.length : 0;
1220
- var toolCallEntry = (entry.composition || []).find(function(c) { return c.category === 'tool_calls'; });
1221
- var toolCallCount = toolCallEntry ? toolCallEntry.count : 0;
1222
- var u = entry.usage || {};
1223
- var outputTok = u.outputTokens || 0;
1224
- var cacheRead = u.cacheReadTokens || 0;
1225
- var cacheWrite = u.cacheWriteTokens || 0;
1226
-
1227
- var html = '';
1228
-
1229
- // --- Stats row ---
1230
- var utilColor = utilPct > 80 ? 'red' : utilPct > 60 ? 'amber' : 'blue';
1231
- var utilTint = utilPct > 80 ? 'stat-tint-red' : utilPct > 60 ? 'stat-tint-amber' : 'stat-tint-blue';
1232
- html += '<div class="stats-row">';
1233
- html += '<div class="panel"><div class="panel-body"><div class="stat-card ' + utilTint + '"><div class="stat-val ' + utilColor + '">' + utilPct + '%</div><div class="stat-lbl">Context (' + fmtK(ci.totalTokens) + '/' + fmtK(entry.contextLimit) + ')</div></div></div></div>';
1234
- html += '<div class="panel"><div class="panel-body"><div class="stat-card stat-tint-green"><div class="stat-val green">' + fmtCost(turnCost) + '</div><div class="stat-lbl">Turn cost (' + fmtCost(sessionCost) + ' session)</div></div></div></div>';
1235
- html += '<div class="panel"><div class="panel-body"><div class="stat-card"><div class="stat-val white">' + fmtK(outputTok) + '</div><div class="stat-lbl">Output tokens</div></div></div></div>';
1236
- html += '<div class="panel"><div class="panel-body"><div class="stat-card"><div class="stat-val amber">' + toolCallCount + '</div><div class="stat-lbl">Tool calls</div></div></div></div>';
1237
- html += '</div>';
1238
-
1239
- // --- Secondary stats line (cache, messages, stop reason) ---
1240
- var secondaryParts = [];
1241
- secondaryParts.push(msgCount + ' messages');
1242
- if (cacheRead > 0 || cacheWrite > 0) {
1243
- var cachePct = ci.totalTokens > 0 ? Math.round(cacheRead / ci.totalTokens * 100) : 0;
1244
- secondaryParts.push('cache: ' + cachePct + '% hit (' + fmtK(cacheRead) + ' read, ' + fmtK(cacheWrite) + ' write)');
1245
- }
1246
- if (entry.stopReason) secondaryParts.push('stop: ' + entry.stopReason);
1247
- if (entry.responseModel && entry.responseModel !== ci.model) secondaryParts.push('model: ' + entry.responseModel);
1248
- html += '<div style="font-family:var(--font-mono);font-size:10px;color:var(--text-dim);display:flex;gap:16px;flex-wrap:wrap;padding:0 2px;">';
1249
- secondaryParts.forEach(function(p) { html += '<span>' + esc(p) + '</span>'; });
1250
- html += '</div>';
1251
-
1252
- // --- Treemap ---
1253
- html += renderTreemap(entry);
1254
-
1255
- // --- Agent breakdown ---
1256
- html += renderAgentBreakdown(session, classified);
1257
-
1258
- // --- Timeline ---
1259
- html += renderTimeline(entries, classified);
1260
-
1261
- // --- Turn Detail ---
1262
- html += renderTurnDetail(entry, entries, classified);
1263
-
1264
- // --- Recommendations ---
1265
- html += renderRecommendations(entry, entries, classified);
1266
-
1267
- main.innerHTML = html;
1268
- attachMainHandlers();
1269
- restoreScroll('main-panel', scrollPos);
1270
- }
1271
-
1272
- // --- Treemap ---
1273
- function renderTreemap(entry) {
1274
- var composition = entry.composition || [];
1275
- if (composition.length === 0) return '';
1276
-
1277
- var totalTok = composition.reduce(function(s, c) { return s + c.tokens; }, 0);
1278
- var turnLabel = '';
1279
- var session = getSelectedSession();
1280
- if (session) {
1281
- var entries = getSessionEntries(session);
1282
- var classified = classifyEntries(entries);
1283
- var mainNum = 0;
1284
- for (var j = 0; j < classified.length; j++) {
1285
- if (classified[j].isMain) mainNum++;
1286
- if (classified[j].entry.id === entry.id) break;
1287
- }
1288
- turnLabel = ' / Turn ' + mainNum;
1289
- }
1290
-
1291
- var html = '<div class="panel panel-hero">';
1292
- html += '<div class="panel-header">Composition' + esc(turnLabel) + ' <span style="color:var(--text-secondary);font-weight:400;">' + fmtK(totalTok) + ' tok</span>';
1293
- html += '<div class="toggle-group">';
1294
- html += '<div class="toggle-btn' + (treemapMode === 'detailed' ? ' active' : '') + '" data-treemap="detailed">Detailed</div>';
1295
- html += '<div class="toggle-btn' + (treemapMode === 'simple' ? ' active' : '') + '" data-treemap="simple">Simple</div>';
1296
- html += '</div></div>';
1297
- html += '<div class="panel-body">';
1298
-
1299
- if (treemapMode === 'detailed') {
1300
- html += '<div class="treemap">';
1301
- composition.forEach(function(c) {
1302
- var meta = CATEGORY_META[c.category] || { label: c.category, color: '#4b5563' };
1303
- var pct = totalTok > 0 ? Math.round(c.tokens / totalTok * 100) : 0;
1304
- var labelHtml = pct >= 3 ? esc(meta.label) + '<span>' + fmtK(c.tokens) + ' (' + pct + '%)</span>' : '';
1305
- html += '<div class="treemap-block tm-' + esc(c.category) + '" data-category="' + esc(c.category) + '" style="flex:' + c.tokens + ';" title="' + esc(meta.label) + ': ' + c.tokens.toLocaleString() + ' tokens (' + pct + '%)">' + labelHtml + '</div>';
1306
- });
1307
- html += '</div>';
1308
- html += '<div class="treemap-legend">';
1309
- composition.forEach(function(c) {
1310
- var meta = CATEGORY_META[c.category] || { label: c.category, color: '#4b5563' };
1311
- html += '<div class="treemap-legend-item"><div class="treemap-legend-dot" style="background:' + meta.color + ';"></div>' + esc(meta.label) + '</div>';
1312
- });
1313
- html += '</div>';
1314
- } else {
1315
- var groups = {};
1316
- for (var gk in SIMPLE_GROUPS) {
1317
- groups[gk] = 0;
1318
- SIMPLE_GROUPS[gk].forEach(function(cat) {
1319
- var found = composition.find(function(c) { return c.category === cat; });
1320
- if (found) groups[gk] += found.tokens;
1321
- });
1322
- }
1323
- html += '<div class="treemap">';
1324
- for (var gk2 in groups) {
1325
- if (groups[gk2] === 0) continue;
1326
- var meta2 = SIMPLE_META[gk2];
1327
- var pct2 = totalTok > 0 ? Math.round(groups[gk2] / totalTok * 100) : 0;
1328
- html += '<div class="treemap-block" style="flex:' + groups[gk2] + ';background:' + meta2.color + ';">'
1329
- + esc(meta2.label) + '<span>' + fmtK(groups[gk2]) + ' (' + pct2 + '%)</span></div>';
1330
- }
1331
- html += '</div>';
1332
- html += '<div class="treemap-legend">';
1333
- for (var gk3 in SIMPLE_META) {
1334
- if (!groups[gk3]) continue;
1335
- html += '<div class="treemap-legend-item"><div class="treemap-legend-dot" style="background:' + SIMPLE_META[gk3].color + ';"></div>' + esc(SIMPLE_META[gk3].label) + '</div>';
1336
- }
1337
- html += '</div>';
1338
- }
1339
-
1340
- html += '</div></div>';
1341
- return html;
1342
- }
1343
-
1344
- // --- Agent Breakdown ---
1345
- function renderAgentBreakdown(session, classified) {
1346
- var agents = {};
1347
- classified.forEach(function(item) {
1348
- var ak = item.entry.agentKey || '_main';
1349
- if (!agents[ak]) {
1350
- agents[ak] = { key: ak, label: item.entry.agentLabel || 'Main', model: item.entry.contextInfo.model, latestTokens: 0, cost: 0, count: 0, isMain: item.isMain, latestEntry: item.entry };
1351
- }
1352
- agents[ak].latestTokens = item.entry.contextInfo.totalTokens;
1353
- agents[ak].latestEntry = item.entry;
1354
- agents[ak].cost += item.entry.costUsd || 0;
1355
- agents[ak].count += 1;
1356
- });
1357
-
1358
- var agentList = Object.values(agents);
1359
-
1360
- // Separate main agents from sub-agents, then group sub-agents by model
1361
- var mainAgents = agentList.filter(function(ag) { return ag.isMain; });
1362
- var subAgents = agentList.filter(function(ag) { return !ag.isMain; });
1363
-
1364
- var subByModel = {};
1365
- subAgents.forEach(function(ag) {
1366
- var sm = shortModel(ag.model);
1367
- if (!subByModel[sm]) {
1368
- subByModel[sm] = { model: ag.model, shortModel: sm, totalTokens: 0, totalCost: 0, count: 0, agents: [] };
1369
- }
1370
- subByModel[sm].totalTokens += ag.latestTokens;
1371
- subByModel[sm].totalCost += ag.cost;
1372
- subByModel[sm].count += ag.count;
1373
- subByModel[sm].agents.push(ag);
1374
- });
1375
-
1376
- var totalTok = 0;
1377
- mainAgents.forEach(function(ag) { totalTok += ag.latestTokens; });
1378
- for (var smk in subByModel) { totalTok += subByModel[smk].totalTokens; }
1379
-
1380
- var html = '<div class="panel agents-panel">';
1381
- html += '<div class="panel-header">Agents</div>';
1382
- html += '<div class="panel-body" style="max-height:320px;overflow-y:auto;">';
1383
-
1384
- // Main agents
1385
- mainAgents.forEach(function(ag) {
1386
- var pct = totalTok > 0 ? Math.round(ag.latestTokens / totalTok * 100) : 0;
1387
- var color = modelBarColor(ag.model);
1388
- html += '<div class="agent-row">';
1389
- html += '<span class="agent-lbl">Main <span style="font-family:var(--font-mono);color:var(--text-muted);">' + esc(shortModel(ag.model)) + '</span></span>';
1390
- html += '<div class="agent-bar-wrap"><div class="agent-bar" style="width:' + pct + '%;background:' + color + ';"></div></div>';
1391
- html += '<span class="agent-val">' + fmtK(ag.latestTokens) + ' / ' + fmtCost(ag.cost) + '</span>';
1392
- html += '</div>';
1393
- });
1394
-
1395
- // Sub-agent groups (expandable)
1396
- var subModelKeys = Object.keys(subByModel).sort(function(a, b) { return subByModel[b].totalTokens - subByModel[a].totalTokens; });
1397
- subModelKeys.forEach(function(sm) {
1398
- var g = subByModel[sm];
1399
- var groupId = 'agent-group-' + sm;
1400
- var isExpanded = expandedAgentGroups.has(groupId);
1401
- var pct = totalTok > 0 ? Math.round(g.totalTokens / totalTok * 100) : 0;
1402
- var color = modelBarColor(g.model);
1403
- var arrow = isExpanded ? '\u25BE' : '\u25B8';
1404
- html += '<div class="agent-row" data-agent-group="' + esc(groupId) + '" style="cursor:pointer;">';
1405
- html += '<span class="agent-lbl"><span style="color:var(--text-muted);margin-right:4px;">' + arrow + '</span>Sub-agents <span style="font-family:var(--font-mono);color:var(--text-muted);">' + esc(sm) + '</span>';
1406
- html += ' <span style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);">\u00d7' + g.count + '</span></span>';
1407
- html += '<div class="agent-bar-wrap"><div class="agent-bar" style="width:' + pct + '%;background:' + color + ';"></div></div>';
1408
- html += '<span class="agent-val">' + fmtK(g.totalTokens) + ' / ' + fmtCost(g.totalCost) + '</span>';
1409
- html += '</div>';
1410
-
1411
- if (isExpanded) {
1412
- g.agents.sort(function(a, b) { return b.latestTokens - a.latestTokens; });
1413
- g.agents.forEach(function(ag) {
1414
- var subPct = totalTok > 0 ? Math.round(ag.latestTokens / totalTok * 100) : 0;
1415
- var label = ag.label || ag.key;
1416
- html += '<div class="agent-row" style="padding-left:20px;">';
1417
- html += '<span class="agent-lbl" style="color:var(--text-dim);font-size:11px;">' + esc(label) + ' <span style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);">\u00d7' + ag.count + '</span></span>';
1418
- html += '<div class="agent-bar-wrap"><div class="agent-bar" style="width:' + subPct + '%;background:' + color + ';opacity:0.6;"></div></div>';
1419
- html += '<span class="agent-val" style="font-size:10px;">' + fmtK(ag.latestTokens) + ' / ' + fmtCost(ag.cost) + '</span>';
1420
- html += '</div>';
1421
- });
1422
- }
1423
- });
1424
-
1425
- var subPct = classified.length > 0
1426
- ? Math.round(classified.filter(function(x) { return !x.isMain; }).length / classified.length * 100)
1427
- : 0;
1428
- if (subPct > 0) {
1429
- var subTok = subAgents.reduce(function(s, ag) { return s + ag.latestTokens; }, 0);
1430
- var subTokPct = totalTok > 0 ? Math.round(subTok / totalTok * 100) : 0;
1431
- html += '<div style="margin-top:8px;font-size:11px;color:var(--text-muted);">'
1432
- + subPct + '% calls are subagents (' + subTokPct + '% of context)</div>';
1433
- }
1434
-
1435
- html += '</div></div>';
1436
- return html;
1437
- }
1438
-
1439
- // --- Timeline ---
1440
- function renderTimeline(entries, classified) {
1441
- if (entries.length === 0) return '';
1442
-
1443
- var filtered = classified;
1444
- if (timelineMode === 'main') {
1445
- filtered = classified.filter(function(x) { return x.isMain; });
1446
- }
1447
-
1448
- var maxTok = 0;
1449
- filtered.forEach(function(item) {
1450
- var val = timelineMode === 'cost' ? (item.entry.costUsd || 0) * 100000 : item.entry.contextInfo.totalTokens;
1451
- if (val > maxTok) maxTok = val;
1452
- });
1453
-
1454
- var selectedEntry = getSelectedEntry(getSelectedSession());
1455
-
1456
- var html = '<div class="panel panel-hero">';
1457
- html += '<div class="panel-header">Timeline';
1458
- html += '<div class="toggle-group">';
1459
- html += '<div class="toggle-btn' + (timelineMode === 'all' ? ' active' : '') + '" data-timeline="all">All</div>';
1460
- html += '<div class="toggle-btn' + (timelineMode === 'main' ? ' active' : '') + '" data-timeline="main">Main</div>';
1461
- html += '<div class="toggle-btn' + (timelineMode === 'cost' ? ' active' : '') + '" data-timeline="cost">Cost</div>';
1462
- html += '</div></div>';
1463
- html += '<div class="panel-body">';
1464
- var isSparse = filtered.length <= 40;
1465
-
1466
- // Compute Y-axis ticks
1467
- var isCostMode = timelineMode === 'cost';
1468
- var yMax = isCostMode ? (maxTok / 100000) : maxTok; // convert back from scaled cost
1469
- var yTicks = computeYTicks(yMax, 4);
1470
-
1471
- html += '<div class="tl-container">';
1472
-
1473
- // Y-axis
1474
- html += '<div class="tl-y-axis">';
1475
- for (var yi = yTicks.length - 1; yi >= 0; yi--) {
1476
- var yv = yTicks[yi];
1477
- html += '<span>' + (isCostMode ? fmtCost(yv) : fmtK(yv)) + '</span>';
1478
- }
1479
- html += '</div>';
1480
-
1481
- html += '<div class="timeline-wrap" style="flex:1;min-width:0;">';
1482
- html += '<div class="timeline-chart' + (isSparse ? ' tl-sparse' : '') + '">';
1483
-
1484
- var tlMainNum = 0;
1485
- var tlNums = [];
1486
- filtered.forEach(function(item) {
1487
- var val = timelineMode === 'cost' ? (item.entry.costUsd || 0) * 100000 : item.entry.contextInfo.totalTokens;
1488
- var pct = maxTok > 0 ? Math.max(3, Math.round(val / maxTok * 100)) : 3;
1489
- var isActive = selectedEntry && item.entry.id === selectedEntry.id;
1490
- var model = item.entry.contextInfo.model;
1491
- var origIdx = entries.indexOf(item.entry);
1492
- if (item.isMain) tlMainNum++;
1493
- tlNums.push(item.isMain ? tlMainNum : '');
1494
- html += '<div class="tl-bar' + (isActive ? ' active' : '') + '" style="height:' + pct + '%;background:' + modelBarGradient(model, item.isMain) + ';" data-tl-turn="' + origIdx + '" title="' + (item.isMain ? '' : 'Sub ') + esc(shortModel(model)) + ': ' + fmtK(item.entry.contextInfo.totalTokens) + ' tok"></div>';
1495
- });
1496
-
1497
- html += '</div>';
1498
-
1499
- // Turn number labels below bars
1500
- var labelStep = filtered.length > 30 ? Math.ceil(filtered.length / 15) : 1;
1501
- html += '<div class="tl-labels">';
1502
- tlNums.forEach(function(n, i) {
1503
- var show = n && (labelStep <= 1 || n % labelStep === 0 || n === tlMainNum);
1504
- html += '<div class="tl-label">' + (show ? n : '') + '</div>';
1505
- });
1506
- html += '</div>';
1507
-
1508
- html += '</div>'; // timeline-wrap
1509
- html += '</div>'; // tl-container
1510
-
1511
- var seenRoles = {};
1512
- filtered.forEach(function(item) {
1513
- var m = item.entry.contextInfo.model;
1514
- var sm = shortModel(m);
1515
- var key = (item.isMain ? 'main-' : 'sub-') + sm;
1516
- if (!seenRoles[key]) seenRoles[key] = { model: m, isMain: item.isMain, shortModel: sm };
1517
- });
1518
-
1519
- html += '<div class="tl-legend">';
1520
- for (var rk in seenRoles) {
1521
- var info = seenRoles[rk];
1522
- var role = info.isMain ? 'Main' : 'Sub';
1523
- html += '<div class="tl-legend-item"><span class="tl-legend-dot" style="background:' + modelBarColor(info.model) + ';' + (info.isMain ? '' : 'opacity:0.5;') + '"></span>' + role + ' (' + esc(info.shortModel) + ')</div>';
1524
- }
1525
- html += '</div>';
1526
-
1527
- html += '</div></div>';
1528
- return html;
1529
- }
1530
-
1531
- // --- Turn Detail ---
1532
- function renderTurnDetail(entry, entries, classified) {
1533
- var idx = entries.indexOf(entry);
1534
- var turnGroup = [];
1535
- var mainIdx = idx;
1536
- for (var i = idx; i >= 0; i--) {
1537
- if (classified[i].isMain) { mainIdx = i; break; }
1538
- }
1539
- for (var j = mainIdx; j < classified.length; j++) {
1540
- if (j > mainIdx && classified[j].isMain) break;
1541
- turnGroup.push(classified[j]);
1542
- }
1543
-
1544
- var turnNum = 0;
1545
- for (var k = 0; k <= mainIdx; k++) {
1546
- if (classified[k].isMain) turnNum++;
1547
- }
1548
-
1549
- var html = '<div class="panel panel-hero">';
1550
- html += '<div class="panel-header">Turn ' + turnNum;
1551
- html += '<div class="toggle-group">';
1552
- html += '<div class="toggle-btn' + (turnDetailMode === 'calls' ? ' active' : '') + '" data-detail="calls">Calls</div>';
1553
- html += '<div class="toggle-btn' + (turnDetailMode === 'diff' ? ' active' : '') + '" data-detail="diff">Diff</div>';
1554
- html += '</div></div>';
1555
- html += '<div class="panel-body">';
1556
-
1557
- if (turnDetailMode === 'calls') {
1558
- var subCount = turnGroup.filter(function(x) { return !x.isMain; }).length;
1559
- var totalCallTok = turnGroup.reduce(function(s, x) { return s + x.entry.contextInfo.totalTokens; }, 0);
1560
- var totalCallCost = turnGroup.reduce(function(s, x) { return s + (x.entry.costUsd || 0); }, 0);
1561
- var isCollapsible = turnGroup.length > 3;
1562
- var isExpanded = !isCollapsible || turnCallsExpanded;
1563
- var arrow = isExpanded ? '\u25BE' : '\u25B8';
1564
-
1565
- html += '<div class="turn-detail-header"' + (isCollapsible ? ' data-toggle-calls style="cursor:pointer;"' : '') + '>';
1566
- if (isCollapsible) html += '<span style="color:var(--text-muted);margin-right:4px;">' + arrow + '</span>';
1567
- html += turnGroup.length + ' API call' + (turnGroup.length !== 1 ? 's' : '');
1568
- if (subCount > 0) html += ' <span style="color:var(--text-muted);">(' + subCount + ' sub)</span>';
1569
- html += '<span style="margin-left:auto;font-family:var(--font-mono);color:var(--text-dim);">' + fmtK(totalCallTok) + ' tok / ' + fmtCost(totalCallCost) + '</span>';
1570
- html += '</div>';
1571
-
1572
- if (isExpanded) {
1573
- turnGroup.forEach(function(item) {
1574
- var e = item.entry;
1575
- var isMain = item.isMain;
1576
- var sm = shortModel(e.contextInfo.model);
1577
- var summary = extractCallSummary(e) || e.agentLabel || (isMain ? 'Main call' : 'Subagent call');
1578
- var desc = esc(summary);
1579
- html += '<div class="call-row' + (isMain ? ' main-call' : '') + '">';
1580
- html += '<span class="call-model ' + modelClass(e.contextInfo.model) + '">' + esc(sm) + '</span>';
1581
- html += '<span class="call-desc' + (isMain ? ' main' : '') + '">' + desc + '</span>';
1582
- html += '<span class="call-tok">' + fmtK(e.contextInfo.totalTokens) + ' tok</span>';
1583
- html += '<span class="call-cost">' + fmtCost(e.costUsd) + '</span>';
1584
- html += '</div>';
1585
- if (turnGroup.length > 1) {
1586
- var inputTools = extractInputTools(e);
1587
- if (inputTools && inputTools.length > 0) {
1588
- html += '<div class="call-tools">\u2190 ' + esc(inputTools.join(', ')) + '</div>';
1589
- }
1590
- }
1591
- });
1592
- }
1593
- html += renderMessageCategories(entry);
1594
- } else {
1595
- html += renderContextDiff(entry, entries, classified, turnNum);
1596
- }
1597
-
1598
- html += '</div></div>';
1599
- return html;
1600
- }
1601
-
1602
- // --- Message Categories ---
1603
- function renderMessageCategories(entry) {
1604
- var ci = entry.contextInfo;
1605
- var msgs = ci.messages || [];
1606
- if (msgs.length === 0) return '';
1607
-
1608
- var msgTokens = msgs.reduce(function(s, m) { return s + (m.tokens || 0); }, 0);
1609
-
1610
- // Build tool_use_id -> tool name map for correlating results to their tools
1611
- var toolNameMap = {};
1612
- msgs.forEach(function(m) {
1613
- if (m.contentBlocks) {
1614
- m.contentBlocks.forEach(function(b) {
1615
- if (b.type === 'tool_use' && b.id && b.name) toolNameMap[b.id] = b.name;
1616
- });
1617
- }
1618
- });
1619
-
1620
- var html = '<div class="msg-section">';
1621
- html += '<div class="msg-section-title"><span>' + msgs.length + ' messages</span><span>' + fmtK(msgTokens) + ' tok</span></div>';
1622
-
1623
- var categories = {};
1624
- var catOrder = ['tool_results', 'system_injections', 'tool_calls', 'thinking', 'assistant_text', 'user_text'];
1625
-
1626
- msgs.forEach(function(msg, i) {
1627
- var cat = classifyMessageRole(msg);
1628
- if (!categories[cat]) categories[cat] = { items: [], tokens: 0 };
1629
- categories[cat].items.push({ msg: msg, idx: i });
1630
- categories[cat].tokens += msg.tokens || 0;
1631
- });
1632
-
1633
- for (var cat in categories) {
1634
- if (catOrder.indexOf(cat) === -1) catOrder.push(cat);
1635
- }
1636
-
1637
- catOrder.forEach(function(cat) {
1638
- var data = categories[cat];
1639
- if (!data) return;
1640
- var meta = CATEGORY_META[cat] || { label: cat, color: '#4b5563' };
1641
- var pct = msgTokens > 0 ? Math.round(data.tokens / msgTokens * 100) : 0;
1642
- var catId = 'msgcat-' + cat;
1643
- var isExpanded = expandedCategories.has(catId);
1644
- var arrow = isExpanded ? '\u25BE' : '\u25B8';
1645
-
1646
- html += '<div class="msg-category">';
1647
- html += '<div class="msg-cat-header" data-cat="' + catId + '">';
1648
- html += '<span style="color:var(--text-muted);">' + arrow + '</span>';
1649
- html += '<span class="msg-cat-dot" style="background:' + meta.color + ';"></span>';
1650
- html += '<span class="msg-cat-name" style="color:' + catTextColor(cat) + ';">' + esc(meta.label) + '</span>';
1651
- html += '<span class="msg-cat-stats">' + data.items.length + ' / ' + fmtK(data.tokens) + ' (' + pct + '%)</span>';
1652
- html += '<div class="msg-cat-bar-wrap"><div class="msg-cat-bar" style="width:' + pct + '%;background:' + meta.color + ';"></div></div>';
1653
- html += '</div>';
1654
-
1655
- html += '<div class="msg-items' + (isExpanded ? ' expanded' : '') + '" id="' + catId + '">';
1656
- data.items.forEach(function(item) {
1657
- var msg = item.msg;
1658
- var preview = extractPreview(msg, toolNameMap);
1659
- var isLarge = (msg.tokens || 0) > 1000;
1660
- html += '<div class="msg-line">';
1661
- html += '<span class="msg-line-icon">' + catIcon(cat) + '</span>';
1662
- html += '<span class="msg-line-text">' + esc(preview) + '</span>';
1663
- html += '<span class="msg-line-tok' + (isLarge ? ' large' : '') + '">' + fmtK(msg.tokens || 0) + '</span>';
1664
- html += '</div>';
1665
- });
1666
- html += '</div>';
1667
- html += '</div>';
1668
- });
1669
-
1670
- html += '</div>';
1671
- return html;
1672
- }
1673
-
1674
- function classifyMessageRole(msg) {
1675
- var role = msg.role || 'user';
1676
- var content = msg.content || '';
1677
- var blocks = msg.contentBlocks;
1678
-
1679
- if (blocks && Array.isArray(blocks)) {
1680
- var hasToolUse = blocks.some(function(b) { return b.type === 'tool_use'; });
1681
- var hasToolResult = blocks.some(function(b) { return b.type === 'tool_result'; });
1682
- var hasThinking = blocks.some(function(b) { return b.type === 'thinking'; });
1683
- if (hasToolResult) return 'tool_results';
1684
- if (hasToolUse) return 'tool_calls';
1685
- if (hasThinking) return 'thinking';
1686
- }
1687
-
1688
- if (typeof content === 'string' && content.includes('<system-reminder>')) return 'system_injections';
1689
- if (role === 'assistant') return 'assistant_text';
1690
- if (role === 'user') return 'user_text';
1691
- return 'other';
1692
- }
1693
-
1694
- function extractPreview(msg, toolNameMap) {
1695
- var content = msg.content || '';
1696
- var blocks = msg.contentBlocks;
1697
-
1698
- if (blocks && Array.isArray(blocks)) {
1699
- for (var i = 0; i < blocks.length; i++) {
1700
- var b = blocks[i];
1701
- if (b.type === 'tool_use') return (b.name || 'tool') + '(' + (b.input ? JSON.stringify(b.input).slice(0, 60) : '') + ')';
1702
- if (b.type === 'tool_result') {
1703
- var toolName = (toolNameMap && b.tool_use_id) ? toolNameMap[b.tool_use_id] : null;
1704
- var rc = typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');
1705
- var prefix = toolName ? toolName + ': ' : '';
1706
- return prefix + rc.slice(0, 80 - prefix.length);
1707
- }
1708
- if (b.type === 'thinking') return (b.thinking || b.text || '').slice(0, 80);
1709
- if (b.type === 'text' || b.type === 'input_text') return (b.text || '').slice(0, 80);
1710
- }
1711
- }
1712
-
1713
- if (content.charAt(0) === '[') {
1714
- try {
1715
- var parsed = JSON.parse(content);
1716
- if (Array.isArray(parsed) && parsed[0]) {
1717
- var first = parsed[0];
1718
- if (first.type === 'tool_use') return (first.name || 'tool') + '(...)';
1719
- if (first.type === 'tool_result') return String(first.content || '').slice(0, 80);
1720
- if (first.text) return first.text.slice(0, 80);
1721
- }
1722
- } catch(e) {}
1723
- }
1724
-
1725
- return content.slice(0, 80);
1726
- }
1727
-
1728
- function catTextColor(cat) {
1729
- var colors = {
1730
- tool_results: '#10b981', system_injections: '#818cf8', tool_calls: '#f472b6',
1731
- thinking: '#a78bfa', assistant_text: '#f59e0b', user_text: '#10b981',
1732
- images: '#9ca3af', other: '#9ca3af',
1733
- };
1734
- return colors[cat] || '#888';
1735
- }
1736
-
1737
- function catIcon(cat) {
1738
- var icons = {
1739
- tool_results: '\u25A0',
1740
- system_injections: '\u25C6',
1741
- tool_calls: '\u25B6',
1742
- thinking: '\u25CB',
1743
- assistant_text: '\u25B7',
1744
- user_text: '\u25CF',
1745
- };
1746
- return icons[cat] || '\u25AA';
1747
- }
1748
-
1749
- function extractCallSummary(entry) {
1750
- var msgs = (entry.contextInfo && entry.contextInfo.messages) || [];
1751
- // 1. Anthropic format: scan backward for assistant message with tool_use blocks
1752
- for (var i = msgs.length - 1; i >= 0; i--) {
1753
- var m = msgs[i];
1754
- if (m.role === 'assistant' && m.contentBlocks) {
1755
- var names = [];
1756
- var seen = {};
1757
- m.contentBlocks.forEach(function(b) {
1758
- if (b.type === 'tool_use' && b.name && !seen[b.name]) {
1759
- seen[b.name] = true;
1760
- names.push(b.name);
1761
- }
1762
- });
1763
- if (names.length > 0) {
1764
- if (names.length <= 4) return names.join(', ');
1765
- return names.slice(0, 3).join(', ') + ' +' + (names.length - 3);
1766
- }
1767
- }
1768
- }
1769
- // 2. OpenAI format: extract function/tool names from JSON content via regex (avoids parsing huge payloads)
1770
- var fnNames = [];
1771
- var fnSeen = {};
1772
- for (var fi = msgs.length - 1; fi >= 0; fi--) {
1773
- var fc = msgs[fi].content || '';
1774
- if (typeof fc === 'string' && fc.length > 5 && fc.indexOf('"name"') !== -1 && (fc.indexOf('"function_call"') !== -1 || fc.indexOf('"custom_tool_call"') !== -1) && fc.indexOf('"function_call_output"') === -1) {
1775
- var nameMatch = fc.match(/"name"\s*:\s*"([^"]+)"/);
1776
- if (nameMatch && !fnSeen[nameMatch[1]]) { fnSeen[nameMatch[1]] = true; fnNames.push(nameMatch[1]); }
1777
- }
1778
- if (fnNames.length >= 4) break;
1779
- }
1780
- if (fnNames.length > 0) {
1781
- if (fnNames.length <= 4) return fnNames.join(', ');
1782
- return fnNames.slice(0, 3).join(', ') + ' +' + (fnNames.length - 3);
1783
- }
1784
- // 3. Fallback: last readable assistant text (skip JSON)
1785
- for (var ai = msgs.length - 1; ai >= 0; ai--) {
1786
- if (msgs[ai].role === 'assistant') {
1787
- var ac = msgs[ai].content || '';
1788
- if (msgs[ai].contentBlocks) {
1789
- for (var ak = 0; ak < msgs[ai].contentBlocks.length; ak++) {
1790
- var ab = msgs[ai].contentBlocks[ak];
1791
- if ((ab.type === 'text' || ab.type === 'input_text') && ab.text) { ac = ab.text; break; }
1792
- }
1793
- }
1794
- if (typeof ac === 'string' && ac.length > 0 && ac.charAt(0) !== '{' && ac.charAt(0) !== '[' && ac.charAt(0) !== '<') {
1795
- return ac.slice(0, 50);
1796
- }
1797
- }
1798
- }
1799
- // 4. Fallback: last user text that isn't JSON tool output
1800
- for (var j = msgs.length - 1; j >= 0; j--) {
1801
- if (msgs[j].role === 'user') {
1802
- var content = msgs[j].content || '';
1803
- if (msgs[j].contentBlocks) {
1804
- for (var k = 0; k < msgs[j].contentBlocks.length; k++) {
1805
- var b = msgs[j].contentBlocks[k];
1806
- if ((b.type === 'text' || b.type === 'input_text') && b.text) {
1807
- content = b.text;
1808
- break;
1809
- }
1810
- }
1811
- }
1812
- // Try to extract text from OpenAI input_text array format
1813
- if (typeof content === 'string' && content.charAt(0) === '[' && content.indexOf('input_text') !== -1) {
1814
- var itMatch = content.match(/"text"\s*:\s*"([^"]{1,80})/);
1815
- if (itMatch) {
1816
- var itText = itMatch[1];
1817
- if (!itText.startsWith('#') && !itText.startsWith('<environment')) {
1818
- return itText.slice(0, 50);
1819
- }
1820
- }
1821
- continue;
1822
- }
1823
- if (typeof content === 'string' && content.length > 0 && content.charAt(0) !== '{' && content.charAt(0) !== '[' && content.charAt(0) !== '<') {
1824
- return content.slice(0, 50);
1825
- }
1826
- }
1827
- }
1828
- return '';
1829
- }
1830
-
1831
- function extractInputTools(entry) {
1832
- var msgs = (entry.contextInfo && entry.contextInfo.messages) || [];
1833
- // Build name map from all tool_use blocks
1834
- var nameMap = {};
1835
- msgs.forEach(function(m) {
1836
- if (m.contentBlocks) {
1837
- m.contentBlocks.forEach(function(b) {
1838
- if (b.type === 'tool_use' && b.id && b.name) nameMap[b.id] = b.name;
1839
- });
1840
- }
1841
- });
1842
- // Find last user message with tool_result blocks
1843
- for (var i = msgs.length - 1; i >= 0; i--) {
1844
- var m = msgs[i];
1845
- if (m.role === 'user' && m.contentBlocks) {
1846
- var tools = [];
1847
- var seen = {};
1848
- m.contentBlocks.forEach(function(b) {
1849
- if (b.type === 'tool_result' && b.tool_use_id) {
1850
- var name = nameMap[b.tool_use_id] || b.tool_use_id;
1851
- if (!seen[name]) { seen[name] = true; tools.push(name); }
1852
- }
1853
- });
1854
- if (tools.length > 0) return tools;
1855
- }
1856
- }
1857
- return null;
1858
- }
1859
-
1860
- // --- Context Diff ---
1861
- function renderContextDiff(entry, entries, classified, turnNum) {
1862
- var idx = entries.indexOf(entry);
1863
- var prevEntry = null;
1864
- for (var i = idx - 1; i >= 0; i--) {
1865
- if (classified[i].isMain) { prevEntry = entries[i]; break; }
1866
- }
1867
-
1868
- if (!prevEntry) {
1869
- return '<div style="font-family:var(--font-mono);color:var(--text-muted);font-size:11px;">First turn -- no previous context to diff.</div>';
1870
- }
1871
-
1872
- var prevComp = prevEntry.composition || [];
1873
- var currComp = entry.composition || [];
1874
- var prevTotal = prevComp.reduce(function(s, c) { return s + c.tokens; }, 0);
1875
- var currTotal = currComp.reduce(function(s, c) { return s + c.tokens; }, 0);
1876
- var delta = currTotal - prevTotal;
1877
- var deltaSign = delta >= 0 ? '+' : '';
1878
-
1879
- var html = '<div class="diff-header">';
1880
- html += '<span>Turn ' + (turnNum - 1) + ' \u2192 ' + turnNum + '</span>';
1881
- html += '<span style="color:' + (delta >= 0 ? 'var(--accent-green)' : 'var(--accent-red)') + ';">' + deltaSign + fmtK(delta) + ' tok</span>';
1882
- html += '</div>';
1883
-
1884
- var allCats = {};
1885
- prevComp.forEach(function(c) { allCats[c.category] = true; });
1886
- currComp.forEach(function(c) { allCats[c.category] = true; });
1887
-
1888
- for (var cat in allCats) {
1889
- var prev = prevComp.find(function(c) { return c.category === cat; });
1890
- var curr = currComp.find(function(c) { return c.category === cat; });
1891
- var prevTok = prev ? prev.tokens : 0;
1892
- var currTok = curr ? curr.tokens : 0;
1893
- var d = currTok - prevTok;
1894
- var meta = CATEGORY_META[cat] || { label: cat };
1895
-
1896
- if (d === 0) {
1897
- html += '<div class="diff-line diff-same"> ' + esc(meta.label) + ': ' + fmtK(currTok) + ' (unchanged)</div>';
1898
- } else if (prevTok === 0) {
1899
- html += '<div class="diff-line diff-add">+ ' + esc(meta.label) + ': ' + fmtK(currTok) + ' (new)</div>';
1900
- } else if (currTok === 0) {
1901
- html += '<div class="diff-line diff-remove">- ' + esc(meta.label) + ': ' + fmtK(prevTok) + ' (removed)</div>';
1902
- } else if (d > 0) {
1903
- html += '<div class="diff-line diff-add">+ ' + esc(meta.label) + ': ' + fmtK(prevTok) + ' \u2192 ' + fmtK(currTok) + ' (+' + fmtK(d) + ')</div>';
1904
- } else {
1905
- html += '<div class="diff-line diff-remove">- ' + esc(meta.label) + ': ' + fmtK(prevTok) + ' \u2192 ' + fmtK(currTok) + ' (' + fmtK(d) + ')</div>';
1906
- }
1907
- }
1908
-
1909
- return html;
1910
- }
1911
-
1912
- // --- Recommendations ---
1913
- function renderRecommendations(entry, entries, classified) {
1914
- var recs = [];
1915
- var ci = entry.contextInfo;
1916
- var comp = entry.composition || [];
1917
- var totalTok = comp.reduce(function(s, c) { return s + c.tokens; }, 0);
1918
-
1919
- // Build tool name map for findings
1920
- var findingsToolNameMap = {};
1921
- (ci.messages || []).forEach(function(m) {
1922
- if (m.contentBlocks) {
1923
- m.contentBlocks.forEach(function(b) {
1924
- if (b.type === 'tool_use' && b.id && b.name) findingsToolNameMap[b.id] = b.name;
1925
- });
1926
- }
1927
- });
1928
-
1929
- var toolResults = comp.find(function(c) { return c.category === 'tool_results'; });
1930
- if (toolResults && totalTok > 0 && toolResults.pct > 25) {
1931
- var largestResult = 0;
1932
- var largestToolName = '';
1933
- (ci.messages || []).forEach(function(m) {
1934
- if (classifyMessageRole(m) === 'tool_results' && m.tokens > largestResult) {
1935
- largestResult = m.tokens;
1936
- // Find the tool name from contentBlocks
1937
- if (m.contentBlocks) {
1938
- for (var bi = 0; bi < m.contentBlocks.length; bi++) {
1939
- var b = m.contentBlocks[bi];
1940
- if (b.type === 'tool_result' && b.tool_use_id && findingsToolNameMap[b.tool_use_id]) {
1941
- largestToolName = findingsToolNameMap[b.tool_use_id];
1942
- break;
1943
- }
1944
- }
1945
- }
1946
- }
1947
- });
1948
- if (largestResult > 2000) {
1949
- var lrPct = Math.round(largestResult / totalTok * 100);
1950
- var lrTitle = 'Large tool result' + (largestToolName ? ': ' + largestToolName : '') + ' (' + fmtK(largestResult) + ' tokens, ' + lrPct + '% of context)';
1951
- recs.push({
1952
- severity: 'high',
1953
- title: lrTitle,
1954
- detail: 'This single result reduces space for conversation history and is re-sent every turn. Consider truncating output or using summary flags.',
1955
- impact: fmtK(largestResult) + ' tok',
1956
- });
1957
- }
1958
- }
1959
-
1960
- var toolDefs = comp.find(function(c) { return c.category === 'tool_definitions'; });
1961
- if (toolDefs && toolDefs.pct > 30) {
1962
- var usedTools = {};
1963
- entries.forEach(function(e) {
1964
- (e.contextInfo.messages || []).forEach(function(m) {
1965
- if (m.contentBlocks) {
1966
- m.contentBlocks.forEach(function(b) {
1967
- if (b.type === 'tool_use' && b.name) usedTools[b.name] = true;
1968
- });
1969
- }
1970
- });
1971
- });
1972
- var usedCount = Object.keys(usedTools).length;
1973
- var totalTools = ci.tools ? ci.tools.length : 0;
1974
- // Get all defined tool names
1975
- var allDefinedNames = [];
1976
- if (ci.tools) {
1977
- ci.tools.forEach(function(t) {
1978
- var name = t.name || (t.function && t.function.name);
1979
- if (name) allDefinedNames.push(name);
1980
- });
1981
- }
1982
- var unusedNames = allDefinedNames.filter(function(n) { return !usedTools[n]; });
1983
- if (totalTools > usedCount + 3) {
1984
- var unusedPreview = unusedNames.slice(0, 5).join(', ');
1985
- if (unusedNames.length > 5) unusedPreview += ' +' + (unusedNames.length - 5) + ' more';
1986
- var wastedTok = Math.round(toolDefs.tokens * unusedNames.length / totalTools);
1987
- recs.push({
1988
- severity: 'high',
1989
- title: usedCount + '/' + totalTools + ' tools used, ' + unusedNames.length + ' idle',
1990
- detail: 'Unused: ' + unusedPreview + '. Definitions cost ~' + fmtK(wastedTok) + ' tokens \u2014 re-sent every turn. Remove unused tools to free context.',
1991
- impact: '~' + fmtK(wastedTok) + ' tok',
1992
- });
1993
- }
1994
- }
1995
-
1996
- if (toolResults && toolResults.pct > 60) {
1997
- var userText = comp.find(function(c) { return c.category === 'user_text'; });
1998
- var userPct = userText ? userText.pct : 0;
1999
- // Build per-tool token breakdown
2000
- var perToolTokens = {};
2001
- (ci.messages || []).forEach(function(m) {
2002
- if (classifyMessageRole(m) === 'tool_results' && m.contentBlocks) {
2003
- m.contentBlocks.forEach(function(b) {
2004
- if (b.type === 'tool_result' && b.tool_use_id) {
2005
- var tname = findingsToolNameMap[b.tool_use_id] || 'unknown';
2006
- perToolTokens[tname] = (perToolTokens[tname] || 0) + (m.tokens || 0);
2007
- }
2008
- });
2009
- }
2010
- });
2011
- var topTools = Object.keys(perToolTokens).map(function(n) {
2012
- return { name: n, tokens: perToolTokens[n] };
2013
- }).sort(function(a, b) { return b.tokens - a.tokens; }).slice(0, 3);
2014
- var topToolsStr = topTools.map(function(t) { return t.name + ' (' + fmtK(t.tokens) + ')'; }).join(', ');
2015
- recs.push({
2016
- severity: 'med',
2017
- title: 'Tool results dominate: ' + toolResults.pct + '% of context',
2018
- detail: 'Top tools: ' + topToolsStr + '. User text is only ' + userPct + '%. Large results crowd out conversation history.',
2019
- impact: 'Structural',
2020
- });
2021
- }
2022
-
2023
- var utilPct = entry.contextLimit > 0 ? Math.round(ci.totalTokens / entry.contextLimit * 100) : 0;
2024
- if (utilPct > 80) {
2025
- recs.push({
2026
- severity: 'high',
2027
- title: 'Context utilization critical: ' + utilPct + '%',
2028
- detail: fmtK(ci.totalTokens) + ' of ' + fmtK(entry.contextLimit) + ' tokens. Overflow risk imminent \u2014 model will drop or summarize older messages.',
2029
- impact: 'Critical',
2030
- });
2031
- } else if (utilPct > 60) {
2032
- recs.push({
2033
- severity: 'med',
2034
- title: 'Context utilization high: ' + utilPct + '%',
2035
- detail: fmtK(ci.totalTokens) + ' of ' + fmtK(entry.contextLimit) + ' tokens used.',
2036
- impact: 'Monitor',
2037
- });
2038
- } else {
2039
- recs.push({
2040
- severity: 'low',
2041
- title: 'Context utilization healthy: ' + utilPct + '%',
2042
- detail: fmtK(ci.totalTokens) + ' of ' + fmtK(entry.contextLimit) + ' tokens. Safe range.',
2043
- impact: 'OK',
2044
- });
2045
- }
2046
-
2047
- var idx = entries.indexOf(entry);
2048
- if (idx > 0) {
2049
- var prevIdx = -1;
2050
- for (var i = idx - 1; i >= 0; i--) {
2051
- if (classified[i].isMain) { prevIdx = i; break; }
2052
- }
2053
- if (prevIdx >= 0) {
2054
- var prevTok = entries[prevIdx].contextInfo.totalTokens;
2055
- if (ci.totalTokens < prevTok * 0.7) {
2056
- recs.push({
2057
- severity: 'med',
2058
- title: 'Compaction detected',
2059
- detail: 'Tokens dropped ' + fmtK(prevTok) + ' \u2192 ' + fmtK(ci.totalTokens) + '. Context was summarized or truncated \u2014 older details may be lost.',
2060
- impact: '-' + fmtK(prevTok - ci.totalTokens) + ' tok',
2061
- });
2062
- }
2063
- }
2064
- }
2065
-
2066
- var sysInj = comp.find(function(c) { return c.category === 'system_injections'; });
2067
- if (sysInj && sysInj.pct > 5) {
2068
- recs.push({
2069
- severity: 'med',
2070
- title: 'System injections: ' + sysInj.pct + '% of context',
2071
- detail: fmtK(sysInj.tokens) + ' tokens of system-reminder blocks injected into messages. These are re-sent every turn, reducing space for actual conversation.',
2072
- impact: fmtK(sysInj.tokens) + ' tok',
2073
- });
2074
- }
2075
-
2076
- if (recs.length === 0) return '';
2077
-
2078
- var html = '<div class="panel">';
2079
- html += '<div class="panel-header">Findings <span style="font-size:10px;color:var(--text-dim);font-weight:400;">' + recs.length + '</span></div>';
2080
- html += '<div class="panel-body">';
2081
-
2082
- recs.forEach(function(r) {
2083
- html += '<div class="rec-item">';
2084
- html += '<span class="rec-severity ' + r.severity + '"></span>';
2085
- html += '<div class="rec-body">';
2086
- html += '<div class="rec-title">' + esc(r.title) + '</div>';
2087
- html += '<div class="rec-detail">' + esc(r.detail) + '</div>';
2088
- html += '</div>';
2089
- html += '<span class="rec-impact ' + r.severity + '">' + esc(r.impact) + '</span>';
2090
- html += '</div>';
2091
- });
2092
-
2093
- html += '</div></div>';
2094
- return html;
2095
- }
2096
-
2097
- // --- Event handlers for main panel ---
2098
- function attachMainHandlers() {
2099
- document.querySelectorAll('[data-treemap]').forEach(function(el) {
2100
- el.addEventListener('click', function() {
2101
- treemapMode = el.getAttribute('data-treemap');
2102
- renderMain();
2103
- });
2104
- });
2105
-
2106
- document.querySelectorAll('[data-timeline]').forEach(function(el) {
2107
- el.addEventListener('click', function() {
2108
- timelineMode = el.getAttribute('data-timeline');
2109
- renderMain();
2110
- });
2111
- });
2112
-
2113
- document.querySelectorAll('[data-detail]').forEach(function(el) {
2114
- el.addEventListener('click', function() {
2115
- turnDetailMode = el.getAttribute('data-detail');
2116
- renderMain();
2117
- });
2118
- });
2119
-
2120
- document.querySelectorAll('[data-cat]').forEach(function(el) {
2121
- el.addEventListener('click', function() {
2122
- var catId = el.getAttribute('data-cat');
2123
- if (expandedCategories.has(catId)) expandedCategories.delete(catId);
2124
- else expandedCategories.add(catId);
2125
- renderMain();
2126
- });
2127
- });
2128
-
2129
- document.querySelectorAll('[data-agent-group]').forEach(function(el) {
2130
- el.addEventListener('click', function() {
2131
- var groupId = el.getAttribute('data-agent-group');
2132
- if (expandedAgentGroups.has(groupId)) expandedAgentGroups.delete(groupId);
2133
- else expandedAgentGroups.add(groupId);
2134
- renderMain();
2135
- });
2136
- });
2137
-
2138
- document.querySelectorAll('[data-toggle-calls]').forEach(function(el) {
2139
- el.addEventListener('click', function() {
2140
- turnCallsExpanded = !turnCallsExpanded;
2141
- renderMain();
2142
- });
2143
- });
2144
-
2145
- // Treemap block click → navigate to matching category
2146
- document.querySelectorAll('.treemap-block[data-category]').forEach(function(el) {
2147
- el.addEventListener('click', function() {
2148
- var cat = el.getAttribute('data-category');
2149
- if (!cat) return;
2150
- var catId = 'msgcat-' + cat;
2151
- // Switch to calls view if needed
2152
- if (turnDetailMode !== 'calls') {
2153
- turnDetailMode = 'calls';
2154
- expandedCategories.add(catId);
2155
- renderMain();
2156
- return;
2157
- }
2158
- // Expand the category
2159
- expandedCategories.add(catId);
2160
- renderMain();
2161
- // Scroll to and flash-highlight the category
2162
- setTimeout(function() {
2163
- var target = document.getElementById(catId);
2164
- if (!target) return;
2165
- var catEl = target.closest('.msg-category');
2166
- if (catEl) {
2167
- catEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
2168
- catEl.classList.remove('flash-highlight');
2169
- void catEl.offsetWidth; // reflow to re-trigger animation
2170
- catEl.classList.add('flash-highlight');
2171
- }
2172
- }, 50);
2173
- });
2174
- });
2175
-
2176
- document.querySelectorAll('[data-tl-turn]').forEach(function(el) {
2177
- el.addEventListener('click', function() {
2178
- var idx = parseInt(el.getAttribute('data-tl-turn'), 10);
2179
- if (!isNaN(idx)) {
2180
- selectedTurnIndex = idx;
2181
- renderSidebar();
2182
- renderMain();
2183
- }
2184
- });
2185
- });
2186
-
2187
- // Auto-scroll timeline to active bar
2188
- var activeBar = document.querySelector('.tl-bar.active');
2189
- if (activeBar) {
2190
- var wrap = activeBar.closest('.timeline-wrap');
2191
- if (wrap && wrap.scrollWidth > wrap.clientWidth) {
2192
- var barLeft = activeBar.offsetLeft;
2193
- var wrapW = wrap.clientWidth;
2194
- if (barLeft < wrap.scrollLeft || barLeft > wrap.scrollLeft + wrapW - 20) {
2195
- wrap.scrollLeft = Math.max(0, barLeft - wrapW / 2);
2196
- }
2197
- }
2198
- }
2199
- }
2200
-
2201
- // --- Source filter ---
2202
- document.getElementById('hdr-source-filter').addEventListener('change', function() {
2203
- sourceFilter = this.value;
2204
- if (selectedSessionId) {
2205
- var session = getSelectedSession();
2206
- if (!session) {
2207
- selectedSessionId = null;
2208
- selectedTurnIndex = -1;
2209
- }
2210
- }
2211
- renderSidebar();
2212
- renderMain();
2213
- });
2214
-
2215
- // --- Export ---
2216
- document.getElementById('export-btn').addEventListener('click', function() {
2217
- var url = '/api/export/lhar.json';
2218
- if (selectedSessionId) url += '?conversation=' + encodeURIComponent(selectedSessionId);
2219
- fetch(url).then(function(r) { return r.blob(); }).then(function(blob) {
2220
- var a = document.createElement('a');
2221
- a.href = URL.createObjectURL(blob);
2222
- a.download = 'context-lens-export.lhar.json';
2223
- a.click();
2224
- URL.revokeObjectURL(a.href);
2225
- });
2226
- });
2227
-
2228
- // --- Reset ---
2229
- document.getElementById('reset-btn').addEventListener('click', function() {
2230
- if (!confirm('Delete all captured data?')) return;
2231
- fetch('/api/reset', { method: 'POST' }).then(function() {
2232
- selectedSessionId = null;
2233
- selectedTurnIndex = -1;
2234
- expandedSessions.clear();
2235
- currentRevision = -1;
2236
- fetchAndRender();
2237
- });
2238
- });
2239
-
2240
- // --- Polling ---
2241
- function fetchAndRender() {
2242
- // Flash poll indicator
2243
- var indicator = document.getElementById('poll-indicator');
2244
- if (indicator) { indicator.classList.add('active'); setTimeout(function() { indicator.classList.remove('active'); }, 300); }
2245
-
2246
- fetch('/api/requests').then(function(r) { return r.json(); }).then(function(data) {
2247
- var changed = data.revision !== currentRevision;
2248
- currentRevision = data.revision;
2249
- currentData = data;
2250
-
2251
- if (changed) {
2252
- if (!selectedSessionId) {
2253
- var convos = getConversations();
2254
- if (convos.length > 0) {
2255
- selectedSessionId = convos[0].id;
2256
- selectedTurnIndex = -1;
2257
- expandedSessions.add(convos[0].id);
2258
- }
2259
- }
2260
- renderHeader();
2261
- renderSidebar();
2262
- renderMain();
2263
- }
2264
- }).catch(function(err) {
2265
- console.error('Fetch error:', err);
2266
- });
2267
- }
2268
-
2269
- // =============================
2270
- // Detail Panel
2271
- // =============================
2272
- var detailOpen = false;
2273
- var detailTab = 'rendered'; // 'rendered' | 'raw'
2274
- var detailMsg = null; // current ParsedMessage
2275
- var detailMsgList = []; // all messages in current entry
2276
- var detailMsgIndex = -1; // index into detailMsgList
2277
- var detailEntry = null; // the CapturedEntry for metadata
2278
- var detailToolNameMap = {}; // tool_use_id -> name
2279
- var detailMaxLines = 500; // truncation threshold
2280
- var detailMaxChars = 50000; // char cap for performance
2281
- var detailShowAll = false; // show full content
2282
-
2283
- function openDetail(msg, msgList, msgIndex, entry, toolMap) {
2284
- detailMsg = msg;
2285
- detailMsgList = msgList;
2286
- detailMsgIndex = msgIndex;
2287
- detailEntry = entry;
2288
- detailToolNameMap = toolMap || {};
2289
- detailTab = 'rendered';
2290
- detailShowAll = false;
2291
- detailOpen = true;
2292
- document.getElementById('detail-overlay').classList.add('open');
2293
- renderDetail();
2294
- // Mark selected msg-line
2295
- document.querySelectorAll('.msg-line').forEach(function(el, i) {
2296
- el.classList.toggle('selected', i === msgIndex);
2297
- });
2298
- }
2299
-
2300
- function closeDetail() {
2301
- detailOpen = false;
2302
- document.getElementById('detail-overlay').classList.remove('open');
2303
- document.querySelectorAll('.msg-line.selected').forEach(function(el) { el.classList.remove('selected'); });
2304
- }
2305
-
2306
- function renderDetail() {
2307
- if (!detailMsg) return;
2308
- var msg = detailMsg;
2309
- var entry = detailEntry;
2310
-
2311
- // Title
2312
- var cat = classifyMessageRole(msg);
2313
- var meta = CATEGORY_META[cat] || { label: cat, color: '#4b5563' };
2314
- var titleText = meta.label;
2315
-
2316
- // Enrich title for tool_use/tool_result
2317
- if (msg.contentBlocks) {
2318
- for (var i = 0; i < msg.contentBlocks.length; i++) {
2319
- var b = msg.contentBlocks[i];
2320
- if (b.type === 'tool_use') { titleText = 'Tool Call — ' + (b.name || 'unknown'); break; }
2321
- if (b.type === 'tool_result') {
2322
- var tn = (b.tool_use_id && detailToolNameMap[b.tool_use_id]) || null;
2323
- titleText = 'Tool Result' + (tn ? ' — ' + tn : '');
2324
- break;
2325
- }
2326
- if (b.type === 'thinking') { titleText = 'Thinking'; break; }
2327
- }
2328
- }
2329
-
2330
- document.getElementById('detail-title').textContent = titleText;
2331
- document.getElementById('detail-title').style.color = catTextColor(cat);
2332
-
2333
- // Subtitle
2334
- var tokPct = entry && entry.contextInfo.totalTokens > 0
2335
- ? ((msg.tokens || 0) / entry.contextInfo.totalTokens * 100).toFixed(1)
2336
- : '0';
2337
- var subParts = [
2338
- '<span>Role: <b style="color:var(--text-secondary);">' + esc(msg.role) + '</b></span>',
2339
- '<span>' + fmtK(msg.tokens || 0) + ' tokens</span>',
2340
- '<span>' + tokPct + '% of context</span>',
2341
- ];
2342
- if (detailMsgList.length > 0) {
2343
- subParts.push('<span>Message ' + (detailMsgIndex + 1) + ' of ' + detailMsgList.length + '</span>');
2344
- }
2345
- document.getElementById('detail-subtitle').innerHTML = subParts.join('');
2346
-
2347
- // Tabs
2348
- document.querySelectorAll('#detail-tabs .detail-tab').forEach(function(el) {
2349
- el.classList.toggle('active', el.getAttribute('data-dtab') === detailTab);
2350
- });
2351
-
2352
- // Nav buttons
2353
- document.getElementById('detail-prev').disabled = detailMsgIndex <= 0;
2354
- document.getElementById('detail-next').disabled = detailMsgIndex >= detailMsgList.length - 1;
2355
-
2356
- // Content
2357
- var contentEl = document.getElementById('detail-content');
2358
- if (detailTab === 'rendered') {
2359
- contentEl.innerHTML = renderDetailRendered(msg);
2360
- } else {
2361
- contentEl.innerHTML = '<pre class="detail-pre">' + highlightJSON(JSON.stringify(msgToRawObject(msg), null, 2)) + '</pre>';
2362
- }
2363
- contentEl.scrollTop = 0;
2364
-
2365
- // Attach "show more" handler
2366
- var showMoreEl = contentEl.querySelector('.detail-truncated');
2367
- if (showMoreEl) {
2368
- showMoreEl.addEventListener('click', function() {
2369
- detailShowAll = true;
2370
- renderDetail();
2371
- });
2372
- }
2373
-
2374
- // Metadata
2375
- renderDetailMeta(msg, entry);
2376
- }
2377
-
2378
- function renderMinimalMarkdown(text) {
2379
- if (!text) return '';
2380
- var lines = text.split('\n');
2381
- var out = [];
2382
- var inCodeBlock = false;
2383
- var codeLines = [];
2384
-
2385
- for (var i = 0; i < lines.length; i++) {
2386
- var line = lines[i];
2387
- // Code block toggle
2388
- if (line.trimStart().startsWith('```')) {
2389
- if (inCodeBlock) {
2390
- out.push('<pre><code>' + escPre(codeLines.join('\n')) + '</code></pre>');
2391
- codeLines = [];
2392
- inCodeBlock = false;
2393
- } else {
2394
- inCodeBlock = true;
2395
- }
2396
- continue;
2397
- }
2398
- if (inCodeBlock) { codeLines.push(line); continue; }
2399
- // Headings
2400
- var hMatch = line.match(/^(#{1,3})\s+(.+)$/);
2401
- if (hMatch) {
2402
- var level = hMatch[1].length;
2403
- out.push('<h' + level + '>' + inlineMarkdown(hMatch[2]) + '</h' + level + '>');
2404
- continue;
2405
- }
2406
- // List items
2407
- if (/^\s*[-*]\s+/.test(line)) {
2408
- // Collect consecutive list items
2409
- var items = [];
2410
- while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
2411
- items.push('<li>' + inlineMarkdown(lines[i].replace(/^\s*[-*]\s+/, '')) + '</li>');
2412
- i++;
2413
- }
2414
- i--; // back up one since the for loop will increment
2415
- out.push('<ul>' + items.join('') + '</ul>');
2416
- continue;
2417
- }
2418
- // Empty line
2419
- if (line.trim() === '') { continue; }
2420
- // Regular paragraph
2421
- out.push('<p>' + inlineMarkdown(line) + '</p>');
2422
- }
2423
- // Close unclosed code block
2424
- if (inCodeBlock && codeLines.length > 0) {
2425
- out.push('<pre><code>' + escPre(codeLines.join('\n')) + '</code></pre>');
2426
- }
2427
- return '<div class="md-rendered">' + out.join('') + '</div>';
2428
- }
2429
-
2430
- function inlineMarkdown(text) {
2431
- var s = esc(text);
2432
- // Inline code
2433
- s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
2434
- // Bold
2435
- s = s.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
2436
- return s;
2437
- }
2438
-
2439
- function renderToolUseCompact(name, input) {
2440
- var html = '<div class="detail-block-label tool-use">tool_use: ' + esc(name || '?') + '</div>';
2441
- if (!input || typeof input !== 'object') {
2442
- html += '<pre class="detail-pre">' + highlightJSON(JSON.stringify(input || {}, null, 2)) + '</pre>';
2443
- return html;
2444
- }
2445
- var keys = Object.keys(input);
2446
- if (keys.length === 0) {
2447
- html += '<pre class="detail-pre" style="color:var(--text-muted);">{}</pre>';
2448
- return html;
2449
- }
2450
- html += '<ul class="tool-params">';
2451
- keys.forEach(function(k) {
2452
- var val = input[k];
2453
- var valStr;
2454
- if (typeof val === 'string') {
2455
- valStr = val.length > 200 ? escPre(val.slice(0, 200)) + '\u2026' : escPre(val);
2456
- } else {
2457
- var js = JSON.stringify(val, null, 2);
2458
- valStr = js.length > 200 ? escPre(js.slice(0, 200)) + '\u2026' : escPre(js);
2459
- }
2460
- html += '<li><span class="tool-param-key">' + esc(k) + '</span><span class="tool-param-val">' + valStr + '</span></li>';
2461
- });
2462
- html += '</ul>';
2463
- return html;
2464
- }
2465
-
2466
- function renderDetailRendered(msg) {
2467
- var blocks = msg.contentBlocks;
2468
- if (blocks && Array.isArray(blocks) && blocks.length > 0) {
2469
- var parts = [];
2470
- blocks.forEach(function(b, idx) {
2471
- if (idx > 0) parts.push('<hr class="detail-block-sep">');
2472
- if (b.type === 'tool_use') {
2473
- parts.push(renderToolUseCompact(b.name, b.input));
2474
- } else if (b.type === 'tool_result') {
2475
- var tn = (b.tool_use_id && detailToolNameMap[b.tool_use_id]) || null;
2476
- parts.push('<div class="detail-block-label tool-result">tool_result' + (tn ? ': ' + esc(tn) : '') + '</div>');
2477
- var rc = typeof b.content === 'string' ? b.content : JSON.stringify(b.content, null, 2);
2478
- if (!detailShowAll && rc.length > detailMaxChars) rc = rc.slice(0, detailMaxChars);
2479
- parts.push(renderHighlightedContent(rc));
2480
- } else if (b.type === 'thinking') {
2481
- parts.push('<div class="detail-block-label thinking">thinking</div>');
2482
- var thinkText = b.thinking || b.text || '';
2483
- if (!detailShowAll && thinkText.length > detailMaxChars) thinkText = thinkText.slice(0, detailMaxChars);
2484
- parts.push(renderMinimalMarkdown(thinkText));
2485
- } else if (b.type === 'text' || b.type === 'input_text') {
2486
- parts.push('<div class="detail-block-label text">' + esc(b.type) + '</div>');
2487
- var blockText = b.text || '';
2488
- if (!detailShowAll && blockText.length > detailMaxChars) blockText = blockText.slice(0, detailMaxChars);
2489
- parts.push(renderMinimalMarkdown(blockText));
2490
- } else if (b.type === 'image') {
2491
- parts.push('<div class="detail-block-label image">image</div>');
2492
- parts.push('<pre class="detail-pre" style="color:var(--text-muted);">[Image content — ' + (b.source ? 'base64 encoded' : 'reference') + ']</pre>');
2493
- } else {
2494
- parts.push('<pre class="detail-pre">' + highlightJSON(JSON.stringify(b, null, 2)) + '</pre>');
2495
- }
2496
- });
2497
- return parts.join('');
2498
- }
2499
-
2500
- // Fallback: raw content string
2501
- return renderHighlightedContent(msg.content || '');
2502
- }
2503
-
2504
- function renderHighlightedContent(text) {
2505
- if (!text) return '<pre class="detail-pre" style="color:var(--text-muted);">[empty]</pre>';
2506
- var fullLen = text.length;
2507
- var charTruncated = false;
2508
- if (!detailShowAll && text.length > detailMaxChars) {
2509
- charTruncated = true;
2510
- text = text.slice(0, detailMaxChars);
2511
- }
2512
- var lines = text.split('\n');
2513
- var lineTruncated = false;
2514
- if (!detailShowAll && lines.length > detailMaxLines) {
2515
- lineTruncated = true;
2516
- lines = lines.slice(0, detailMaxLines);
2517
- }
2518
- var truncated = charTruncated || lineTruncated;
2519
- // Detect content type — just check first char, skip JSON.parse
2520
- var trimmed = text.trimStart();
2521
- var html;
2522
- if (trimmed.charAt(0) === '{' || trimmed.charAt(0) === '[') {
2523
- html = '<pre class="detail-pre">' + highlightJSON(lines.join('\n')) + '</pre>';
2524
- } else if (isDiffContent(trimmed)) {
2525
- html = '<pre class="detail-pre">' + highlightDiff(lines) + '</pre>';
2526
- } else {
2527
- html = '<pre class="detail-pre">' + highlightAuto(lines) + '</pre>';
2528
- }
2529
- if (truncated) {
2530
- var info = charTruncated
2531
- ? '\u25BE Showing first ' + Math.round(detailMaxChars / 1000) + 'K of ' + Math.round(fullLen / 1000) + 'K chars — click to show all'
2532
- : '\u25BE Showing first ' + detailMaxLines + ' lines — click to show all';
2533
- html += '<div class="detail-truncated">' + info + '</div>';
2534
- }
2535
- return html;
2536
- }
2537
-
2538
- function isDiffContent(text) {
2539
- return text.startsWith('diff --git') || text.startsWith('---') || text.startsWith('@@') ||
2540
- (text.indexOf('\n+') !== -1 && text.indexOf('\n-') !== -1 && text.indexOf('\n@@') !== -1);
2541
- }
2542
-
2543
- // --- Syntax highlighting ---
2544
-
2545
- // Escape for use inside <pre> — preserves " so JSON regexes work
2546
- function escPre(s) { return s ? String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') : ''; }
2547
-
2548
- function highlightJSON(str) {
2549
- return escPre(str)
2550
- .replace(/"([^"\\]*(\\.[^"\\]*)*)"\s*:/g, '<span class="syn-key">"$1"</span>:')
2551
- .replace(/:\s*"([^"\\]*(\\.[^"\\]*)*)"/g, ': <span class="syn-str">"$1"</span>')
2552
- .replace(/:\s*(-?\d+\.?\d*([eE][+-]?\d+)?)/g, ': <span class="syn-num">$1</span>')
2553
- .replace(/:\s*(true|false)/g, ': <span class="syn-bool">$1</span>')
2554
- .replace(/:\s*(null)/g, ': <span class="syn-null">$1</span>')
2555
- .replace(/[{}[\]]/g, '<span class="syn-brace">$&</span>');
2556
- }
2557
-
2558
- function highlightDiff(lines) {
2559
- return lines.map(function(line) {
2560
- var el = escPre(line);
2561
- if (line.startsWith('+++') || line.startsWith('---')) return '<span class="syn-diff-file">' + el + '</span>';
2562
- if (line.startsWith('@@')) return '<span class="syn-diff-hunk">' + el + '</span>';
2563
- if (line.startsWith('+')) return '<span class="syn-diff-add">' + el + '</span>';
2564
- if (line.startsWith('-')) return '<span class="syn-diff-del">' + el + '</span>';
2565
- if (line.startsWith('diff ')) return '<span class="syn-diff-file">' + el + '</span>';
2566
- return el;
2567
- }).join('\n');
2568
- }
2569
-
2570
- function highlightAuto(lines) {
2571
- return lines.map(function(line) {
2572
- var el = escPre(line);
2573
- // Shell prompt detection
2574
- if (/^\$\s/.test(line) || /^>\s/.test(line)) return '<span class="syn-prompt">' + el + '</span>';
2575
- // Comments
2576
- if (/^\s*#/.test(line)) return '<span class="syn-comment">' + el + '</span>';
2577
- return el;
2578
- }).join('\n');
2579
- }
2580
-
2581
- function msgToRawObject(msg) {
2582
- var obj = { role: msg.role };
2583
- if (msg.contentBlocks && msg.contentBlocks.length > 0) {
2584
- obj.content = msg.contentBlocks;
2585
- } else {
2586
- // Try to parse content if it's JSON
2587
- try {
2588
- obj.content = JSON.parse(msg.content);
2589
- } catch(e) {
2590
- obj.content = msg.content;
2591
- }
2592
- }
2593
- obj._tokens = msg.tokens;
2594
- return obj;
2595
- }
2596
-
2597
- function renderDetailMeta(msg, entry) {
2598
- var metaEl = document.getElementById('detail-meta');
2599
- var pairs = [];
2600
- pairs.push(['Role', msg.role]);
2601
- pairs.push(['Tokens', fmtK(msg.tokens || 0)]);
2602
- if (entry) {
2603
- var pct = entry.contextInfo.totalTokens > 0
2604
- ? ((msg.tokens || 0) / entry.contextInfo.totalTokens * 100).toFixed(1) + '%'
2605
- : '0%';
2606
- pairs.push(['% of context', pct]);
2607
- pairs.push(['Model', entry.contextInfo.model || '?']);
2608
- }
2609
-
2610
- // tool_use_id / tool name
2611
- if (msg.contentBlocks) {
2612
- msg.contentBlocks.forEach(function(b) {
2613
- if (b.type === 'tool_use') {
2614
- pairs.push(['tool_use_id', b.id || '?']);
2615
- pairs.push(['tool_name', b.name || '?']);
2616
- }
2617
- if (b.type === 'tool_result' && b.tool_use_id) {
2618
- pairs.push(['tool_use_id', b.tool_use_id]);
2619
- var tn = detailToolNameMap[b.tool_use_id];
2620
- if (tn) pairs.push(['tool_name', tn]);
2621
- }
2622
- });
2623
- }
2624
-
2625
- if (entry) {
2626
- pairs.push(['Position', 'msg ' + (detailMsgIndex + 1) + ' of ' + detailMsgList.length]);
2627
- }
2628
-
2629
- var html = '';
2630
- pairs.forEach(function(p) {
2631
- html += '<span class="detail-meta-label">' + esc(p[0]) + '</span>';
2632
- html += '<span class="detail-meta-val" title="' + esc(p[1]) + '">' + esc(String(p[1])) + '</span>';
2633
- });
2634
- metaEl.innerHTML = html;
2635
- }
2636
-
2637
- // --- Detail panel event handlers ---
2638
-
2639
- // Close
2640
- document.getElementById('detail-close').addEventListener('click', closeDetail);
2641
- document.getElementById('detail-backdrop').addEventListener('click', closeDetail);
2642
- document.addEventListener('keydown', function(e) {
2643
- if (!detailOpen) return;
2644
- if (e.key === 'Escape') { closeDetail(); e.preventDefault(); }
2645
- if (e.key === 'ArrowUp' || e.key === 'k') { navigateDetail(-1); e.preventDefault(); }
2646
- if (e.key === 'ArrowDown' || e.key === 'j') { navigateDetail(1); e.preventDefault(); }
2647
- });
2648
-
2649
- // Tab switching
2650
- document.querySelectorAll('#detail-tabs .detail-tab').forEach(function(el) {
2651
- el.addEventListener('click', function() {
2652
- detailTab = el.getAttribute('data-dtab');
2653
- renderDetail();
2654
- });
2655
- });
2656
-
2657
- // Nav
2658
- document.getElementById('detail-prev').addEventListener('click', function() { navigateDetail(-1); });
2659
- document.getElementById('detail-next').addEventListener('click', function() { navigateDetail(1); });
2660
-
2661
- function navigateDetail(dir) {
2662
- var newIdx = detailMsgIndex + dir;
2663
- if (newIdx < 0 || newIdx >= detailMsgList.length) return;
2664
- detailMsgIndex = newIdx;
2665
- detailMsg = detailMsgList[newIdx];
2666
- detailShowAll = false;
2667
- renderDetail();
2668
- // Update selected line
2669
- document.querySelectorAll('.msg-line').forEach(function(el, i) {
2670
- el.classList.toggle('selected', i === newIdx);
2671
- });
2672
- // Scroll selected line into view in main panel
2673
- var selectedLine = document.querySelector('.msg-line.selected');
2674
- if (selectedLine) selectedLine.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
2675
- }
2676
-
2677
- // Copy
2678
- document.getElementById('detail-copy').addEventListener('click', function() {
2679
- if (!detailMsg) return;
2680
- var text;
2681
- if (detailTab === 'raw') {
2682
- text = JSON.stringify(msgToRawObject(detailMsg), null, 2);
2683
- } else {
2684
- // Copy rendered text content
2685
- text = extractFullText(detailMsg);
2686
- }
2687
- navigator.clipboard.writeText(text).then(function() {
2688
- var btn = document.getElementById('detail-copy');
2689
- btn.textContent = 'Copied!';
2690
- btn.classList.add('copied');
2691
- setTimeout(function() { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500);
2692
- });
2693
- });
2694
-
2695
- function extractFullText(msg) {
2696
- if (msg.contentBlocks && msg.contentBlocks.length > 0) {
2697
- return msg.contentBlocks.map(function(b) {
2698
- if (b.type === 'tool_use') return b.name + '(' + JSON.stringify(b.input, null, 2) + ')';
2699
- if (b.type === 'tool_result') return typeof b.content === 'string' ? b.content : JSON.stringify(b.content, null, 2);
2700
- if (b.type === 'thinking') return b.thinking || b.text || '';
2701
- if (b.type === 'text' || b.type === 'input_text') return b.text || '';
2702
- return JSON.stringify(b, null, 2);
2703
- }).join('\n\n');
2704
- }
2705
- return msg.content || '';
2706
- }
2707
-
2708
- // Resize handle
2709
- (function() {
2710
- var handle = document.getElementById('detail-resize');
2711
- var panel = document.getElementById('detail-panel');
2712
- var dragging = false;
2713
- var startX, startW;
2714
-
2715
- handle.addEventListener('mousedown', function(e) {
2716
- dragging = true;
2717
- startX = e.clientX;
2718
- startW = panel.offsetWidth;
2719
- handle.classList.add('dragging');
2720
- document.body.style.cursor = 'col-resize';
2721
- document.body.style.userSelect = 'none';
2722
- e.preventDefault();
2723
- });
2724
-
2725
- document.addEventListener('mousemove', function(e) {
2726
- if (!dragging) return;
2727
- var delta = startX - e.clientX;
2728
- var newW = Math.max(320, Math.min(window.innerWidth * 0.85, startW + delta));
2729
- panel.style.width = newW + 'px';
2730
- });
2731
-
2732
- document.addEventListener('mouseup', function() {
2733
- if (!dragging) return;
2734
- dragging = false;
2735
- handle.classList.remove('dragging');
2736
- document.body.style.cursor = '';
2737
- document.body.style.userSelect = '';
2738
- });
2739
- })();
2740
-
2741
- // --- Wire up msg-line clicks in attachMainHandlers ---
2742
- var origAttachMainHandlers = attachMainHandlers;
2743
- attachMainHandlers = function() {
2744
- origAttachMainHandlers();
2745
- wireDetailClicks();
2746
- };
2747
-
2748
- function wireDetailClicks() {
2749
- var entry = null;
2750
- var session = getSelectedSession();
2751
- if (session) entry = getSelectedEntry(session);
2752
- if (!entry) return;
2753
-
2754
- var msgs = (entry.contextInfo && entry.contextInfo.messages) || [];
2755
- if (msgs.length === 0) return;
2756
-
2757
- // Build tool name map
2758
- var toolMap = {};
2759
- msgs.forEach(function(m) {
2760
- if (m.contentBlocks) {
2761
- m.contentBlocks.forEach(function(b) {
2762
- if (b.type === 'tool_use' && b.id && b.name) toolMap[b.id] = b.name;
2763
- });
2764
- }
2765
- });
2766
-
2767
- // Build flat list of messages matching visible msg-lines
2768
- // msg-lines are rendered by category in catOrder — we need to match them
2769
- var allMsgLines = document.querySelectorAll('.msg-line');
2770
- var flatMsgs = [];
2771
-
2772
- // Reproduce the category ordering from renderMessageCategories
2773
- var categories = {};
2774
- var catOrder = ['tool_results', 'system_injections', 'tool_calls', 'thinking', 'assistant_text', 'user_text'];
2775
- msgs.forEach(function(m, i) {
2776
- var cat = classifyMessageRole(m);
2777
- if (!categories[cat]) categories[cat] = [];
2778
- categories[cat].push({ msg: m, origIdx: i });
2779
- });
2780
- for (var cat in categories) {
2781
- if (catOrder.indexOf(cat) === -1) catOrder.push(cat);
2782
- }
2783
- catOrder.forEach(function(cat) {
2784
- if (!categories[cat]) return;
2785
- categories[cat].forEach(function(item) {
2786
- flatMsgs.push(item.msg);
2787
- });
2788
- });
2789
-
2790
- allMsgLines.forEach(function(el, lineIdx) {
2791
- if (lineIdx >= flatMsgs.length) return;
2792
- el.addEventListener('click', function(e) {
2793
- e.stopPropagation();
2794
- openDetail(flatMsgs[lineIdx], flatMsgs, lineIdx, entry, toolMap);
2795
- });
2796
- });
2797
- }
2798
-
2799
- fetchAndRender();
2800
- setInterval(fetchAndRender, 2000);
2801
- })();
2802
- </script>
2803
- </body>
2804
- </html>