htmlgraph 0.26.5__py3-none-any.whl → 0.26.7__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 (70) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +355 -26
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/subagent_stop.py +71 -12
  38. htmlgraph/hooks/validator.py +192 -79
  39. htmlgraph/operations/__init__.py +18 -0
  40. htmlgraph/operations/initialization.py +596 -0
  41. htmlgraph/operations/initialization.py.backup +228 -0
  42. htmlgraph/orchestration/__init__.py +16 -1
  43. htmlgraph/orchestration/claude_launcher.py +185 -0
  44. htmlgraph/orchestration/command_builder.py +71 -0
  45. htmlgraph/orchestration/headless_spawner.py +72 -1332
  46. htmlgraph/orchestration/plugin_manager.py +136 -0
  47. htmlgraph/orchestration/prompts.py +137 -0
  48. htmlgraph/orchestration/spawners/__init__.py +16 -0
  49. htmlgraph/orchestration/spawners/base.py +194 -0
  50. htmlgraph/orchestration/spawners/claude.py +170 -0
  51. htmlgraph/orchestration/spawners/codex.py +442 -0
  52. htmlgraph/orchestration/spawners/copilot.py +299 -0
  53. htmlgraph/orchestration/spawners/gemini.py +478 -0
  54. htmlgraph/orchestration/subprocess_runner.py +33 -0
  55. htmlgraph/orchestration.md +563 -0
  56. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  57. htmlgraph/orchestrator_config.py +357 -0
  58. htmlgraph/orchestrator_mode.py +45 -12
  59. htmlgraph/transcript.py +16 -4
  60. htmlgraph-0.26.7.data/data/htmlgraph/dashboard.html +6592 -0
  61. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/METADATA +1 -1
  62. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/RECORD +68 -34
  63. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/entry_points.txt +1 -1
  64. htmlgraph/cli.py +0 -7256
  65. htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
  66. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/styles.css +0 -0
  67. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  68. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  69. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  70. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/WHEEL +0 -0
@@ -198,76 +198,464 @@
198
198
  }
199
199
  }
200
200
 
201
+ /**
202
+ * Insert new event into the grouped conversation turn activity feed
203
+ * Handles both UserQuery events (new turns) and child events (tool calls, etc.)
204
+ */
201
205
  function insertNewEventIntoActivityFeed(eventData) {
202
- const activityFeed = document.querySelector('.activity-feed-view');
203
- if (!activityFeed) return;
206
+ const conversationFeed = document.querySelector('.conversation-feed');
207
+ if (!conversationFeed) return;
204
208
 
205
- const activityList = activityFeed.querySelector('.activity-list');
206
- if (!activityList) return;
207
-
208
- const emptyState = activityList.querySelector('.empty-state');
209
+ // Check for empty state and remove it
210
+ const emptyState = conversationFeed.querySelector('.empty-state');
209
211
  if (emptyState) {
210
212
  emptyState.remove();
211
- if (!activityList.querySelector('.activity-table')) {
212
- // New column order: Agent | Tool | Input | Output | Status | Timestamp (no ID)
213
- const table = `
214
- <table class="activity-table">
215
- <thead>
216
- <tr>
217
- <th class="col-agent">Agent</th>
218
- <th class="col-tool">Tool</th>
219
- <th class="col-input">Input</th>
220
- <th class="col-output">Output</th>
221
- <th class="col-status">Status</th>
222
- <th class="col-timestamp">Timestamp</th>
223
- </tr>
224
- </thead>
225
- <tbody></tbody>
226
- </table>
227
- `;
228
- activityList.insertAdjacentHTML('afterbegin', table);
229
- }
230
213
  }
231
214
 
232
- const table = activityList.querySelector('.activity-table');
233
- if (!table) return;
215
+ // Get or create the turns list container
216
+ let turnsList = conversationFeed.querySelector('.conversation-turns-list');
217
+ if (!turnsList) {
218
+ turnsList = document.createElement('div');
219
+ turnsList.className = 'conversation-turns-list';
220
+ conversationFeed.appendChild(turnsList);
221
+ }
234
222
 
235
- const tbody = table.querySelector('tbody');
236
- if (!tbody) return;
223
+ // Check for duplicates
224
+ if (document.querySelector(`[data-event-id="${eventData.event_id}"]`)) {
225
+ console.log('Event already exists:', eventData.event_id);
226
+ return;
227
+ }
237
228
 
238
- const eventRow = createActivityRowHTML(eventData);
229
+ // Handle UserQuery events - create new conversation turn
230
+ if (eventData.tool_name === 'UserQuery') {
231
+ insertNewConversationTurn(eventData, turnsList);
232
+ return;
233
+ }
239
234
 
235
+ // Handle child events (tool calls, etc.)
240
236
  if (eventData.parent_event_id) {
241
- const parentRow = tbody.querySelector(`tr[data-event-id="${eventData.parent_event_id}"]`);
242
- if (parentRow) {
243
- parentRow.insertAdjacentHTML('afterend', eventRow);
244
- highlightRow(tbody.querySelector(`tr[data-event-id="${eventData.event_id}"]`));
245
- return;
237
+ insertChildEvent(eventData);
238
+ return;
239
+ }
240
+
241
+ console.warn('Event with no parent_event_id and not UserQuery:', eventData);
242
+ }
243
+
244
+ /**
245
+ * Create and insert a new conversation turn for a UserQuery event
246
+ */
247
+ function insertNewConversationTurn(userQueryEvent, turnsList) {
248
+ const turnId = userQueryEvent.event_id;
249
+ const prompt = userQueryEvent.input_summary || userQueryEvent.summary || '';
250
+ const timestamp = formatTimestamp(userQueryEvent.timestamp);
251
+
252
+ // Determine if this turn has spawner delegation
253
+ const hasSpawner = userQueryEvent.context && userQueryEvent.context.spawner_type ? 'spawner' : 'direct';
254
+ const agentId = userQueryEvent.agent_id || 'unknown';
255
+
256
+ const turnHtml = `
257
+ <div class="conversation-turn"
258
+ data-turn-id="${turnId}"
259
+ data-spawner-type="${hasSpawner}"
260
+ data-agent="${agentId}">
261
+ <!-- User Query Parent Row (Clickable) -->
262
+ <div class="userquery-parent"
263
+ onclick="toggleConversationTurn('${turnId}')"
264
+ data-turn-id="${turnId}">
265
+
266
+ <!-- Expand/Collapse Toggle -->
267
+ <span class="expand-toggle-turn" id="toggle-${turnId}">▶</span>
268
+
269
+ <!-- User Prompt Text -->
270
+ <div class="prompt-section">
271
+ <span class="prompt-text" title="${escapeHtml(prompt)}">
272
+ ${escapeHtml(prompt.substring(0, 100))}${prompt.length > 100 ? '...' : ''}
273
+ </span>
274
+ </div>
275
+
276
+ <!-- Stats Badges (initialized to 0) -->
277
+ <div class="turn-stats">
278
+ <span class="stat-badge tool-count" data-value="0" style="display: none;"></span>
279
+ <span class="stat-badge duration" data-value="0">0s</span>
280
+ </div>
281
+
282
+ <!-- Timestamp -->
283
+ <div class="turn-timestamp">
284
+ ${timestamp}
285
+ </div>
286
+ </div>
287
+
288
+ <!-- Child Events Container (Hidden by default) -->
289
+ <div class="turn-children collapsed" id="children-${turnId}">
290
+ <div class="no-children-message">
291
+ <span class="tree-connector">└─</span>
292
+ <span class="text-muted">No child events</span>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ `;
297
+
298
+ // Insert at top of turns list
299
+ const firstTurn = turnsList.firstChild;
300
+ if (firstTurn) {
301
+ firstTurn.insertAdjacentHTML('beforebegin', turnHtml);
302
+ } else {
303
+ turnsList.insertAdjacentHTML('afterbegin', turnHtml);
304
+ }
305
+
306
+ // Auto-expand the new turn
307
+ const childrenContainer = document.getElementById(`children-${turnId}`);
308
+ const toggleButton = document.getElementById(`toggle-${turnId}`);
309
+ if (childrenContainer && toggleButton) {
310
+ childrenContainer.classList.remove('collapsed');
311
+ toggleButton.classList.add('expanded');
312
+ }
313
+
314
+ // Highlight the new turn briefly
315
+ const newTurn = document.querySelector(`[data-turn-id="${turnId}"]`);
316
+ if (newTurn) {
317
+ highlightElement(newTurn.querySelector('.userquery-parent'));
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Find the root conversation turn ID that contains this event in the DOM.
323
+ * Walks up the DOM tree to find the parent conversation-turn element.
324
+ *
325
+ * @param {string} eventId - The event ID to find
326
+ * @returns {string|null} - The root UserQuery event_id or null if not found
327
+ */
328
+ function findRootConversationTurn(eventId) {
329
+ // First, check if this event is already in the DOM
330
+ const eventElement = document.querySelector(`[data-event-id="${eventId}"]`);
331
+ if (!eventElement) {
332
+ return null; // Event not in DOM yet
333
+ }
334
+
335
+ // Walk up the DOM to find the nearest conversation turn container
336
+ let current = eventElement;
337
+ while (current && current.parentElement) {
338
+ current = current.parentElement;
339
+ if (current.classList && current.classList.contains('conversation-turn')) {
340
+ return current.getAttribute('data-turn-id');
341
+ }
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ /**
348
+ * Find or create a children container for an event.
349
+ * If the event is the root conversation turn, use children-${turnId}.
350
+ * If the event is a nested event, create a nested children container.
351
+ *
352
+ * @param {string} parentEventId - The parent event ID
353
+ * @returns {HTMLElement|null} - The children container or null if not found
354
+ */
355
+ function findOrCreateChildrenContainer(parentEventId) {
356
+ // First, check if this is a conversation turn (root level)
357
+ let container = document.getElementById(`children-${parentEventId}`);
358
+ if (container) {
359
+ return container;
360
+ }
361
+
362
+ // Otherwise, look for the parent event in the DOM
363
+ const parentElement = document.querySelector(`[data-event-id="${parentEventId}"]`);
364
+ if (!parentElement) {
365
+ return null; // Parent event not in DOM yet
366
+ }
367
+
368
+ // Check if parent event already has a nested children container
369
+ let nestedContainer = parentElement.querySelector(':scope > .event-children');
370
+ if (!nestedContainer) {
371
+ // Create a nested children container
372
+ nestedContainer = document.createElement('div');
373
+ nestedContainer.className = 'event-children';
374
+ nestedContainer.setAttribute('data-parent-id', parentEventId);
375
+ parentElement.appendChild(nestedContainer);
376
+ }
377
+
378
+ return nestedContainer;
379
+ }
380
+
381
+ /**
382
+ * Calculate the depth of an event based on how many containers separate it from the root turn.
383
+ * Walks up the DOM tree counting .turn-children and .event-children containers.
384
+ *
385
+ * @param {string} parentEventId - The parent event ID
386
+ * @returns {number} - The depth (0 for direct children of a UserQuery turn)
387
+ */
388
+ function calculateEventDepth(parentEventId) {
389
+ let depth = 0;
390
+
391
+ // Start by finding the children container for this parent
392
+ let container = document.getElementById(`children-${parentEventId}`);
393
+ if (!container) {
394
+ const parentElement = document.querySelector(`[data-event-id="${parentEventId}"]`);
395
+ if (parentElement) {
396
+ container = parentElement.querySelector(':scope > .event-children');
397
+ }
398
+ }
399
+
400
+ if (!container) {
401
+ return 0; // Parent not yet in DOM or is a root turn
402
+ }
403
+
404
+ // Walk up the DOM to count nesting levels
405
+ let current = container.parentElement; // Start from parent of container
406
+ while (current) {
407
+ // Check if we're in a .turn-children container (root level)
408
+ if (current.classList && current.classList.contains('turn-children')) {
409
+ return depth; // We've reached the root turn
410
+ }
411
+
412
+ // Check if we're in a child-event-row (nested event) that has children
413
+ if (current.classList && current.classList.contains('child-event-row')) {
414
+ depth++;
415
+ // Move up to find the next ancestor event row
416
+ current = current.parentElement; // Move to .event-children container
417
+ if (current) {
418
+ current = current.parentElement; // Move to parent event row
419
+ }
420
+ } else {
421
+ current = current.parentElement;
246
422
  }
247
423
  }
248
424
 
249
- const firstRow = tbody.querySelector('tr');
250
- if (firstRow) {
251
- firstRow.insertAdjacentHTML('beforebegin', eventRow);
425
+ return depth;
426
+ }
427
+
428
+ /**
429
+ * Insert a child event into its parent (which could be a conversation turn or another event).
430
+ * Handles multi-level nesting for spawner delegations.
431
+ */
432
+ function insertChildEvent(eventData) {
433
+ const parentEventId = eventData.parent_event_id;
434
+ if (!parentEventId) {
435
+ console.warn('Child event has no parent_event_id:', eventData.event_id);
436
+ return;
437
+ }
438
+
439
+ // Find or create the children container for this parent
440
+ const childrenContainer = findOrCreateChildrenContainer(parentEventId);
441
+ if (!childrenContainer) {
442
+ console.warn('Could not find or create children container for parent:', parentEventId);
443
+ return;
444
+ }
445
+
446
+ // Remove "no children" message if it exists
447
+ const noChildrenMsg = childrenContainer.querySelector('.no-children-message');
448
+ if (noChildrenMsg) {
449
+ noChildrenMsg.remove();
450
+ }
451
+
452
+ // Extract event data
453
+ const toolName = eventData.tool_name || 'unknown';
454
+ const summary = eventData.output_summary || eventData.input_summary || eventData.summary || '';
455
+ const duration = eventData.duration_seconds || 0;
456
+ const timestamp = formatTimestamp(eventData.timestamp);
457
+ const agentId = eventData.agent_id || 'Claude Code';
458
+ const model = eventData.context && eventData.context.model ? eventData.context.model : null;
459
+ const spawnerType = eventData.context && eventData.context.spawner_type ? eventData.context.spawner_type : null;
460
+ const spawnedAgent = eventData.context && eventData.context.spawned_agent ? eventData.context.spawned_agent : null;
461
+ const costUsd = eventData.context && eventData.context.cost_usd ? eventData.context.cost_usd : null;
462
+
463
+ // Calculate depth: count how many levels of nesting this event is at
464
+ const depth = calculateEventDepth(parentEventId);
465
+
466
+ // Determine tree connector based on whether this is the last child
467
+ const existingChildren = childrenContainer.querySelectorAll(':scope > .child-event-row');
468
+ const isLastChild = existingChildren.length === 0;
469
+ const hasChildren = eventData.context && eventData.context.has_children;
470
+ const treeConnector = (isLastChild && !hasChildren) ? '└─' : '├─';
471
+
472
+ // Build child event HTML with nested container placeholder
473
+ let childHtml = `
474
+ <div class="child-event-row depth-${depth}"
475
+ data-event-id="${eventData.event_id}"
476
+ data-parent-id="${parentEventId}"
477
+ style="margin-left: ${depth * 20}px;">
478
+
479
+ <!-- Tree Connector -->
480
+ <span class="tree-connector">${treeConnector}</span>
481
+
482
+ <!-- Tool Name -->
483
+ <span class="child-tool-name">${escapeHtml(toolName)}</span>
484
+
485
+ <!-- Summary/Input -->
486
+ <span class="child-summary" title="${escapeHtml(summary)}">
487
+ ${escapeHtml(summary.substring(0, 80))}${summary.length > 80 ? '...' : ''}
488
+ </span>
489
+ `;
490
+
491
+ // Add agent badge with spawner support
492
+ if (spawnerType) {
493
+ // Spawner delegation: show orchestrator → spawned AI
494
+ childHtml += `
495
+ <span class="child-agent-badge agent-${agentId.toLowerCase().replace(/\\s+/g, '-')}">
496
+ ${escapeHtml(agentId)}
497
+ ${model ? `<span class="model-indicator">${escapeHtml(model)}</span>` : ''}
498
+ </span>
499
+ <span class="delegation-arrow">→</span>
500
+ <span class="spawner-badge spawner-${spawnerType.toLowerCase()}">
501
+ ${escapeHtml(spawnedAgent || spawnerType)}
502
+ ${costUsd ? `<span class="cost-badge">$${costUsd.toFixed(2)}</span>` : ''}
503
+ </span>
504
+ `;
252
505
  } else {
253
- tbody.insertAdjacentHTML('afterbegin', eventRow);
506
+ // Regular agent: just show agent name + model if available
507
+ childHtml += `
508
+ <span class="child-agent-badge agent-${agentId.toLowerCase().replace(/\\s+/g, '-')}">
509
+ ${escapeHtml(agentId)}
510
+ ${model ? `<span class="model-indicator">${escapeHtml(model)}</span>` : ''}
511
+ </span>
512
+ `;
513
+ }
514
+
515
+ // Add duration and timestamp
516
+ childHtml += `
517
+ <!-- Duration -->
518
+ <span class="child-duration">
519
+ ${duration.toFixed(2)}s
520
+ </span>
521
+
522
+ <!-- Timestamp -->
523
+ <span class="child-timestamp">
524
+ ${timestamp}
525
+ </span>
526
+ </div>
527
+ `;
528
+
529
+ // Insert child event
530
+ childrenContainer.insertAdjacentHTML('beforeend', childHtml);
531
+
532
+ // Update root conversation turn statistics
533
+ const rootTurnId = findRootConversationTurn(eventData.event_id);
534
+ if (rootTurnId) {
535
+ updateParentTurnStats(rootTurnId, eventData);
536
+ }
537
+
538
+ // Highlight the new child event
539
+ const newChild = document.querySelector(`[data-event-id="${eventData.event_id}"]`);
540
+ if (newChild) {
541
+ highlightElement(newChild);
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Update the statistics of a parent conversation turn based on a child event
547
+ */
548
+ function updateParentTurnStats(parentTurnId, childEvent) {
549
+ const turnElement = document.querySelector(`[data-turn-id="${parentTurnId}"]`);
550
+ if (!turnElement) return;
551
+
552
+ const statsContainer = turnElement.querySelector('.turn-stats');
553
+ if (!statsContainer) return;
554
+
555
+ // Get current stats from badges
556
+ let toolCount = parseInt(statsContainer.querySelector('.stat-badge.tool-count')?.getAttribute('data-value') || '0', 10);
557
+ let totalDuration = parseFloat(statsContainer.querySelector('.stat-badge.duration')?.getAttribute('data-value') || '0');
558
+ let successCount = parseInt(statsContainer.querySelector('.stat-badge.success')?.getAttribute('data-value') || '0', 10);
559
+ let errorCount = parseInt(statsContainer.querySelector('.stat-badge.error')?.getAttribute('data-value') || '0', 10);
560
+
561
+ // Update counts based on event
562
+ if (childEvent.tool_name !== 'UserQuery') {
563
+ toolCount++;
564
+ }
565
+
566
+ totalDuration += (childEvent.duration_seconds || 0);
567
+
568
+ // Determine success/error based on status
569
+ const status = childEvent.status || 'completed';
570
+ if (status === 'completed' || status === 'success') {
571
+ successCount++;
572
+ } else if (status === 'error' || status === 'failed') {
573
+ errorCount++;
254
574
  }
255
575
 
256
- highlightRow(tbody.querySelector('tr:first-child'));
576
+ // Update tool count badge
577
+ const toolCountBadge = statsContainer.querySelector('.stat-badge.tool-count');
578
+ if (toolCountBadge) {
579
+ toolCountBadge.setAttribute('data-value', toolCount);
580
+ toolCountBadge.textContent = toolCount;
581
+ toolCountBadge.style.display = toolCount > 0 ? 'inline-block' : 'none';
582
+ }
257
583
 
258
- if (typeof convertTimestampsToLocal === 'function') {
259
- convertTimestampsToLocal();
584
+ // Update duration badge
585
+ const durationBadge = statsContainer.querySelector('.stat-badge.duration');
586
+ if (durationBadge) {
587
+ durationBadge.setAttribute('data-value', totalDuration.toFixed(2));
588
+ durationBadge.textContent = totalDuration.toFixed(2) + 's';
260
589
  }
261
590
 
262
- const allRows = tbody.querySelectorAll('tr');
263
- if (allRows.length > 100) {
264
- const itemsToRemove = allRows.length - 100;
265
- for (let i = 0; i < itemsToRemove; i++) {
266
- allRows[allRows.length - 1 - i].remove();
591
+ // Update success badge
592
+ let successBadge = statsContainer.querySelector('.stat-badge.success');
593
+ if (successCount > 0) {
594
+ if (!successBadge) {
595
+ const badge = document.createElement('span');
596
+ badge.className = 'stat-badge success';
597
+ statsContainer.appendChild(badge);
598
+ successBadge = badge;
267
599
  }
600
+ successBadge.setAttribute('data-value', successCount);
601
+ successBadge.textContent = `✓ ${successCount}`;
602
+ } else if (successBadge) {
603
+ successBadge.remove();
604
+ }
605
+
606
+ // Update error badge
607
+ let errorBadge = statsContainer.querySelector('.stat-badge.error');
608
+ if (errorCount > 0) {
609
+ if (!errorBadge) {
610
+ const badge = document.createElement('span');
611
+ badge.className = 'stat-badge error';
612
+ statsContainer.appendChild(badge);
613
+ errorBadge = badge;
614
+ }
615
+ errorBadge.setAttribute('data-value', errorCount);
616
+ errorBadge.textContent = `✗ ${errorCount}`;
617
+ } else if (errorBadge) {
618
+ errorBadge.remove();
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Format timestamp to readable format (HH:MM:SS)
624
+ */
625
+ function formatTimestamp(timestamp) {
626
+ try {
627
+ const date = new Date(timestamp);
628
+ const hours = String(date.getHours()).padStart(2, '0');
629
+ const minutes = String(date.getMinutes()).padStart(2, '0');
630
+ const seconds = String(date.getSeconds()).padStart(2, '0');
631
+ return `${hours}:${minutes}:${seconds}`;
632
+ } catch (e) {
633
+ return timestamp;
268
634
  }
269
635
  }
270
636
 
637
+ /**
638
+ * Escape HTML special characters to prevent XSS
639
+ */
640
+ function escapeHtml(text) {
641
+ if (!text) return '';
642
+ const div = document.createElement('div');
643
+ div.textContent = text;
644
+ return div.innerHTML;
645
+ }
646
+
647
+ /**
648
+ * Highlight an element briefly with a background color animation
649
+ */
650
+ function highlightElement(element) {
651
+ if (!element) return;
652
+ element.style.transition = 'background-color 0.3s ease';
653
+ element.style.backgroundColor = 'rgba(163, 230, 53, 0.2)';
654
+ setTimeout(() => {
655
+ element.style.backgroundColor = '';
656
+ }, 500);
657
+ }
658
+
271
659
  function highlightRow(row) {
272
660
  if (row) {
273
661
  row.classList.add('new-event-highlight');
@@ -328,13 +716,6 @@
328
716
  return html;
329
717
  }
330
718
 
331
- function escapeHtml(text) {
332
- if (!text) return '';
333
- const div = document.createElement('div');
334
- div.textContent = text;
335
- return div.innerHTML;
336
- }
337
-
338
719
  function convertTimestampsToLocal() {
339
720
  const timestampElements = document.querySelectorAll('[data-utc-time]');
340
721
  timestampElements.forEach(element => {
@@ -791,6 +1172,168 @@
791
1172
  display: none;
792
1173
  }
793
1174
 
1175
+ /* Nested event children container styles */
1176
+ .event-children {
1177
+ display: flex;
1178
+ flex-direction: column;
1179
+ margin-top: 0;
1180
+ border-left: 1px solid var(--border-subtle);
1181
+ padding-left: var(--spacing-sm);
1182
+ }
1183
+
1184
+ .child-event-row {
1185
+ display: flex;
1186
+ align-items: center;
1187
+ gap: var(--spacing-sm);
1188
+ padding: var(--spacing-sm) var(--spacing-md);
1189
+ border-radius: 4px;
1190
+ background: rgba(0, 0, 0, 0.1);
1191
+ font-size: 0.9rem;
1192
+ color: var(--text-primary);
1193
+ border: 1px solid transparent;
1194
+ transition: all 0.2s ease;
1195
+ }
1196
+
1197
+ .child-event-row:hover {
1198
+ background: rgba(0, 0, 0, 0.15);
1199
+ border-color: var(--border-subtle);
1200
+ }
1201
+
1202
+ .child-event-row.depth-0 {
1203
+ margin-left: 0;
1204
+ }
1205
+
1206
+ .child-event-row.depth-1 {
1207
+ margin-left: 20px;
1208
+ }
1209
+
1210
+ .child-event-row.depth-2 {
1211
+ margin-left: 40px;
1212
+ }
1213
+
1214
+ .child-event-row.depth-3 {
1215
+ margin-left: 60px;
1216
+ }
1217
+
1218
+ .child-event-row.depth-4 {
1219
+ margin-left: 80px;
1220
+ }
1221
+
1222
+ .child-event-row.depth-5 {
1223
+ margin-left: 100px;
1224
+ }
1225
+
1226
+ .tree-connector {
1227
+ color: var(--text-muted);
1228
+ font-size: 0.85rem;
1229
+ font-family: monospace;
1230
+ flex-shrink: 0;
1231
+ }
1232
+
1233
+ .child-tool-name {
1234
+ font-weight: 600;
1235
+ color: var(--accent-lime);
1236
+ flex-shrink: 0;
1237
+ }
1238
+
1239
+ .child-summary {
1240
+ flex: 1;
1241
+ overflow: hidden;
1242
+ text-overflow: ellipsis;
1243
+ white-space: nowrap;
1244
+ color: var(--text-secondary);
1245
+ font-size: 0.85rem;
1246
+ }
1247
+
1248
+ .child-agent-badge {
1249
+ display: inline-flex;
1250
+ align-items: center;
1251
+ gap: var(--spacing-xs);
1252
+ padding: var(--spacing-xs) var(--spacing-sm);
1253
+ background: rgba(205, 255, 0, 0.1);
1254
+ border: 1px solid rgba(205, 255, 0, 0.3);
1255
+ border-radius: 3px;
1256
+ font-size: 0.75rem;
1257
+ font-weight: 600;
1258
+ color: var(--accent-lime);
1259
+ flex-shrink: 0;
1260
+ }
1261
+
1262
+ .child-agent-badge.agent-claude {
1263
+ background: rgba(139, 92, 246, 0.1);
1264
+ border-color: rgba(139, 92, 246, 0.3);
1265
+ color: var(--agent-claude);
1266
+ }
1267
+
1268
+ .child-agent-badge.agent-gemini {
1269
+ background: rgba(59, 130, 246, 0.1);
1270
+ border-color: rgba(59, 130, 246, 0.3);
1271
+ color: var(--agent-gemini);
1272
+ }
1273
+
1274
+ .model-indicator {
1275
+ font-size: 0.7rem;
1276
+ opacity: 0.8;
1277
+ }
1278
+
1279
+ .delegation-arrow {
1280
+ color: var(--text-muted);
1281
+ font-weight: bold;
1282
+ flex-shrink: 0;
1283
+ }
1284
+
1285
+ .spawner-badge {
1286
+ display: inline-flex;
1287
+ align-items: center;
1288
+ gap: var(--spacing-xs);
1289
+ padding: var(--spacing-xs) var(--spacing-sm);
1290
+ background: rgba(139, 92, 246, 0.15);
1291
+ border: 1px solid rgba(139, 92, 246, 0.4);
1292
+ border-radius: 3px;
1293
+ font-size: 0.75rem;
1294
+ font-weight: 600;
1295
+ color: #8B5CF6;
1296
+ flex-shrink: 0;
1297
+ }
1298
+
1299
+ .spawner-badge.spawner-gemini {
1300
+ background: rgba(59, 130, 246, 0.15);
1301
+ border-color: rgba(59, 130, 246, 0.4);
1302
+ color: #3B82F6;
1303
+ }
1304
+
1305
+ .spawner-badge.spawner-codex {
1306
+ background: rgba(34, 197, 94, 0.15);
1307
+ border-color: rgba(34, 197, 94, 0.4);
1308
+ color: #22C55E;
1309
+ }
1310
+
1311
+ .spawner-badge.spawner-copilot {
1312
+ background: rgba(251, 146, 60, 0.15);
1313
+ border-color: rgba(251, 146, 60, 0.4);
1314
+ color: #FB923C;
1315
+ }
1316
+
1317
+ .cost-badge {
1318
+ font-size: 0.7rem;
1319
+ opacity: 0.9;
1320
+ margin-left: 2px;
1321
+ }
1322
+
1323
+ .child-duration {
1324
+ font-size: 0.8rem;
1325
+ color: var(--text-secondary);
1326
+ flex-shrink: 0;
1327
+ font-family: monospace;
1328
+ }
1329
+
1330
+ .child-timestamp {
1331
+ font-size: 0.8rem;
1332
+ color: var(--text-muted);
1333
+ flex-shrink: 0;
1334
+ font-family: monospace;
1335
+ }
1336
+
794
1337
  @media (max-width: 1024px) {
795
1338
  .col-agent { width: 100px; }
796
1339
  .col-tool { width: 90px; }
@@ -806,6 +1349,17 @@
806
1349
  .truncate {
807
1350
  max-width: 100%;
808
1351
  }
1352
+
1353
+ .child-event-row {
1354
+ flex-wrap: wrap;
1355
+ gap: var(--spacing-xs);
1356
+ padding: var(--spacing-sm);
1357
+ }
1358
+
1359
+ .child-summary {
1360
+ flex-basis: 100%;
1361
+ order: 3;
1362
+ }
809
1363
  }
810
1364
  </style>
811
1365
  </body>