htmlgraph 0.26.5__py3-none-any.whl → 0.26.6__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +50 -10
- htmlgraph/api/templates/dashboard-redesign.html +608 -54
- htmlgraph/api/templates/partials/activity-feed.html +21 -0
- htmlgraph/api/templates/partials/features.html +81 -12
- htmlgraph/api/templates/partials/orchestration.html +35 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +939 -0
- htmlgraph/cli/base.py +660 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +856 -0
- htmlgraph/cli/main.py +143 -0
- htmlgraph/cli/models.py +462 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +398 -0
- htmlgraph/cli/work/__init__.py +159 -0
- htmlgraph/cli/work/features.py +567 -0
- htmlgraph/cli/work/orchestration.py +675 -0
- htmlgraph/cli/work/sessions.py +465 -0
- htmlgraph/cli/work/tracks.py +485 -0
- htmlgraph/dashboard.html +6414 -634
- htmlgraph/db/schema.py +8 -3
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
- htmlgraph/docs/README.md +2 -3
- htmlgraph/hooks/event_tracker.py +157 -25
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/orchestrator.py +137 -71
- htmlgraph/hooks/orchestrator_reflector.py +23 -0
- htmlgraph/hooks/pretooluse.py +29 -6
- htmlgraph/hooks/session_handler.py +28 -0
- htmlgraph/hooks/session_summary.py +391 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/validator.py +192 -79
- htmlgraph/operations/__init__.py +18 -0
- htmlgraph/operations/initialization.py +596 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/orchestration/__init__.py +16 -1
- htmlgraph/orchestration/claude_launcher.py +185 -0
- htmlgraph/orchestration/command_builder.py +71 -0
- htmlgraph/orchestration/headless_spawner.py +72 -1332
- htmlgraph/orchestration/plugin_manager.py +136 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +170 -0
- htmlgraph/orchestration/spawners/codex.py +442 -0
- htmlgraph/orchestration/spawners/copilot.py +299 -0
- htmlgraph/orchestration/spawners/gemini.py +478 -0
- htmlgraph/orchestration/subprocess_runner.py +33 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +45 -12
- htmlgraph/transcript.py +16 -4
- htmlgraph-0.26.6.data/data/htmlgraph/dashboard.html +6592 -0
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.6.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.6.dist-info}/RECORD +67 -33
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.6.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -7256
- htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
- {htmlgraph-0.26.5.data → htmlgraph-0.26.6.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.6.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
|
|
203
|
-
if (!
|
|
206
|
+
const conversationFeed = document.querySelector('.conversation-feed');
|
|
207
|
+
if (!conversationFeed) return;
|
|
204
208
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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>
|