htmlgraph 0.24.2__py3-none-any.whl → 0.25.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2115 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +783 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +570 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3315 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1334 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/__init__.py +8 -0
  68. htmlgraph/hooks/bootstrap.py +169 -0
  69. htmlgraph/hooks/context.py +271 -0
  70. htmlgraph/hooks/drift_handler.py +521 -0
  71. htmlgraph/hooks/event_tracker.py +405 -15
  72. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  73. htmlgraph/hooks/pretooluse.py +476 -6
  74. htmlgraph/hooks/prompt_analyzer.py +648 -0
  75. htmlgraph/hooks/session_handler.py +583 -0
  76. htmlgraph/hooks/state_manager.py +501 -0
  77. htmlgraph/hooks/subagent_stop.py +309 -0
  78. htmlgraph/hooks/task_enforcer.py +39 -0
  79. htmlgraph/models.py +111 -15
  80. htmlgraph/operations/fastapi_server.py +230 -0
  81. htmlgraph/orchestration/headless_spawner.py +22 -14
  82. htmlgraph/pydantic_models.py +476 -0
  83. htmlgraph/quality_gates.py +350 -0
  84. htmlgraph/repo_hash.py +511 -0
  85. htmlgraph/sdk.py +348 -10
  86. htmlgraph/server.py +194 -0
  87. htmlgraph/session_hooks.py +300 -0
  88. htmlgraph/session_manager.py +131 -1
  89. htmlgraph/session_registry.py +587 -0
  90. htmlgraph/session_state.py +436 -0
  91. htmlgraph/system_prompts.py +449 -0
  92. htmlgraph/templates/orchestration-view.html +350 -0
  93. htmlgraph/track_builder.py +19 -0
  94. htmlgraph/validation.py +115 -0
  95. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
  96. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
  97. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
  98. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
  99. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  100. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  101. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  102. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
  103. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,783 @@
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>{{ title }} - HtmlGraph</title>
7
+ <script src="/static/htmx.min.js"></script>
8
+ <link rel="stylesheet" href="/static/style.css">
9
+ </head>
10
+ <body class="dark-theme">
11
+ <div class="dashboard-container">
12
+ <!-- Header -->
13
+ <header class="dashboard-header">
14
+ <div class="header-content">
15
+ <h1 class="logo">
16
+ <span class="logo-icon">⚡</span>
17
+ HtmlGraph Dashboard
18
+ </h1>
19
+ <div class="header-stats">
20
+ <div class="stat-badge">
21
+ <span class="stat-label">Events</span>
22
+ <span class="stat-value" id="event-count">0</span>
23
+ </div>
24
+ <div class="stat-badge">
25
+ <span class="stat-label">Agents</span>
26
+ <span class="stat-value" id="agent-count">0</span>
27
+ </div>
28
+ <div class="stat-badge">
29
+ <span class="stat-label">Sessions</span>
30
+ <span class="stat-value" id="session-count">0</span>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </header>
35
+
36
+ <!-- Navigation Tabs -->
37
+ <nav class="tabs-navigation">
38
+ <button class="tab-button active"
39
+ hx-get="/views/activity-feed"
40
+ hx-target="#content-area"
41
+ hx-trigger="click"
42
+ data-tab="activity">
43
+ <span class="tab-icon">📋</span>
44
+ Activity Feed
45
+ </button>
46
+ <button class="tab-button"
47
+ hx-get="/views/orchestration"
48
+ hx-target="#content-area"
49
+ hx-trigger="click"
50
+ data-tab="orchestration">
51
+ <span class="tab-icon">🔗</span>
52
+ Orchestration
53
+ </button>
54
+ <button class="tab-button"
55
+ hx-get="/views/features"
56
+ hx-target="#content-area"
57
+ hx-trigger="click"
58
+ data-tab="features">
59
+ <span class="tab-icon">🎯</span>
60
+ Features
61
+ </button>
62
+ <button class="tab-button"
63
+ hx-get="/views/agents"
64
+ hx-target="#content-area"
65
+ hx-trigger="click"
66
+ data-tab="agents">
67
+ <span class="tab-icon">🤖</span>
68
+ Agents
69
+ </button>
70
+ <button class="tab-button"
71
+ hx-get="/views/metrics"
72
+ hx-target="#content-area"
73
+ hx-trigger="click"
74
+ data-tab="metrics">
75
+ <span class="tab-icon">📊</span>
76
+ Metrics
77
+ </button>
78
+ <button class="tab-button"
79
+ hx-get="/views/spawners"
80
+ hx-target="#content-area"
81
+ hx-trigger="click"
82
+ data-tab="spawners">
83
+ <span class="tab-icon">🚀</span>
84
+ Spawners
85
+ </button>
86
+ </nav>
87
+
88
+ <!-- Content Area -->
89
+ <main class="content-area" id="content-area">
90
+ <div class="loading-indicator">
91
+ <div class="spinner"></div>
92
+ <p>Loading dashboard...</p>
93
+ </div>
94
+ </main>
95
+ </div>
96
+
97
+ <!-- WebSocket for live updates -->
98
+ <script>
99
+ let eventCount = 0;
100
+ let agentSet = new Set();
101
+ let sessionCount = 0;
102
+ let processedEventIds = new Set();
103
+
104
+ // Load initial stats from server
105
+ async function loadInitialStats() {
106
+ try {
107
+ const response = await fetch('/api/initial-stats');
108
+ const data = await response.json();
109
+
110
+ // Update header stats
111
+ eventCount = data.total_events || 0;
112
+ sessionCount = data.total_sessions || 0;
113
+
114
+ // Update agent set from database
115
+ if (data.agents) {
116
+ data.agents.forEach(agent => agentSet.add(agent));
117
+ }
118
+
119
+ // Update UI
120
+ document.getElementById('event-count').textContent = eventCount;
121
+ document.getElementById('agent-count').textContent = agentSet.size;
122
+ document.getElementById('session-count').textContent = sessionCount;
123
+
124
+ console.log('Initial stats loaded:', data);
125
+ } catch (error) {
126
+ console.error('Failed to load initial stats:', error);
127
+ }
128
+ }
129
+
130
+ // Initialize dashboard on load
131
+ document.addEventListener('DOMContentLoaded', function() {
132
+ // Load initial stats
133
+ loadInitialStats();
134
+
135
+ // Load initial activity feed
136
+ htmx.ajax('GET', '/views/activity-feed', {target: '#content-area'});
137
+
138
+ // Connect WebSocket for real-time updates
139
+ connectWebSocket();
140
+ });
141
+
142
+ // Convert timestamps after HTMX loads Activity Feed
143
+ document.body.addEventListener('htmx:afterSettle', function(evt) {
144
+ if (evt.detail.target.id === 'content-area') {
145
+ // Activity Feed has been loaded, convert timestamps to local timezone
146
+ if (typeof convertTimestampsToLocal === 'function') {
147
+ convertTimestampsToLocal();
148
+ }
149
+ }
150
+ });
151
+
152
+ function connectWebSocket() {
153
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
154
+ const ws = new WebSocket(wsProtocol + '//' + window.location.host + '/ws/events');
155
+
156
+ ws.onopen = function(event) {
157
+ console.log('WebSocket connected for real-time events');
158
+ updateWebSocketStatus(true);
159
+ };
160
+
161
+ ws.onmessage = function(event) {
162
+ try {
163
+ const data = JSON.parse(event.data);
164
+
165
+ if (data.type === 'event') {
166
+ // Prevent duplicate event insertions
167
+ if (processedEventIds.has(data.event_id)) {
168
+ return;
169
+ }
170
+ processedEventIds.add(data.event_id);
171
+
172
+ // Update stats
173
+ eventCount++;
174
+ if (data.agent_id) {
175
+ agentSet.add(data.agent_id);
176
+ }
177
+
178
+ // Update header
179
+ document.getElementById('event-count').textContent = eventCount;
180
+ document.getElementById('agent-count').textContent = agentSet.size;
181
+
182
+ // Add animation class to event count badge
183
+ const badge = document.getElementById('event-count').parentElement;
184
+ badge.classList.add('pulse');
185
+ setTimeout(() => badge.classList.remove('pulse'), 500);
186
+
187
+ // Insert new event into Activity Feed if visible
188
+ insertNewEventIntoActivityFeed(data);
189
+ }
190
+ } catch (e) {
191
+ console.error('WebSocket message error:', e);
192
+ }
193
+ };
194
+
195
+ ws.onerror = function(event) {
196
+ console.error('WebSocket error:', event);
197
+ updateWebSocketStatus(false);
198
+ };
199
+
200
+ ws.onclose = function(event) {
201
+ console.log('WebSocket disconnected, reconnecting in 3s...');
202
+ updateWebSocketStatus(false);
203
+ setTimeout(connectWebSocket, 3000);
204
+ };
205
+ }
206
+
207
+ function updateWebSocketStatus(isConnected) {
208
+ // Update live update indicator in activity feed
209
+ const indicator = document.querySelector('.auto-refresh-indicator');
210
+ if (indicator) {
211
+ const dot = indicator.querySelector('.refresh-dot');
212
+ if (isConnected) {
213
+ dot.style.backgroundColor = '#10b981';
214
+ indicator.style.opacity = '1';
215
+ } else {
216
+ dot.style.backgroundColor = '#ef4444';
217
+ indicator.style.opacity = '0.6';
218
+ }
219
+ }
220
+ }
221
+
222
+ function insertNewEventIntoActivityFeed(eventData) {
223
+ // Check if activity feed is currently displayed
224
+ const activityFeed = document.querySelector('.activity-feed-view');
225
+ if (!activityFeed) {
226
+ // Activity feed not visible, skip insertion
227
+ return;
228
+ }
229
+
230
+ const activityList = activityFeed.querySelector('.activity-list');
231
+ if (!activityList) {
232
+ return;
233
+ }
234
+
235
+ // Check if there's an empty state message
236
+ const emptyState = activityList.querySelector('.empty-state');
237
+ if (emptyState) {
238
+ // Replace empty state with table
239
+ emptyState.remove();
240
+ // Create table if it doesn't exist
241
+ if (!activityList.querySelector('.activity-table')) {
242
+ const table = `
243
+ <table class="activity-table">
244
+ <thead>
245
+ <tr>
246
+ <th class="col-timestamp">Timestamp</th>
247
+ <th class="col-agent">Agent</th>
248
+ <th class="col-tool">Tool</th>
249
+ <th class="col-input">Input</th>
250
+ <th class="col-output">Output</th>
251
+ <th class="col-status">Status</th>
252
+ <th class="col-id">ID</th>
253
+ </tr>
254
+ </thead>
255
+ <tbody></tbody>
256
+ </table>
257
+ `;
258
+ activityList.insertAdjacentHTML('afterbegin', table);
259
+ }
260
+ }
261
+
262
+ // Get or create tbody
263
+ const table = activityList.querySelector('.activity-table');
264
+ if (!table) {
265
+ return;
266
+ }
267
+
268
+ const tbody = table.querySelector('tbody');
269
+ if (!tbody) {
270
+ return;
271
+ }
272
+
273
+ // Create new table row HTML
274
+ const eventRow = createActivityRowHTML(eventData);
275
+
276
+ // Handle hierarchical placement
277
+ if (eventData.parent_event_id) {
278
+ // Try to find parent row
279
+ const parentRow = tbody.querySelector(`tr[data-event-id="${eventData.parent_event_id}"]`);
280
+ if (parentRow) {
281
+ // Insert after parent row
282
+ parentRow.insertAdjacentHTML('afterend', eventRow);
283
+ highlightRow(tbody.querySelector(`tr[data-event-id="${eventData.event_id}"]`));
284
+ return;
285
+ }
286
+ }
287
+
288
+ // Default: Insert at the top of tbody
289
+ const firstRow = tbody.querySelector('tr');
290
+ if (firstRow) {
291
+ firstRow.insertAdjacentHTML('beforebegin', eventRow);
292
+ } else {
293
+ tbody.insertAdjacentHTML('afterbegin', eventRow);
294
+ }
295
+
296
+ // Highlight the new row
297
+ highlightRow(tbody.querySelector('tr:first-child'));
298
+
299
+ // Convert new event timestamp to local timezone
300
+ if (typeof convertTimestampsToLocal === 'function') {
301
+ convertTimestampsToLocal();
302
+ }
303
+
304
+ // Keep only last 100 events in feed to prevent memory issues
305
+ const allRows = tbody.querySelectorAll('tr');
306
+ if (allRows.length > 100) {
307
+ // Remove oldest items from bottom
308
+ const itemsToRemove = allRows.length - 100;
309
+ for (let i = 0; i < itemsToRemove; i++) {
310
+ allRows[allRows.length - 1 - i].remove();
311
+ }
312
+ }
313
+ }
314
+
315
+ function highlightRow(row) {
316
+ if (row) {
317
+ row.classList.add('new-event-highlight');
318
+ // Remove highlight after animation
319
+ setTimeout(() => {
320
+ row.classList.remove('new-event-highlight');
321
+ }, 2000);
322
+ }
323
+ }
324
+
325
+ function createActivityRowHTML(eventData) {
326
+ // Determine event type emoji
327
+ let eventEmoji = '📋';
328
+ if (eventData.event_type === 'delegation') {
329
+ eventEmoji = '🔗';
330
+ } else if (eventData.event_type === 'tool_call') {
331
+ eventEmoji = '🔨';
332
+ } else if (eventData.event_type === 'completion') {
333
+ eventEmoji = '🎉';
334
+ } else if (eventData.event_type === 'tool_result') {
335
+ eventEmoji = '✅';
336
+ } else if (eventData.event_type === 'error') {
337
+ eventEmoji = '❌';
338
+ }
339
+
340
+ const inputSummary = eventData.input_summary ? eventData.input_summary.substring(0, 100) : '';
341
+ const inputTruncated = eventData.input_summary && eventData.input_summary.length > 100 ? '…' : '';
342
+ const outputSummary = eventData.output_summary ? eventData.output_summary.substring(0, 100) : '';
343
+ const outputTruncated = eventData.output_summary && eventData.output_summary.length > 100 ? '…' : '';
344
+
345
+ const isChild = !!eventData.parent_event_id;
346
+ const rowClass = isChild ? 'child-row' : 'parent-row';
347
+ const indentStyle = isChild ? 'padding-left: 2rem;' : '';
348
+ const borderStyle = isChild ? 'border-left: 4px solid var(--text-muted);' : 'border-left: 4px solid var(--accent);';
349
+
350
+ // Create table row HTML
351
+ const html = `
352
+ <tr class="activity-row ${rowClass} event-${eventData.status || 'pending'}"
353
+ data-event-id="${escapeHtml(eventData.event_id)}"
354
+ style="${borderStyle}">
355
+ <td class="col-timestamp" style="${indentStyle}">
356
+ <span class="event-type-badge" title="${escapeHtml(eventData.event_type)}">
357
+ ${eventEmoji}
358
+ </span>
359
+ <span class="timestamp-text" data-utc-time="${escapeHtml(eventData.timestamp)}">${escapeHtml(eventData.timestamp)}</span>
360
+ </td>
361
+ <td class="col-agent">
362
+ <span class="agent-badge agent-${escapeHtml(eventData.agent_id.toLowerCase())}">${escapeHtml(eventData.agent_id)}</span>
363
+ ${isChild ? `<span class="child-indicator" title="Child of ${escapeHtml(eventData.parent_event_id.substring(0, 8))}">↳</span>` : ''}
364
+ </td>
365
+ <td class="col-tool">
366
+ ${eventData.tool_name ? `<code class="tool-name">${escapeHtml(eventData.tool_name)}</code>` : '<span class="text-muted">—</span>'}
367
+ </td>
368
+ <td class="col-input">
369
+ ${inputSummary ? `<span class="truncate" title="${escapeHtml(eventData.input_summary)}">${escapeHtml(inputSummary)}${inputTruncated}</span>` : '<span class="text-muted">—</span>'}
370
+ </td>
371
+ <td class="col-output">
372
+ ${outputSummary ? `<span class="truncate" title="${escapeHtml(eventData.output_summary)}">${escapeHtml(outputSummary)}${outputTruncated}</span>` : '<span class="text-muted">—</span>'}
373
+ </td>
374
+ <td class="col-status">
375
+ <span class="status-badge status-${eventData.status || 'pending'}">${escapeHtml(eventData.status || 'pending')}</span>
376
+ </td>
377
+ <td class="col-id">
378
+ <code class="event-id-code" title="${escapeHtml(eventData.event_id)}">${escapeHtml(eventData.event_id.substring(0, 8))}</code>
379
+ </td>
380
+ </tr>
381
+ `;
382
+ return html;
383
+ }
384
+
385
+ function escapeHtml(text) {
386
+ if (!text) return '';
387
+ const div = document.createElement('div');
388
+ div.textContent = text;
389
+ return div.innerHTML;
390
+ }
391
+
392
+ // Convert UTC timestamps to local timezone
393
+ function convertTimestampsToLocal() {
394
+ const timestampElements = document.querySelectorAll('[data-utc-time]');
395
+ console.log('convertTimestampsToLocal() called, found', timestampElements.length, 'timestamps to convert');
396
+ timestampElements.forEach(element => {
397
+ const utcTime = element.getAttribute('data-utc-time');
398
+ if (utcTime) {
399
+ try {
400
+ // Parse ISO 8601 UTC time - convert naive datetime to UTC format
401
+ // Input: "2026-01-06 18:01:19" → "2026-01-06T18:01:19Z"
402
+ const date = new Date(utcTime.replace(' ', 'T') + 'Z');
403
+ // Convert to local timezone using Intl API for best compatibility
404
+ const localTime = new Intl.DateTimeFormat('en-US', {
405
+ year: 'numeric',
406
+ month: '2-digit',
407
+ day: '2-digit',
408
+ hour: '2-digit',
409
+ minute: '2-digit',
410
+ second: '2-digit',
411
+ hour12: false,
412
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
413
+ }).format(date);
414
+ // Replace the displayed timestamp with local time
415
+ element.textContent = localTime;
416
+ // Add title attribute to show full ISO format on hover
417
+ element.setAttribute('title', `UTC: ${utcTime} | Local: ${localTime}`);
418
+ } catch (err) {
419
+ console.warn('Failed to convert timestamp:', utcTime, err);
420
+ }
421
+ }
422
+ });
423
+ }
424
+
425
+ // Handle tab switching
426
+ document.querySelectorAll('.tab-button').forEach(button => {
427
+ button.addEventListener('click', function() {
428
+ // Update active state
429
+ document.querySelectorAll('.tab-button').forEach(b => {
430
+ b.classList.remove('active');
431
+ });
432
+ this.classList.add('active');
433
+ });
434
+ });
435
+
436
+ // ============================================
437
+ // Jaeger-Style Trace Interactivity Functions
438
+ // (Must be global for HTMX-loaded partials)
439
+ // ============================================
440
+
441
+ // Toggle expand/collapse with animation
442
+ function toggleTrace(id, event) {
443
+ if (event) event.stopPropagation();
444
+
445
+ const parentRow = document.querySelector(`[data-id="${id}"]`);
446
+ const children = document.querySelectorAll(`[data-parent="${id}"]`);
447
+ const toggle = document.querySelector(`[data-id="${id}"] .expand-toggle`);
448
+
449
+ children.forEach(child => {
450
+ child.classList.toggle('collapsed');
451
+ });
452
+
453
+ if (toggle) {
454
+ toggle.classList.toggle('expanded');
455
+ }
456
+
457
+ // Toggle expanded class on parent row for visual styling
458
+ if (parentRow) {
459
+ parentRow.classList.toggle('expanded');
460
+ }
461
+
462
+ // Update breadcrumbs if drilling into a trace
463
+ updateBreadcrumbs(id);
464
+ }
465
+
466
+ // Highlight ancestor path on hover (Jaeger pattern)
467
+ function highlightAncestors(row) {
468
+ clearAncestorHighlight();
469
+
470
+ let parentId = row.dataset.parent;
471
+ while (parentId) {
472
+ const parent = document.querySelector(`[data-id="${parentId}"]`);
473
+ if (parent) {
474
+ parent.classList.add('ancestor-highlight');
475
+ parentId = parent.dataset.parent;
476
+ } else {
477
+ break;
478
+ }
479
+ }
480
+ }
481
+
482
+ // Clear all ancestor highlights
483
+ function clearAncestorHighlight() {
484
+ document.querySelectorAll('.ancestor-highlight').forEach(el => {
485
+ el.classList.remove('ancestor-highlight');
486
+ });
487
+ }
488
+
489
+ // Expand all traces
490
+ function expandAllTraces() {
491
+ document.querySelectorAll('.child-row').forEach(child => {
492
+ child.classList.remove('collapsed');
493
+ });
494
+ document.querySelectorAll('.expand-toggle').forEach(toggle => {
495
+ toggle.classList.add('expanded');
496
+ });
497
+ document.querySelectorAll('.parent-row.has-children').forEach(parent => {
498
+ parent.classList.add('expanded');
499
+ });
500
+ // Show breadcrumbs when all expanded
501
+ const breadcrumbs = document.getElementById('trace-breadcrumbs');
502
+ if (breadcrumbs) breadcrumbs.style.display = 'flex';
503
+ }
504
+
505
+ // Collapse all traces
506
+ function collapseAllTraces() {
507
+ document.querySelectorAll('.child-row').forEach(child => {
508
+ child.classList.add('collapsed');
509
+ });
510
+ document.querySelectorAll('.expand-toggle').forEach(toggle => {
511
+ toggle.classList.remove('expanded');
512
+ });
513
+ document.querySelectorAll('.parent-row.has-children').forEach(parent => {
514
+ parent.classList.remove('expanded');
515
+ });
516
+ // Hide breadcrumbs when all collapsed
517
+ const breadcrumbs = document.getElementById('trace-breadcrumbs');
518
+ if (breadcrumbs) breadcrumbs.style.display = 'none';
519
+ }
520
+
521
+ // Breadcrumb management
522
+ let breadcrumbStack = ['root'];
523
+
524
+ function updateBreadcrumbs(id) {
525
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
526
+ if (!breadcrumbsContainer) return;
527
+
528
+ const row = document.querySelector(`[data-id="${id}"]`);
529
+ if (!row) return;
530
+
531
+ // Only show breadcrumbs when we have nested navigation
532
+ const depth = parseInt(row.dataset.depth || '0');
533
+ if (depth > 0 || breadcrumbStack.length > 1) {
534
+ breadcrumbsContainer.style.display = 'flex';
535
+ }
536
+
537
+ // Get tool name or operation for breadcrumb label
538
+ const toolName = row.querySelector('.tool-name');
539
+ const label = toolName ? toolName.textContent : `Trace ${id.substring(0, 8)}`;
540
+
541
+ // Add to breadcrumb stack if not already present
542
+ if (!breadcrumbStack.includes(id)) {
543
+ breadcrumbStack.push(id);
544
+
545
+ const separator = document.createElement('span');
546
+ separator.className = 'separator';
547
+ separator.textContent = '>';
548
+
549
+ const crumb = document.createElement('span');
550
+ crumb.className = 'breadcrumb active';
551
+ crumb.dataset.id = id;
552
+ crumb.textContent = label;
553
+ crumb.onclick = () => navigateToBreadcrumb(id);
554
+
555
+ // Remove active class from previous breadcrumbs
556
+ breadcrumbsContainer.querySelectorAll('.breadcrumb').forEach(b => {
557
+ b.classList.remove('active');
558
+ });
559
+
560
+ breadcrumbsContainer.appendChild(separator);
561
+ breadcrumbsContainer.appendChild(crumb);
562
+ }
563
+ }
564
+
565
+ function navigateToBreadcrumb(id) {
566
+ // Find position in stack and remove everything after
567
+ const index = breadcrumbStack.indexOf(id);
568
+ if (index === -1) return;
569
+
570
+ // Remove breadcrumbs after this one
571
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
572
+ if (!breadcrumbsContainer) return;
573
+
574
+ const allCrumbs = breadcrumbsContainer.querySelectorAll('.breadcrumb, .separator');
575
+
576
+ let removing = false;
577
+ allCrumbs.forEach(el => {
578
+ if (removing) {
579
+ el.remove();
580
+ }
581
+ if (el.dataset && el.dataset.id === id) {
582
+ el.classList.add('active');
583
+ removing = true;
584
+ }
585
+ });
586
+
587
+ // Update stack
588
+ breadcrumbStack = breadcrumbStack.slice(0, index + 1);
589
+
590
+ // Hide breadcrumbs if back to root
591
+ if (breadcrumbStack.length <= 1) {
592
+ breadcrumbsContainer.style.display = 'none';
593
+ }
594
+ }
595
+
596
+ function resetBreadcrumbs() {
597
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
598
+ if (!breadcrumbsContainer) return;
599
+
600
+ breadcrumbsContainer.innerHTML = '<span class="breadcrumb" data-id="root" onclick="resetBreadcrumbs()">Session</span>';
601
+ breadcrumbsContainer.style.display = 'none';
602
+ breadcrumbStack = ['root'];
603
+
604
+ // Collapse all traces
605
+ collapseAllTraces();
606
+ }
607
+
608
+ // Initialize timestamps in activity feed after HTMX swap
609
+ function initializeActivityFeedTimestamps() {
610
+ document.querySelectorAll('.timestamp-text[data-utc-time]').forEach(el => {
611
+ const utcTime = el.dataset.utcTime;
612
+ if (utcTime) {
613
+ try {
614
+ const date = new Date(utcTime);
615
+ el.textContent = date.toLocaleTimeString();
616
+ el.title = date.toLocaleString();
617
+ } catch (e) {
618
+ // Keep original text if parsing fails
619
+ }
620
+ }
621
+ });
622
+ }
623
+
624
+ // Re-initialize after HTMX swaps in activity feed
625
+ document.body.addEventListener('htmx:afterSwap', function(evt) {
626
+ if (evt.detail.target && evt.detail.target.id === 'content-area') {
627
+ initializeActivityFeedTimestamps();
628
+ }
629
+ });
630
+
631
+ // ============================================
632
+ // Spawner Activity Filtering
633
+ // ============================================
634
+
635
+ function filterByAgentType(filterType) {
636
+ const turns = document.querySelectorAll('.conversation-turn');
637
+ turns.forEach(turn => {
638
+ const spawnerType = turn.dataset.spawnerType || 'direct';
639
+
640
+ let shouldShow = false;
641
+ if (filterType === 'all') {
642
+ shouldShow = true;
643
+ } else if (filterType === 'direct') {
644
+ shouldShow = spawnerType === 'direct';
645
+ } else if (filterType === 'spawner') {
646
+ shouldShow = spawnerType !== 'direct';
647
+ } else {
648
+ // Specific spawner type (gemini, codex, copilot)
649
+ shouldShow = spawnerType === filterType;
650
+ }
651
+
652
+ if (shouldShow) {
653
+ turn.classList.remove('hidden');
654
+ } else {
655
+ turn.classList.add('hidden');
656
+ }
657
+ });
658
+ }
659
+ </script>
660
+
661
+ <!-- Styles for real-time event highlighting and spawner badges -->
662
+ <style>
663
+ .new-event-highlight {
664
+ animation: highlightPulse 2s ease-out;
665
+ }
666
+
667
+ /* ============================================
668
+ Spawner Badge Styling
669
+ ============================================ */
670
+
671
+ .delegation-arrow {
672
+ color: var(--text-secondary);
673
+ margin: 0 0.5rem;
674
+ font-size: 0.9em;
675
+ font-weight: normal;
676
+ }
677
+
678
+ .spawner-badge {
679
+ display: inline-flex;
680
+ align-items: center;
681
+ gap: 0.25rem;
682
+ padding: 0.25rem 0.6rem;
683
+ border-radius: 4px;
684
+ font-size: 0.85rem;
685
+ font-weight: 500;
686
+ border: 1px solid;
687
+ background: white;
688
+ }
689
+
690
+ .spawner-badge.spawner-gemini {
691
+ background: #e8f5e9;
692
+ color: #2e7d32;
693
+ border-color: #4caf50;
694
+ }
695
+
696
+ .spawner-badge.spawner-codex {
697
+ background: #e3f2fd;
698
+ color: #1565c0;
699
+ border-color: #2196f3;
700
+ }
701
+
702
+ .spawner-badge.spawner-copilot {
703
+ background: #f3e5f5;
704
+ color: #6a1b9a;
705
+ border-color: #9c27b0;
706
+ }
707
+
708
+ .cost-badge {
709
+ font-size: 0.75rem;
710
+ opacity: 0.8;
711
+ margin-left: 0.25rem;
712
+ }
713
+
714
+ /* Activity feed spawner filter */
715
+ .spawner-filter {
716
+ padding: 0.75rem 1rem;
717
+ margin: 0.5rem 0.5rem 1rem 0.5rem;
718
+ border: 1px solid var(--border-subtle);
719
+ border-radius: 4px;
720
+ background: rgba(163, 230, 53, 0.02);
721
+ }
722
+
723
+ .spawner-filter label {
724
+ margin-right: 0.5rem;
725
+ font-weight: 500;
726
+ font-size: 0.85rem;
727
+ }
728
+
729
+ .spawner-filter select {
730
+ padding: 0.4rem 0.6rem;
731
+ border: 1px solid var(--border-subtle);
732
+ border-radius: 4px;
733
+ background: var(--bg-base);
734
+ color: var(--text-primary);
735
+ cursor: pointer;
736
+ font-size: 0.85rem;
737
+ }
738
+
739
+ .spawner-filter select:hover {
740
+ border-color: var(--accent, #c8ff00);
741
+ }
742
+
743
+ /* Hide turns based on filter */
744
+ .conversation-turn.hidden {
745
+ display: none;
746
+ }
747
+
748
+ @keyframes highlightPulse {
749
+ 0% {
750
+ background-color: rgba(16, 185, 129, 0.2);
751
+ border-left: 4px solid #10b981;
752
+ }
753
+ 100% {
754
+ background-color: transparent;
755
+ border-left: 4px solid transparent;
756
+ }
757
+ }
758
+
759
+ .auto-refresh-indicator {
760
+ transition: opacity 0.3s ease;
761
+ }
762
+
763
+ .refresh-dot {
764
+ display: inline-block;
765
+ width: 8px;
766
+ height: 8px;
767
+ border-radius: 50%;
768
+ margin-right: 8px;
769
+ background-color: #10b981;
770
+ animation: pulse-dot 2s infinite;
771
+ }
772
+
773
+ @keyframes pulse-dot {
774
+ 0%, 100% {
775
+ opacity: 1;
776
+ }
777
+ 50% {
778
+ opacity: 0.5;
779
+ }
780
+ }
781
+ </style>
782
+ </body>
783
+ </html>