claude-mpm 3.4.10__py3-none-any.whl → 3.4.14__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 (29) hide show
  1. claude_mpm/cli/commands/run.py +10 -10
  2. claude_mpm/dashboard/index.html +13 -0
  3. claude_mpm/dashboard/static/css/dashboard.css +2722 -0
  4. claude_mpm/dashboard/static/js/components/agent-inference.js +619 -0
  5. claude_mpm/dashboard/static/js/components/event-processor.js +641 -0
  6. claude_mpm/dashboard/static/js/components/event-viewer.js +914 -0
  7. claude_mpm/dashboard/static/js/components/export-manager.js +362 -0
  8. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +611 -0
  9. claude_mpm/dashboard/static/js/components/hud-library-loader.js +211 -0
  10. claude_mpm/dashboard/static/js/components/hud-manager.js +671 -0
  11. claude_mpm/dashboard/static/js/components/hud-visualizer.js +1718 -0
  12. claude_mpm/dashboard/static/js/components/module-viewer.js +2701 -0
  13. claude_mpm/dashboard/static/js/components/session-manager.js +520 -0
  14. claude_mpm/dashboard/static/js/components/socket-manager.js +343 -0
  15. claude_mpm/dashboard/static/js/components/ui-state-manager.js +427 -0
  16. claude_mpm/dashboard/static/js/components/working-directory.js +866 -0
  17. claude_mpm/dashboard/static/js/dashboard-original.js +4134 -0
  18. claude_mpm/dashboard/static/js/dashboard.js +1978 -0
  19. claude_mpm/dashboard/static/js/socket-client.js +537 -0
  20. claude_mpm/dashboard/templates/index.html +346 -0
  21. claude_mpm/dashboard/test_dashboard.html +372 -0
  22. claude_mpm/scripts/socketio_daemon.py +51 -6
  23. claude_mpm/services/socketio_server.py +41 -5
  24. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/METADATA +2 -1
  25. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/RECORD +29 -9
  26. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/WHEEL +0 -0
  27. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/entry_points.txt +0 -0
  28. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/licenses/LICENSE +0 -0
  29. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2701 @@
1
+ /**
2
+ * Module Viewer Component
3
+ * Displays detailed information about selected events organized by class/type
4
+ */
5
+
6
+ class ModuleViewer {
7
+ constructor(containerId) {
8
+ this.container = document.getElementById(containerId);
9
+ this.dataContainer = null;
10
+ this.jsonContainer = null;
11
+ this.currentEvent = null;
12
+ this.eventsByClass = new Map();
13
+
14
+ this.init();
15
+ }
16
+
17
+ /**
18
+ * Initialize the module viewer
19
+ */
20
+ init() {
21
+ this.setupContainers();
22
+ this.setupEventHandlers();
23
+ this.showEmptyState();
24
+ }
25
+
26
+ /**
27
+ * Setup container references for the two-pane layout
28
+ */
29
+ setupContainers() {
30
+ this.dataContainer = document.getElementById('module-data-content');
31
+ this.jsonContainer = null; // No longer used - JSON is handled via collapsible sections
32
+
33
+ if (!this.dataContainer) {
34
+ console.error('Module viewer data container not found');
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Setup event handlers
40
+ */
41
+ setupEventHandlers() {
42
+ // Listen for event selection
43
+ document.addEventListener('eventSelected', (e) => {
44
+ this.showEventDetails(e.detail.event);
45
+ });
46
+
47
+ // Listen for selection cleared
48
+ document.addEventListener('eventSelectionCleared', () => {
49
+ this.showEmptyState();
50
+ });
51
+
52
+ // Listen for socket event updates to maintain event classification
53
+ document.addEventListener('socketEventUpdate', (e) => {
54
+ this.updateEventsByClass(e.detail.events);
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Show empty state when no event is selected
60
+ */
61
+ showEmptyState() {
62
+ if (this.dataContainer) {
63
+ this.dataContainer.innerHTML = `
64
+ <div class="module-empty">
65
+ <p>Click on an event to view structured data</p>
66
+ <p class="module-hint">Data is organized by event type</p>
67
+ </div>
68
+ `;
69
+ }
70
+
71
+ // JSON container no longer exists - handled via collapsible sections
72
+
73
+ this.currentEvent = null;
74
+ }
75
+
76
+ /**
77
+ * Show details for a selected event
78
+ * @param {Object} event - The selected event
79
+ */
80
+ showEventDetails(event) {
81
+ this.currentEvent = event;
82
+
83
+ // Render structured data in top pane
84
+ this.renderStructuredData(event);
85
+
86
+ // Render JSON in bottom pane
87
+ this.renderJsonData(event);
88
+ }
89
+
90
+ /**
91
+ * Render structured data in the data pane with collapsible JSON section
92
+ * @param {Object} event - The event to render
93
+ */
94
+ renderStructuredData(event) {
95
+ if (!this.dataContainer) return;
96
+
97
+ // Create contextual header
98
+ const contextualHeader = this.createContextualHeader(event);
99
+
100
+ // Create structured view based on event type
101
+ const structuredView = this.createEventStructuredView(event);
102
+
103
+ // Create collapsible JSON section
104
+ const collapsibleJsonSection = this.createCollapsibleJsonSection(event);
105
+
106
+ // Combine all sections in data container
107
+ this.dataContainer.innerHTML = contextualHeader + structuredView + collapsibleJsonSection;
108
+
109
+ // Initialize JSON toggle functionality
110
+ this.initializeJsonToggle();
111
+ }
112
+
113
+ /**
114
+ * Render JSON data in the JSON pane (legacy support - now using collapsible section)
115
+ * @param {Object} event - The event to render
116
+ */
117
+ renderJsonData(event) {
118
+ // JSON is now integrated into data container as collapsible section
119
+ // Hide the JSON pane completely by clearing it
120
+ // JSON container no longer exists - handled via collapsible sections
121
+ }
122
+
123
+ /**
124
+ * Ingest method that determines how to render event(s)
125
+ * @param {Object|Array} eventData - Single event or array of events
126
+ */
127
+ ingest(eventData) {
128
+ if (Array.isArray(eventData)) {
129
+ // Handle multiple events - for now, show the first one
130
+ if (eventData.length > 0) {
131
+ this.showEventDetails(eventData[0]);
132
+ } else {
133
+ this.showEmptyState();
134
+ }
135
+ } else if (eventData && typeof eventData === 'object') {
136
+ // Handle single event
137
+ this.showEventDetails(eventData);
138
+ } else {
139
+ // Invalid data
140
+ this.showEmptyState();
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Update events grouped by class for analysis
146
+ * @param {Array} events - All events
147
+ */
148
+ updateEventsByClass(events) {
149
+ this.eventsByClass.clear();
150
+
151
+ events.forEach(event => {
152
+ const eventClass = this.getEventClass(event);
153
+ if (!this.eventsByClass.has(eventClass)) {
154
+ this.eventsByClass.set(eventClass, []);
155
+ }
156
+ this.eventsByClass.get(eventClass).push(event);
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Get event class/category for grouping
162
+ * @param {Object} event - Event object
163
+ * @returns {string} Event class
164
+ */
165
+ getEventClass(event) {
166
+ if (!event.type) return 'unknown';
167
+
168
+ // Group similar event types
169
+ switch (event.type) {
170
+ case 'session':
171
+ return 'Session Management';
172
+ case 'claude':
173
+ return 'Claude Interactions';
174
+ case 'agent':
175
+ return 'Agent Operations';
176
+ case 'hook':
177
+ return 'Hook System';
178
+ case 'todo':
179
+ return 'Task Management';
180
+ case 'memory':
181
+ return 'Memory Operations';
182
+ case 'log':
183
+ return 'System Logs';
184
+ case 'connection':
185
+ return 'Connection Events';
186
+ default:
187
+ return 'Other Events';
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Create contextual header for the structured data
193
+ * @param {Object} event - Event to display
194
+ * @returns {string} HTML content
195
+ */
196
+ createContextualHeader(event) {
197
+ const timestamp = this.formatTimestamp(event.timestamp);
198
+ const data = event.data || {};
199
+ let headerText = '';
200
+
201
+ // Determine header text based on event type
202
+ switch (event.type) {
203
+ case 'hook':
204
+ // For Tools: "ToolName: [Agent] [time]"
205
+ const toolName = this.extractToolName(data);
206
+ const agent = this.extractAgent(event) || 'Unknown';
207
+ if (toolName) {
208
+ headerText = `${toolName}: ${agent} ${timestamp}`;
209
+ } else {
210
+ const hookName = this.getHookDisplayName(event, data);
211
+ headerText = `${hookName}: ${agent} ${timestamp}`;
212
+ }
213
+ break;
214
+
215
+ case 'agent':
216
+ // For Agents: "Agent: [AgentType] [time]"
217
+ const agentType = data.agent_type || data.name || 'Unknown';
218
+ headerText = `Agent: ${agentType} ${timestamp}`;
219
+ break;
220
+
221
+ case 'todo':
222
+ // For TodoWrite: "TodoWrite: [Agent] [time]"
223
+ const todoAgent = this.extractAgent(event) || 'PM';
224
+ headerText = `TodoWrite: ${todoAgent} ${timestamp}`;
225
+ break;
226
+
227
+ case 'memory':
228
+ // For Memory: "Memory: [Operation] [time]"
229
+ const operation = data.operation || 'Unknown';
230
+ headerText = `Memory: ${operation} ${timestamp}`;
231
+ break;
232
+
233
+ case 'session':
234
+ case 'claude':
235
+ case 'log':
236
+ case 'connection':
237
+ // For Events: "Event: [Type.Subtype] [time]"
238
+ const eventType = event.type;
239
+ const subtype = event.subtype || 'default';
240
+ headerText = `Event: ${eventType}.${subtype} ${timestamp}`;
241
+ break;
242
+
243
+ default:
244
+ // For Files and other events: "File: [filename] [time]" or generic
245
+ const fileName = this.extractFileName(data);
246
+ if (fileName) {
247
+ headerText = `File: ${fileName} ${timestamp}`;
248
+ } else {
249
+ const eventType = event.type || 'Unknown';
250
+ const subtype = event.subtype || 'default';
251
+ headerText = `Event: ${eventType}.${subtype} ${timestamp}`;
252
+ }
253
+ break;
254
+ }
255
+
256
+ return `
257
+ <div class="contextual-header">
258
+ <h3 class="contextual-header-text">${headerText}</h3>
259
+ </div>
260
+ `;
261
+ }
262
+
263
+ /**
264
+ * Create structured view for an event
265
+ * @param {Object} event - Event to display
266
+ * @returns {string} HTML content
267
+ */
268
+ createEventStructuredView(event) {
269
+ const eventClass = this.getEventClass(event);
270
+ const relatedEvents = this.eventsByClass.get(eventClass) || [];
271
+ const eventCount = relatedEvents.length;
272
+
273
+ let content = `
274
+ <div class="structured-view-section">
275
+ ${this.createEventDetailCard(event.type, event, eventCount)}
276
+ </div>
277
+ `;
278
+
279
+ // Add type-specific content
280
+ switch (event.type) {
281
+ case 'agent':
282
+ content += this.createAgentStructuredView(event);
283
+ break;
284
+ case 'hook':
285
+ // Check if this is actually a Task delegation (agent-related hook)
286
+ if (event.data?.tool_name === 'Task' && event.data?.tool_parameters?.subagent_type) {
287
+ content += this.createAgentStructuredView(event);
288
+ } else {
289
+ content += this.createHookStructuredView(event);
290
+ }
291
+ break;
292
+ case 'todo':
293
+ content += this.createTodoStructuredView(event);
294
+ break;
295
+ case 'memory':
296
+ content += this.createMemoryStructuredView(event);
297
+ break;
298
+ case 'claude':
299
+ content += this.createClaudeStructuredView(event);
300
+ break;
301
+ case 'session':
302
+ content += this.createSessionStructuredView(event);
303
+ break;
304
+ default:
305
+ content += this.createGenericStructuredView(event);
306
+ break;
307
+ }
308
+
309
+ // Note: JSON section is now rendered separately in the JSON pane
310
+ return content;
311
+ }
312
+
313
+ /**
314
+ * Create event detail card
315
+ */
316
+ createEventDetailCard(eventType, event, count) {
317
+ const timestamp = new Date(event.timestamp).toLocaleString();
318
+ const eventIcon = this.getEventIcon(eventType);
319
+
320
+ return `
321
+ <div class="event-detail-card">
322
+ <div class="event-detail-header">
323
+ <div class="event-detail-title">
324
+ ${eventIcon} ${eventType || 'Unknown'}.${event.subtype || 'default'}
325
+ </div>
326
+ <div class="event-detail-time">${timestamp}</div>
327
+ </div>
328
+ <div class="event-detail-content">
329
+ ${this.createProperty('Event ID', event.id || 'N/A')}
330
+ ${this.createProperty('Type', `${eventType}.${event.subtype || 'default'}`)}
331
+ ${this.createProperty('Class Events', count)}
332
+ ${event.data && event.data.session_id ?
333
+ this.createProperty('Session', event.data.session_id) : ''}
334
+ </div>
335
+ </div>
336
+ `;
337
+ }
338
+
339
+ /**
340
+ * Create agent-specific structured view
341
+ */
342
+ createAgentStructuredView(event) {
343
+ const data = event.data || {};
344
+
345
+ // Handle Task delegation events (which appear as hook events but contain agent info)
346
+ if (event.type === 'hook' && data.tool_name === 'Task' && data.tool_parameters?.subagent_type) {
347
+ const taskData = data.tool_parameters;
348
+ return `
349
+ <div class="structured-view-section">
350
+ <div class="structured-data">
351
+ ${this.createProperty('Agent Type', taskData.subagent_type)}
352
+ ${this.createProperty('Task Type', 'Subagent Delegation')}
353
+ ${this.createProperty('Phase', event.subtype || 'pre_tool')}
354
+ ${taskData.description ? this.createProperty('Description', taskData.description) : ''}
355
+ ${taskData.prompt ? this.createProperty('Prompt Preview', this.truncateText(taskData.prompt, 200)) : ''}
356
+ ${data.session_id ? this.createProperty('Session ID', data.session_id) : ''}
357
+ ${data.working_directory ? this.createProperty('Working Directory', data.working_directory) : ''}
358
+ </div>
359
+ ${taskData.prompt ? `
360
+ <div class="prompt-section">
361
+ <div class="contextual-header">
362
+ <h3 class="contextual-header-text">📝 Task Prompt</h3>
363
+ </div>
364
+ <div class="structured-data">
365
+ <div class="task-prompt" style="white-space: pre-wrap; max-height: 300px; overflow-y: auto; padding: 10px; background: #f8fafc; border-radius: 6px; font-family: monospace; font-size: 12px; line-height: 1.4;">
366
+ ${taskData.prompt}
367
+ </div>
368
+ </div>
369
+ </div>
370
+ ` : ''}
371
+ </div>
372
+ `;
373
+ }
374
+
375
+ // Handle regular agent events
376
+ return `
377
+ <div class="structured-view-section">
378
+ <div class="structured-data">
379
+ ${this.createProperty('Agent Type', data.agent_type || data.subagent_type || 'Unknown')}
380
+ ${this.createProperty('Name', data.name || 'N/A')}
381
+ ${this.createProperty('Phase', event.subtype || 'N/A')}
382
+ ${data.config ? this.createProperty('Config', typeof data.config === 'object' ? Object.keys(data.config).join(', ') : String(data.config)) : ''}
383
+ ${data.capabilities ? this.createProperty('Capabilities', data.capabilities.join(', ')) : ''}
384
+ ${data.result ? this.createProperty('Result', typeof data.result === 'object' ? '[Object]' : String(data.result)) : ''}
385
+ </div>
386
+ </div>
387
+ `;
388
+ }
389
+
390
+ /**
391
+ * Create hook-specific structured view
392
+ */
393
+ createHookStructuredView(event) {
394
+ const data = event.data || {};
395
+
396
+ // Extract file path information from tool parameters
397
+ const filePath = this.extractFilePathFromHook(data);
398
+ const toolInfo = this.extractToolInfoFromHook(data);
399
+
400
+ // Note: Git diff functionality moved to Files tab only
401
+ // Events tab no longer shows git diff buttons
402
+
403
+ // Create inline tool result content if available (without separate section header)
404
+ const toolResultContent = this.createInlineToolResultContent(data, event);
405
+
406
+ return `
407
+ <div class="structured-view-section">
408
+ <div class="structured-data">
409
+ ${this.createProperty('Hook Name', this.getHookDisplayName(event, data))}
410
+ ${this.createProperty('Event Type', data.event_type || event.subtype || 'N/A')}
411
+ ${filePath ? this.createProperty('File Path', filePath) : ''}
412
+ ${toolInfo.tool_name ? this.createProperty('Tool', toolInfo.tool_name) : ''}
413
+ ${toolInfo.operation_type ? this.createProperty('Operation', toolInfo.operation_type) : ''}
414
+ ${data.session_id ? this.createProperty('Session ID', data.session_id) : ''}
415
+ ${data.working_directory ? this.createProperty('Working Directory', data.working_directory) : ''}
416
+ ${data.duration_ms ? this.createProperty('Duration', `${data.duration_ms}ms`) : ''}
417
+ ${toolResultContent}
418
+ </div>
419
+ </div>
420
+ `;
421
+ }
422
+
423
+ /**
424
+ * Create inline tool result content (no separate section header)
425
+ * @param {Object} data - Event data
426
+ * @param {Object} event - Full event object (optional, for phase checking)
427
+ * @returns {string} HTML content for inline tool result display
428
+ */
429
+ createInlineToolResultContent(data, event = null) {
430
+ const resultSummary = data.result_summary;
431
+
432
+ // Determine if this is a post-tool event
433
+ // Check multiple possible locations for the event phase
434
+ const eventPhase = event?.subtype || data.event_type || data.phase;
435
+ const isPostTool = eventPhase === 'post_tool' || eventPhase?.includes('post');
436
+
437
+ // Debug logging to help troubleshoot tool result display issues
438
+ if (window.DEBUG_TOOL_RESULTS) {
439
+ console.log('🔧 createInlineToolResultContent debug:', {
440
+ hasResultSummary: !!resultSummary,
441
+ eventPhase,
442
+ isPostTool,
443
+ eventSubtype: event?.subtype,
444
+ dataEventType: data.event_type,
445
+ dataPhase: data.phase,
446
+ toolName: data.tool_name,
447
+ resultSummaryKeys: resultSummary ? Object.keys(resultSummary) : []
448
+ });
449
+ }
450
+
451
+ // Only show results if we have result data and this is a post-tool event
452
+ // OR if we have result_summary regardless of phase (some events may not have proper phase info)
453
+ if (!resultSummary) {
454
+ return '';
455
+ }
456
+
457
+ // If we know this is a pre-tool event, don't show results
458
+ if (eventPhase === 'pre_tool' || (eventPhase?.includes('pre') && !eventPhase?.includes('post'))) {
459
+ return '';
460
+ }
461
+
462
+ let resultContent = '';
463
+
464
+ // Add output preview if available
465
+ if (resultSummary.has_output && resultSummary.output_preview) {
466
+ resultContent += `
467
+ ${this.createProperty('Output', this.truncateText(resultSummary.output_preview, 200))}
468
+ ${resultSummary.output_lines ? this.createProperty('Output Lines', resultSummary.output_lines) : ''}
469
+ `;
470
+ }
471
+
472
+ // Add error preview if available
473
+ if (resultSummary.has_error && resultSummary.error_preview) {
474
+ resultContent += `
475
+ ${this.createProperty('Error', this.truncateText(resultSummary.error_preview, 200))}
476
+ `;
477
+ }
478
+
479
+ // If no specific output or error, but we have other result info
480
+ if (!resultSummary.has_output && !resultSummary.has_error && Object.keys(resultSummary).length > 3) {
481
+ // Show other result fields
482
+ const otherFields = Object.entries(resultSummary)
483
+ .filter(([key, value]) => !['has_output', 'has_error', 'exit_code'].includes(key) && value !== undefined)
484
+ .map(([key, value]) => this.createProperty(this.formatFieldName(key), String(value)))
485
+ .join('');
486
+
487
+ resultContent += otherFields;
488
+ }
489
+
490
+ return resultContent;
491
+ }
492
+
493
+ /**
494
+ * Create tool result section if result data is available
495
+ * @param {Object} data - Event data
496
+ * @param {Object} event - Full event object (optional, for phase checking)
497
+ * @returns {string} HTML content for tool result section
498
+ */
499
+ createToolResultSection(data, event = null) {
500
+ const resultSummary = data.result_summary;
501
+
502
+ // Determine if this is a post-tool event
503
+ // Check multiple possible locations for the event phase
504
+ const eventPhase = event?.subtype || data.event_type || data.phase;
505
+ const isPostTool = eventPhase === 'post_tool' || eventPhase?.includes('post');
506
+
507
+ // Debug logging to help troubleshoot tool result display issues
508
+ if (window.DEBUG_TOOL_RESULTS) {
509
+ console.log('🔧 createToolResultSection debug:', {
510
+ hasResultSummary: !!resultSummary,
511
+ eventPhase,
512
+ isPostTool,
513
+ eventSubtype: event?.subtype,
514
+ dataEventType: data.event_type,
515
+ dataPhase: data.phase,
516
+ toolName: data.tool_name,
517
+ resultSummaryKeys: resultSummary ? Object.keys(resultSummary) : []
518
+ });
519
+ }
520
+
521
+ // Only show results if we have result data and this is a post-tool event
522
+ // OR if we have result_summary regardless of phase (some events may not have proper phase info)
523
+ if (!resultSummary) {
524
+ return '';
525
+ }
526
+
527
+ // If we know this is a pre-tool event, don't show results
528
+ if (eventPhase === 'pre_tool' || (eventPhase?.includes('pre') && !eventPhase?.includes('post'))) {
529
+ return '';
530
+ }
531
+
532
+ // Determine result status and icon
533
+ let statusIcon = '⏳';
534
+ let statusClass = 'tool-running';
535
+ let statusText = 'Unknown';
536
+
537
+ if (data.success === true) {
538
+ statusIcon = '✅';
539
+ statusClass = 'tool-success';
540
+ statusText = 'Success';
541
+ } else if (data.success === false) {
542
+ statusIcon = '❌';
543
+ statusClass = 'tool-failure';
544
+ statusText = 'Failed';
545
+ } else if (data.exit_code === 0) {
546
+ statusIcon = '✅';
547
+ statusClass = 'tool-success';
548
+ statusText = 'Completed';
549
+ } else if (data.exit_code === 2) {
550
+ statusIcon = '⚠️';
551
+ statusClass = 'tool-blocked';
552
+ statusText = 'Blocked';
553
+ } else if (data.exit_code !== undefined && data.exit_code !== 0) {
554
+ statusIcon = '❌';
555
+ statusClass = 'tool-failure';
556
+ statusText = 'Error';
557
+ }
558
+
559
+ let resultContent = '';
560
+
561
+ // Add basic result info
562
+ resultContent += `
563
+ <div class="tool-result-status ${statusClass}">
564
+ <span class="tool-result-icon">${statusIcon}</span>
565
+ <span class="tool-result-text">${statusText}</span>
566
+ ${data.exit_code !== undefined ? `<span class="tool-exit-code">Exit Code: ${data.exit_code}</span>` : ''}
567
+ </div>
568
+ `;
569
+
570
+ // Add output preview if available
571
+ if (resultSummary.has_output && resultSummary.output_preview) {
572
+ resultContent += `
573
+ <div class="tool-result-output">
574
+ <div class="tool-result-label">📄 Output:</div>
575
+ <div class="tool-result-preview">
576
+ <pre>${this.escapeHtml(resultSummary.output_preview)}</pre>
577
+ </div>
578
+ ${resultSummary.output_lines ? `<div class="tool-result-meta">Lines: ${resultSummary.output_lines}</div>` : ''}
579
+ </div>
580
+ `;
581
+ }
582
+
583
+ // Add error preview if available
584
+ if (resultSummary.has_error && resultSummary.error_preview) {
585
+ resultContent += `
586
+ <div class="tool-result-error">
587
+ <div class="tool-result-label">⚠️ Error:</div>
588
+ <div class="tool-result-preview error-preview">
589
+ <pre>${this.escapeHtml(resultSummary.error_preview)}</pre>
590
+ </div>
591
+ </div>
592
+ `;
593
+ }
594
+
595
+ // If no specific output or error, but we have other result info
596
+ if (!resultSummary.has_output && !resultSummary.has_error && Object.keys(resultSummary).length > 3) {
597
+ // Show other result fields
598
+ const otherFields = Object.entries(resultSummary)
599
+ .filter(([key, value]) => !['has_output', 'has_error', 'exit_code'].includes(key) && value !== undefined)
600
+ .map(([key, value]) => this.createProperty(this.formatFieldName(key), String(value)))
601
+ .join('');
602
+
603
+ if (otherFields) {
604
+ resultContent += `
605
+ <div class="tool-result-other">
606
+ <div class="tool-result-label">📊 Result Details:</div>
607
+ <div class="structured-data">
608
+ ${otherFields}
609
+ </div>
610
+ </div>
611
+ `;
612
+ }
613
+ }
614
+
615
+ // Only return content if we have something to show
616
+ if (!resultContent.trim()) {
617
+ return '';
618
+ }
619
+
620
+ return `
621
+ <div class="tool-result-section">
622
+ <div class="contextual-header">
623
+ <h3 class="contextual-header-text">🔧 Tool Result</h3>
624
+ </div>
625
+ <div class="tool-result-content">
626
+ ${resultContent}
627
+ </div>
628
+ </div>
629
+ `;
630
+ }
631
+
632
+ /**
633
+ * Check if this is a write operation that modifies files
634
+ * @param {string} toolName - Name of the tool used
635
+ * @param {Object} data - Event data
636
+ * @returns {boolean} True if this is a write operation
637
+ */
638
+ isWriteOperation(toolName, data) {
639
+ // Common write operation tool names
640
+ const writeTools = [
641
+ 'Write',
642
+ 'Edit',
643
+ 'MultiEdit',
644
+ 'NotebookEdit'
645
+ ];
646
+
647
+ if (writeTools.includes(toolName)) {
648
+ return true;
649
+ }
650
+
651
+ // Check for write-related parameters in the data
652
+ if (data.tool_parameters) {
653
+ const params = data.tool_parameters;
654
+
655
+ // Check for content or editing parameters
656
+ if (params.content || params.new_string || params.edits) {
657
+ return true;
658
+ }
659
+
660
+ // Check for file modification indicators
661
+ if (params.edit_mode && params.edit_mode !== 'read') {
662
+ return true;
663
+ }
664
+ }
665
+
666
+ // Check event subtype for write operations
667
+ if (data.event_type === 'post_tool' || data.event_type === 'pre_tool') {
668
+ // Additional heuristics based on tool usage patterns
669
+ if (toolName && (
670
+ toolName.toLowerCase().includes('write') ||
671
+ toolName.toLowerCase().includes('edit') ||
672
+ toolName.toLowerCase().includes('modify')
673
+ )) {
674
+ return true;
675
+ }
676
+ }
677
+
678
+ return false;
679
+ }
680
+
681
+ /**
682
+ * Determines if a file operation is read-only or modifies the file
683
+ * @param {string} operation - The operation type
684
+ * @returns {boolean} True if operation is read-only, false if it modifies the file
685
+ */
686
+ isReadOnlyOperation(operation) {
687
+ if (!operation) return true; // Default to read-only for safety
688
+
689
+ const readOnlyOperations = ['read'];
690
+ const editOperations = ['write', 'edit', 'multiedit', 'create', 'delete', 'move', 'copy'];
691
+
692
+ const opLower = operation.toLowerCase();
693
+
694
+ // Explicitly read-only operations
695
+ if (readOnlyOperations.includes(opLower)) {
696
+ return true;
697
+ }
698
+
699
+ // Explicitly edit operations
700
+ if (editOperations.includes(opLower)) {
701
+ return false;
702
+ }
703
+
704
+ // Default to read-only for unknown operations
705
+ return true;
706
+ }
707
+
708
+ /**
709
+ * Create todo-specific structured view
710
+ */
711
+ createTodoStructuredView(event) {
712
+ const data = event.data || {};
713
+
714
+ let content = '';
715
+
716
+ // Add todo checklist if available - start directly with checklist
717
+ if (data.todos && Array.isArray(data.todos)) {
718
+ content += `
719
+ <div class="todo-checklist">
720
+ ${data.todos.map(todo => `
721
+ <div class="todo-item todo-${todo.status || 'pending'}">
722
+ <span class="todo-status">${this.getTodoStatusIcon(todo.status)}</span>
723
+ <span class="todo-content">${todo.content || 'No content'}</span>
724
+ <span class="todo-priority priority-${todo.priority || 'medium'}">${this.getTodoPriorityIcon(todo.priority)}</span>
725
+ </div>
726
+ `).join('')}
727
+ </div>
728
+ `;
729
+ }
730
+
731
+ return content;
732
+ }
733
+
734
+ /**
735
+ * Create memory-specific structured view
736
+ */
737
+ createMemoryStructuredView(event) {
738
+ const data = event.data || {};
739
+
740
+ return `
741
+ <div class="structured-view-section">
742
+ <div class="structured-data">
743
+ ${this.createProperty('Operation', data.operation || 'Unknown')}
744
+ ${this.createProperty('Key', data.key || 'N/A')}
745
+ ${data.value ? this.createProperty('Value', typeof data.value === 'object' ? '[Object]' : String(data.value)) : ''}
746
+ ${data.namespace ? this.createProperty('Namespace', data.namespace) : ''}
747
+ ${data.metadata ? this.createProperty('Metadata', typeof data.metadata === 'object' ? '[Object]' : String(data.metadata)) : ''}
748
+ </div>
749
+ </div>
750
+ `;
751
+ }
752
+
753
+ /**
754
+ * Create Claude-specific structured view
755
+ */
756
+ createClaudeStructuredView(event) {
757
+ const data = event.data || {};
758
+
759
+ return `
760
+ <div class="structured-view-section">
761
+ <div class="structured-data">
762
+ ${this.createProperty('Type', event.subtype || 'N/A')}
763
+ ${data.prompt ? this.createProperty('Prompt', this.truncateText(data.prompt, 200)) : ''}
764
+ ${data.message ? this.createProperty('Message', this.truncateText(data.message, 200)) : ''}
765
+ ${data.response ? this.createProperty('Response', this.truncateText(data.response, 200)) : ''}
766
+ ${data.content ? this.createProperty('Content', this.truncateText(data.content, 200)) : ''}
767
+ ${data.tokens ? this.createProperty('Tokens', data.tokens) : ''}
768
+ ${data.model ? this.createProperty('Model', data.model) : ''}
769
+ </div>
770
+ </div>
771
+ `;
772
+ }
773
+
774
+ /**
775
+ * Create session-specific structured view
776
+ */
777
+ createSessionStructuredView(event) {
778
+ const data = event.data || {};
779
+
780
+ return `
781
+ <div class="structured-view-section">
782
+ <div class="structured-data">
783
+ ${this.createProperty('Action', event.subtype || 'N/A')}
784
+ ${this.createProperty('Session ID', data.session_id || 'N/A')}
785
+ ${data.working_directory ? this.createProperty('Working Dir', data.working_directory) : ''}
786
+ ${data.git_branch ? this.createProperty('Git Branch', data.git_branch) : ''}
787
+ ${data.agent_type ? this.createProperty('Agent Type', data.agent_type) : ''}
788
+ </div>
789
+ </div>
790
+ `;
791
+ }
792
+
793
+ /**
794
+ * Create generic structured view
795
+ */
796
+ createGenericStructuredView(event) {
797
+ const data = event.data || {};
798
+ const keys = Object.keys(data);
799
+
800
+ if (keys.length === 0) {
801
+ return '';
802
+ }
803
+
804
+ return `
805
+ <div class="structured-view-section">
806
+ <div class="structured-data">
807
+ ${keys.map(key =>
808
+ this.createProperty(key, typeof data[key] === 'object' ?
809
+ '[Object]' : String(data[key]))
810
+ ).join('')}
811
+ </div>
812
+ </div>
813
+ `;
814
+ }
815
+
816
+ /**
817
+ * Create collapsible JSON section that appears below main content
818
+ * @param {Object} event - The event to render
819
+ * @returns {string} HTML content
820
+ */
821
+ createCollapsibleJsonSection(event) {
822
+ const uniqueId = 'json-section-' + Math.random().toString(36).substr(2, 9);
823
+ const jsonString = this.formatJSON(event);
824
+ return `
825
+ <div class="collapsible-json-section" id="${uniqueId}">
826
+ <div class="json-toggle-header"
827
+ onclick="window.moduleViewer.toggleJsonSection()"
828
+ role="button"
829
+ tabindex="0"
830
+ aria-expanded="false"
831
+ onkeydown="if(event.key==='Enter'||event.key===' '){window.moduleViewer.toggleJsonSection();event.preventDefault();}">
832
+ <span class="json-toggle-text">Raw JSON</span>
833
+ <span class="json-toggle-arrow">▼</span>
834
+ </div>
835
+ <div class="json-content-collapsible" style="display: none;" aria-hidden="true">
836
+ <div class="json-display" onclick="window.moduleViewer.copyJsonToClipboard(event)">
837
+ <pre>${jsonString}</pre>
838
+ </div>
839
+ </div>
840
+ </div>
841
+ `;
842
+ }
843
+
844
+ /**
845
+ * Copy JSON content to clipboard
846
+ * @param {Event} event - Click event
847
+ */
848
+ async copyJsonToClipboard(event) {
849
+ // Only trigger on the copy icon area (top-right corner)
850
+ const rect = event.currentTarget.getBoundingClientRect();
851
+ const clickX = event.clientX - rect.left;
852
+ const clickY = event.clientY - rect.top;
853
+
854
+ // Check if click is in the top-right corner (copy icon area)
855
+ if (clickX > rect.width - 50 && clickY < 30) {
856
+ const preElement = event.currentTarget.querySelector('pre');
857
+ if (preElement) {
858
+ try {
859
+ await navigator.clipboard.writeText(preElement.textContent);
860
+ this.showNotification('JSON copied to clipboard', 'success');
861
+ } catch (err) {
862
+ console.error('Failed to copy JSON:', err);
863
+ this.showNotification('Failed to copy JSON', 'error');
864
+ }
865
+ }
866
+ event.stopPropagation();
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Initialize JSON toggle functionality
872
+ */
873
+ initializeJsonToggle() {
874
+ // Make sure the moduleViewer is available globally for onclick handlers
875
+ window.moduleViewer = this;
876
+
877
+ // Add keyboard navigation support
878
+ document.addEventListener('keydown', (e) => {
879
+ if (e.target.classList.contains('json-toggle-header')) {
880
+ if (e.key === 'Enter' || e.key === ' ') {
881
+ this.toggleJsonSection();
882
+ e.preventDefault();
883
+ }
884
+ }
885
+ });
886
+ }
887
+
888
+ /**
889
+ * Toggle JSON section visibility with smooth animation
890
+ */
891
+ toggleJsonSection() {
892
+ const jsonContent = document.querySelector('.json-content-collapsible');
893
+ const arrow = document.querySelector('.json-toggle-arrow');
894
+ const toggleHeader = document.querySelector('.json-toggle-header');
895
+
896
+ if (!jsonContent || !arrow) return;
897
+
898
+ const isHidden = jsonContent.style.display === 'none' || !jsonContent.style.display;
899
+
900
+ if (isHidden) {
901
+ // Show JSON content
902
+ jsonContent.style.display = 'block';
903
+ arrow.textContent = '▲';
904
+ toggleHeader.setAttribute('aria-expanded', 'true');
905
+
906
+ // Scroll the new content into view if needed
907
+ setTimeout(() => {
908
+ jsonContent.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
909
+ }, 100);
910
+ } else {
911
+ // Hide JSON content
912
+ jsonContent.style.display = 'none';
913
+ arrow.textContent = '▼';
914
+ toggleHeader.setAttribute('aria-expanded', 'false');
915
+ }
916
+ }
917
+
918
+ /**
919
+ * Create a property display element with optional file path detection
920
+ */
921
+ createProperty(key, value) {
922
+ const displayValue = this.truncateText(String(value), 300);
923
+
924
+ // Check if this is a file path property that should be clickable
925
+ if (this.isFilePathProperty(key, value)) {
926
+ return `
927
+ <div class="event-property">
928
+ <span class="event-property-key">${key}:</span>
929
+ <span class="event-property-value">
930
+ ${this.createClickableFilePath(value)}
931
+ </span>
932
+ </div>
933
+ `;
934
+ }
935
+
936
+ return `
937
+ <div class="event-property">
938
+ <span class="event-property-key">${key}:</span>
939
+ <span class="event-property-value">${displayValue}</span>
940
+ </div>
941
+ `;
942
+ }
943
+
944
+ /**
945
+ * Check if a property represents a file path that should be clickable
946
+ * @param {string} key - Property key
947
+ * @param {string} value - Property value
948
+ * @returns {boolean} True if this should be a clickable file path
949
+ */
950
+ isFilePathProperty(key, value) {
951
+ const filePathKeys = [
952
+ 'File Path',
953
+ 'file_path',
954
+ 'notebook_path',
955
+ 'Full Path',
956
+ 'Working Directory',
957
+ 'working_directory'
958
+ ];
959
+
960
+ // Check if key indicates a file path
961
+ if (filePathKeys.some(pathKey => key.toLowerCase().includes(pathKey.toLowerCase()))) {
962
+ // Ensure value looks like a file path (contains / or \\ and has reasonable length)
963
+ const strValue = String(value);
964
+ return strValue.length > 0 &&
965
+ (strValue.includes('/') || strValue.includes('\\')) &&
966
+ strValue.length < 500; // Reasonable path length limit
967
+ }
968
+
969
+ return false;
970
+ }
971
+
972
+ /**
973
+ * Create a clickable file path element
974
+ * @param {string} filePath - The file path to make clickable
975
+ * @returns {string} HTML for clickable file path
976
+ */
977
+ createClickableFilePath(filePath) {
978
+ const displayPath = this.truncateText(String(filePath), 300);
979
+ const escapedPath = filePath.replace(/'/g, "\\'");
980
+
981
+ return `
982
+ <span class="clickable-file-path"
983
+ onclick="showFileViewerModal('${escapedPath}')"
984
+ title="Click to view file contents with syntax highlighting&#10;Path: ${filePath}">
985
+ ${displayPath}
986
+ </span>
987
+ `;
988
+ }
989
+
990
+ /**
991
+ * Get icon for event type
992
+ */
993
+ getEventIcon(eventType) {
994
+ const icons = {
995
+ session: '📱',
996
+ claude: '🤖',
997
+ agent: '🎯',
998
+ hook: '🔗',
999
+ todo: '✅',
1000
+ memory: '🧠',
1001
+ log: '📝',
1002
+ connection: '🔌',
1003
+ unknown: '❓'
1004
+ };
1005
+ return icons[eventType] || icons.unknown;
1006
+ }
1007
+
1008
+ /**
1009
+ * Get todo status icon
1010
+ */
1011
+ getTodoStatusIcon(status) {
1012
+ const icons = {
1013
+ completed: '✅',
1014
+ 'in_progress': '🔄',
1015
+ pending: '⏳',
1016
+ cancelled: '❌'
1017
+ };
1018
+ return icons[status] || icons.pending;
1019
+ }
1020
+
1021
+ /**
1022
+ * Get todo priority icon
1023
+ */
1024
+ getTodoPriorityIcon(priority) {
1025
+ const icons = {
1026
+ high: '🔴',
1027
+ medium: '🟡',
1028
+ low: '🟢'
1029
+ };
1030
+ return icons[priority] || icons.medium;
1031
+ }
1032
+
1033
+ /**
1034
+ * Get meaningful hook display name from event data
1035
+ */
1036
+ getHookDisplayName(event, data) {
1037
+ // First check if there's a specific hook name in the data
1038
+ if (data.hook_name) return data.hook_name;
1039
+ if (data.name) return data.name;
1040
+
1041
+ // Use event.subtype or data.event_type to determine hook name
1042
+ const eventType = event.subtype || data.event_type;
1043
+
1044
+ // Map hook event types to meaningful display names
1045
+ const hookNames = {
1046
+ 'user_prompt': 'User Prompt',
1047
+ 'pre_tool': 'Tool Execution (Pre)',
1048
+ 'post_tool': 'Tool Execution (Post)',
1049
+ 'notification': 'Notification',
1050
+ 'stop': 'Session Stop',
1051
+ 'subagent_stop': 'Subagent Stop'
1052
+ };
1053
+
1054
+ if (hookNames[eventType]) {
1055
+ return hookNames[eventType];
1056
+ }
1057
+
1058
+ // If it's a compound event type like "hook.user_prompt", extract the part after "hook."
1059
+ if (typeof event.type === 'string' && event.type.startsWith('hook.')) {
1060
+ const hookType = event.type.replace('hook.', '');
1061
+ if (hookNames[hookType]) {
1062
+ return hookNames[hookType];
1063
+ }
1064
+ }
1065
+
1066
+ // Fallback to formatting the event type nicely
1067
+ if (eventType) {
1068
+ return eventType.split('_')
1069
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1070
+ .join(' ');
1071
+ }
1072
+
1073
+ return 'Unknown Hook';
1074
+ }
1075
+
1076
+ /**
1077
+ * Extract file path from hook event data
1078
+ */
1079
+ extractFilePathFromHook(data) {
1080
+ // Check tool parameters for file path
1081
+ if (data.tool_parameters && data.tool_parameters.file_path) {
1082
+ return data.tool_parameters.file_path;
1083
+ }
1084
+
1085
+ // Check direct file_path field
1086
+ if (data.file_path) {
1087
+ return data.file_path;
1088
+ }
1089
+
1090
+ // Check nested in other common locations
1091
+ if (data.tool_input && data.tool_input.file_path) {
1092
+ return data.tool_input.file_path;
1093
+ }
1094
+
1095
+ // Check for notebook path (alternative field name)
1096
+ if (data.tool_parameters && data.tool_parameters.notebook_path) {
1097
+ return data.tool_parameters.notebook_path;
1098
+ }
1099
+
1100
+ return null;
1101
+ }
1102
+
1103
+ /**
1104
+ * Extract tool information from hook event data
1105
+ */
1106
+ extractToolInfoFromHook(data) {
1107
+ return {
1108
+ tool_name: data.tool_name || (data.tool_parameters && data.tool_parameters.tool_name),
1109
+ operation_type: data.operation_type || (data.tool_parameters && data.tool_parameters.operation_type)
1110
+ };
1111
+ }
1112
+
1113
+ /**
1114
+ * Truncate text to specified length
1115
+ */
1116
+ truncateText(text, maxLength) {
1117
+ if (!text || text.length <= maxLength) return text;
1118
+ return text.substring(0, maxLength) + '...';
1119
+ }
1120
+
1121
+ /**
1122
+ * Format JSON for display
1123
+ */
1124
+ formatJSON(obj) {
1125
+ try {
1126
+ return JSON.stringify(obj, null, 2);
1127
+ } catch (e) {
1128
+ return String(obj);
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * Format timestamp for display
1134
+ * @param {string|number} timestamp - Timestamp to format
1135
+ * @returns {string} Formatted time
1136
+ */
1137
+ formatTimestamp(timestamp) {
1138
+ if (!timestamp) return 'Unknown time';
1139
+
1140
+ try {
1141
+ const date = new Date(timestamp);
1142
+ return date.toLocaleTimeString('en-US', {
1143
+ hour: 'numeric',
1144
+ minute: '2-digit',
1145
+ second: '2-digit',
1146
+ hour12: true
1147
+ });
1148
+ } catch (e) {
1149
+ return 'Invalid time';
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Escape HTML characters to prevent XSS
1155
+ * @param {string} text - Text to escape
1156
+ * @returns {string} Escaped text
1157
+ */
1158
+ escapeHtml(text) {
1159
+ if (!text) return '';
1160
+ const div = document.createElement('div');
1161
+ div.textContent = text;
1162
+ return div.innerHTML;
1163
+ }
1164
+
1165
+ /**
1166
+ * Format field name for display (convert snake_case to Title Case)
1167
+ * @param {string} fieldName - Field name to format
1168
+ * @returns {string} Formatted field name
1169
+ */
1170
+ formatFieldName(fieldName) {
1171
+ return fieldName
1172
+ .split('_')
1173
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1174
+ .join(' ');
1175
+ }
1176
+
1177
+ /**
1178
+ * Extract tool name from event data
1179
+ * @param {Object} data - Event data
1180
+ * @returns {string|null} Tool name
1181
+ */
1182
+ extractToolName(data) {
1183
+ // Check various locations where tool name might be stored
1184
+ if (data.tool_name) return data.tool_name;
1185
+ if (data.tool_parameters && data.tool_parameters.tool_name) return data.tool_parameters.tool_name;
1186
+ if (data.tool_input && data.tool_input.tool_name) return data.tool_input.tool_name;
1187
+
1188
+ // Try to infer from other fields
1189
+ if (data.tool_parameters) {
1190
+ // Common tool patterns
1191
+ if (data.tool_parameters.file_path || data.tool_parameters.notebook_path) {
1192
+ return 'FileOperation';
1193
+ }
1194
+ if (data.tool_parameters.pattern) {
1195
+ return 'Search';
1196
+ }
1197
+ if (data.tool_parameters.command) {
1198
+ return 'Bash';
1199
+ }
1200
+ if (data.tool_parameters.todos) {
1201
+ return 'TodoWrite';
1202
+ }
1203
+ }
1204
+
1205
+ return null;
1206
+ }
1207
+
1208
+ /**
1209
+ * Extract agent information from event data
1210
+ * @param {Object} data - Event data
1211
+ * @returns {string|null} Agent identifier
1212
+ */
1213
+ extractAgent(data) {
1214
+ // First check if we have enhanced inference data from dashboard
1215
+ if (data._agentName && data._agentName !== 'Unknown Agent') {
1216
+ return data._agentName;
1217
+ }
1218
+
1219
+ // Check inference data if available
1220
+ if (data._inference && data._inference.agentName && data._inference.agentName !== 'Unknown') {
1221
+ return data._inference.agentName;
1222
+ }
1223
+
1224
+ // Check various locations where agent info might be stored
1225
+ if (data.agent) return data.agent;
1226
+ if (data.agent_type) return data.agent_type;
1227
+ if (data.agent_name) return data.agent_name;
1228
+
1229
+ // Check session data
1230
+ if (data.session_id && typeof data.session_id === 'string') {
1231
+ // Extract agent from session ID if it contains agent info
1232
+ const sessionParts = data.session_id.split('_');
1233
+ if (sessionParts.length > 1) {
1234
+ return sessionParts[0].toUpperCase();
1235
+ }
1236
+ }
1237
+
1238
+ // Infer from context
1239
+ if (data.todos) return 'PM'; // TodoWrite typically from PM agent
1240
+ if (data.tool_name === 'TodoWrite') return 'PM';
1241
+
1242
+ return null;
1243
+ }
1244
+
1245
+ /**
1246
+ * Extract file name from event data
1247
+ * @param {Object} data - Event data
1248
+ * @returns {string|null} File name
1249
+ */
1250
+ extractFileName(data) {
1251
+ const filePath = this.extractFilePathFromHook(data);
1252
+ if (filePath) {
1253
+ // Extract just the filename from the full path
1254
+ const pathParts = filePath.split('/');
1255
+ return pathParts[pathParts.length - 1];
1256
+ }
1257
+
1258
+ // Check other common file fields
1259
+ if (data.filename) return data.filename;
1260
+ if (data.file) return data.file;
1261
+
1262
+ return null;
1263
+ }
1264
+
1265
+ /**
1266
+ * Clear the module viewer
1267
+ */
1268
+ clear() {
1269
+ this.showEmptyState();
1270
+ }
1271
+
1272
+ /**
1273
+ * Show tool call details (backward compatibility method)
1274
+ * @param {Object} toolCall - The tool call data
1275
+ * @param {string} toolCallKey - The tool call key
1276
+ */
1277
+ showToolCall(toolCall, toolCallKey) {
1278
+ if (!toolCall) {
1279
+ this.showEmptyState();
1280
+ return;
1281
+ }
1282
+
1283
+ const toolName = toolCall.tool_name || 'Unknown Tool';
1284
+ const agentName = toolCall.agent_type || 'PM';
1285
+ const timestamp = this.formatTimestamp(toolCall.timestamp);
1286
+
1287
+ // Extract information from pre and post events
1288
+ const preEvent = toolCall.pre_event;
1289
+ const postEvent = toolCall.post_event;
1290
+
1291
+ // Get parameters from pre-event
1292
+ const parameters = preEvent?.tool_parameters || {};
1293
+ const target = preEvent ? this.extractToolTarget(toolName, parameters) : 'Unknown target';
1294
+
1295
+ // Get execution results from post-event
1296
+ const duration = toolCall.duration_ms ? `${toolCall.duration_ms}ms` : '-';
1297
+ const success = toolCall.success !== undefined ? toolCall.success : null;
1298
+ const exitCode = toolCall.exit_code !== undefined ? toolCall.exit_code : null;
1299
+
1300
+ // Format result summary
1301
+ let resultSummary = toolCall.result_summary || 'No summary available';
1302
+ let formattedResultSummary = '';
1303
+
1304
+ if (typeof resultSummary === 'object' && resultSummary !== null) {
1305
+ const parts = [];
1306
+ if (resultSummary.exit_code !== undefined) {
1307
+ parts.push(`Exit Code: ${resultSummary.exit_code}`);
1308
+ }
1309
+ if (resultSummary.has_output !== undefined) {
1310
+ parts.push(`Has Output: ${resultSummary.has_output ? 'Yes' : 'No'}`);
1311
+ }
1312
+ if (resultSummary.has_error !== undefined) {
1313
+ parts.push(`Has Error: ${resultSummary.has_error ? 'Yes' : 'No'}`);
1314
+ }
1315
+ if (resultSummary.output_lines !== undefined) {
1316
+ parts.push(`Output Lines: ${resultSummary.output_lines}`);
1317
+ }
1318
+ if (resultSummary.output_preview) {
1319
+ parts.push(`Output Preview: ${resultSummary.output_preview}`);
1320
+ }
1321
+ if (resultSummary.error_preview) {
1322
+ parts.push(`Error Preview: ${resultSummary.error_preview}`);
1323
+ }
1324
+ formattedResultSummary = parts.join('\n');
1325
+ } else {
1326
+ formattedResultSummary = String(resultSummary);
1327
+ }
1328
+
1329
+ // Status information
1330
+ let statusIcon = '⏳';
1331
+ let statusText = 'Running...';
1332
+ let statusClass = 'tool-running';
1333
+
1334
+ if (postEvent) {
1335
+ if (success === true) {
1336
+ statusIcon = '✅';
1337
+ statusText = 'Success';
1338
+ statusClass = 'tool-success';
1339
+ } else if (success === false) {
1340
+ statusIcon = '❌';
1341
+ statusText = 'Failed';
1342
+ statusClass = 'tool-failure';
1343
+ } else {
1344
+ statusIcon = '⏳';
1345
+ statusText = 'Completed';
1346
+ statusClass = 'tool-completed';
1347
+ }
1348
+ }
1349
+
1350
+ // Create contextual header
1351
+ const contextualHeader = `
1352
+ <div class="contextual-header">
1353
+ <h3 class="contextual-header-text">${toolName}: ${agentName} ${timestamp}</h3>
1354
+ </div>
1355
+ `;
1356
+
1357
+ // Special handling for TodoWrite
1358
+ if (toolName === 'TodoWrite' && parameters.todos) {
1359
+ const todoContent = `
1360
+ <div class="todo-checklist">
1361
+ ${parameters.todos.map(todo => {
1362
+ const statusIcon = this.getTodoStatusIcon(todo.status);
1363
+ const priorityIcon = this.getTodoPriorityIcon(todo.priority);
1364
+
1365
+ return `
1366
+ <div class="todo-item todo-${todo.status || 'pending'}">
1367
+ <span class="todo-status">${statusIcon}</span>
1368
+ <span class="todo-content">${todo.content || 'No content'}</span>
1369
+ <span class="todo-priority priority-${todo.priority || 'medium'}">${priorityIcon}</span>
1370
+ </div>
1371
+ `;
1372
+ }).join('')}
1373
+ </div>
1374
+ `;
1375
+
1376
+ // Create collapsible JSON section
1377
+ const toolCallData = {
1378
+ toolCall: toolCall,
1379
+ preEvent: preEvent,
1380
+ postEvent: postEvent
1381
+ };
1382
+ const collapsibleJsonSection = this.createCollapsibleJsonSection(toolCallData);
1383
+
1384
+ if (this.dataContainer) {
1385
+ this.dataContainer.innerHTML = contextualHeader + todoContent + collapsibleJsonSection;
1386
+ }
1387
+
1388
+ // Initialize JSON toggle functionality
1389
+ this.initializeJsonToggle();
1390
+ } else {
1391
+ // For other tools, show detailed information
1392
+ const content = `
1393
+ <div class="structured-view-section">
1394
+ <div class="tool-call-details">
1395
+ <div class="tool-call-info ${statusClass}">
1396
+ <div class="structured-field">
1397
+ <strong>Tool Name:</strong> ${toolName}
1398
+ </div>
1399
+ <div class="structured-field">
1400
+ <strong>Agent:</strong> ${agentName}
1401
+ </div>
1402
+ <div class="structured-field">
1403
+ <strong>Status:</strong> ${statusIcon} ${statusText}
1404
+ </div>
1405
+ <div class="structured-field">
1406
+ <strong>Target:</strong> ${target}
1407
+ </div>
1408
+ <div class="structured-field">
1409
+ <strong>Started:</strong> ${new Date(toolCall.timestamp).toLocaleString()}
1410
+ </div>
1411
+ ${duration && duration !== '-' ? `
1412
+ <div class="structured-field">
1413
+ <strong>Duration:</strong> ${duration}
1414
+ </div>
1415
+ ` : ''}
1416
+ ${toolCall.session_id ? `
1417
+ <div class="structured-field">
1418
+ <strong>Session ID:</strong> ${toolCall.session_id}
1419
+ </div>
1420
+ ` : ''}
1421
+ </div>
1422
+
1423
+ ${this.createToolResultFromToolCall(toolCall)}
1424
+ </div>
1425
+ </div>
1426
+ `;
1427
+
1428
+ // Create collapsible JSON section
1429
+ const toolCallData = {
1430
+ toolCall: toolCall,
1431
+ preEvent: preEvent,
1432
+ postEvent: postEvent
1433
+ };
1434
+ const collapsibleJsonSection = this.createCollapsibleJsonSection(toolCallData);
1435
+
1436
+ if (this.dataContainer) {
1437
+ this.dataContainer.innerHTML = contextualHeader + content + collapsibleJsonSection;
1438
+ }
1439
+
1440
+ // Initialize JSON toggle functionality
1441
+ this.initializeJsonToggle();
1442
+ }
1443
+
1444
+ // Hide JSON pane since data is integrated above
1445
+ // JSON container no longer exists - handled via collapsible sections
1446
+ }
1447
+
1448
+ /**
1449
+ * Show file operations details (backward compatibility method)
1450
+ * @param {Object} fileData - The file operations data
1451
+ * @param {string} filePath - The file path
1452
+ */
1453
+ showFileOperations(fileData, filePath) {
1454
+ if (!fileData || !filePath) {
1455
+ this.showEmptyState();
1456
+ return;
1457
+ }
1458
+
1459
+ // Get file name from path for header
1460
+ const fileName = filePath.split('/').pop() || filePath;
1461
+ const operations = fileData.operations || [];
1462
+ const lastOp = operations[operations.length - 1];
1463
+ const headerTimestamp = lastOp ? this.formatTimestamp(lastOp.timestamp) : '';
1464
+
1465
+ // Create contextual header
1466
+ const contextualHeader = `
1467
+ <div class="contextual-header">
1468
+ <h3 class="contextual-header-text">File: ${fileName} ${headerTimestamp}</h3>
1469
+ </div>
1470
+ `;
1471
+
1472
+ const content = `
1473
+ <div class="structured-view-section">
1474
+ <div class="file-details">
1475
+ <div class="file-path-display">
1476
+ <strong>Full Path:</strong> ${this.createClickableFilePath(filePath)}
1477
+ <div id="git-track-status-${filePath.replace(/[^a-zA-Z0-9]/g, '-')}" class="git-track-status" style="margin-top: 8px;">
1478
+ <!-- Git tracking status will be populated here -->
1479
+ </div>
1480
+ </div>
1481
+ <div class="operations-list">
1482
+ ${operations.map(op => `
1483
+ <div class="operation-item">
1484
+ <div class="operation-header">
1485
+ <span class="operation-icon">${this.getOperationIcon(op.operation)}</span>
1486
+ <span class="operation-type">${op.operation}</span>
1487
+ <span class="operation-timestamp">${new Date(op.timestamp).toLocaleString()}</span>
1488
+ ${this.isReadOnlyOperation(op.operation) ? `
1489
+ <!-- Read-only operation: show only file viewer -->
1490
+ <span class="file-viewer-icon"
1491
+ onclick="showFileViewerModal('${filePath}')"
1492
+ title="View file contents with syntax highlighting"
1493
+ style="margin-left: 8px; cursor: pointer; font-size: 16px;">
1494
+ 👁️
1495
+ </span>
1496
+ ` : `
1497
+ <!-- Edit operation: show both file viewer and git diff -->
1498
+ <span class="file-viewer-icon"
1499
+ onclick="showFileViewerModal('${filePath}')"
1500
+ title="View file contents with syntax highlighting"
1501
+ style="margin-left: 8px; cursor: pointer; font-size: 16px;">
1502
+ 👁️
1503
+ </span>
1504
+ <span class="git-diff-icon"
1505
+ onclick="showGitDiffModal('${filePath}', '${op.timestamp}')"
1506
+ title="View git diff for this file operation"
1507
+ style="margin-left: 8px; cursor: pointer; font-size: 16px; display: none;"
1508
+ data-file-path="${filePath}"
1509
+ data-operation-timestamp="${op.timestamp}">
1510
+ 📋
1511
+ </span>
1512
+ `}
1513
+ </div>
1514
+ <div class="operation-details">
1515
+ <strong>Agent:</strong> ${op.agent}<br>
1516
+ <strong>Session:</strong> ${op.sessionId ? op.sessionId.substring(0, 8) + '...' : 'Unknown'}
1517
+ ${op.details ? `<br><strong>Details:</strong> ${op.details}` : ''}
1518
+ </div>
1519
+ </div>
1520
+ `).join('')}
1521
+ </div>
1522
+ </div>
1523
+ </div>
1524
+ `;
1525
+
1526
+ // Check git tracking status and show track control if needed
1527
+ this.checkAndShowTrackControl(filePath);
1528
+
1529
+ // Check git status and conditionally show git diff icons
1530
+ this.checkAndShowGitDiffIcons(filePath);
1531
+
1532
+ // Create collapsible JSON section for file data
1533
+ const collapsibleJsonSection = this.createCollapsibleJsonSection(fileData);
1534
+
1535
+ // Show structured data with JSON section in data pane
1536
+ if (this.dataContainer) {
1537
+ this.dataContainer.innerHTML = contextualHeader + content + collapsibleJsonSection;
1538
+ }
1539
+
1540
+ // Initialize JSON toggle functionality
1541
+ this.initializeJsonToggle();
1542
+
1543
+ // Hide JSON pane since data is integrated above
1544
+ // JSON container no longer exists - handled via collapsible sections
1545
+ }
1546
+
1547
+ /**
1548
+ * Show error message (backward compatibility method)
1549
+ * @param {string} title - Error title
1550
+ * @param {string} message - Error message
1551
+ */
1552
+ showErrorMessage(title, message) {
1553
+ const content = `
1554
+ <div class="module-error">
1555
+ <div class="error-header">
1556
+ <h3>❌ ${title}</h3>
1557
+ </div>
1558
+ <div class="error-message">
1559
+ <p>${message}</p>
1560
+ </div>
1561
+ </div>
1562
+ `;
1563
+
1564
+ // Create collapsible JSON section for error data
1565
+ const errorData = { title, message };
1566
+ const collapsibleJsonSection = this.createCollapsibleJsonSection(errorData);
1567
+
1568
+ if (this.dataContainer) {
1569
+ this.dataContainer.innerHTML = content + collapsibleJsonSection;
1570
+ }
1571
+
1572
+ // Initialize JSON toggle functionality
1573
+ this.initializeJsonToggle();
1574
+
1575
+ // JSON container no longer exists - handled via collapsible sections
1576
+ }
1577
+
1578
+ /**
1579
+ * Show agent event details (backward compatibility method)
1580
+ * @param {Object} event - The agent event
1581
+ * @param {number} index - Event index
1582
+ */
1583
+ showAgentEvent(event, index) {
1584
+ // Show comprehensive agent-specific data instead of just single event
1585
+ this.showAgentSpecificDetails(event, index);
1586
+ }
1587
+
1588
+ /**
1589
+ * Show comprehensive agent-specific details including prompt, todos, and tools
1590
+ * @param {Object} event - The selected agent event
1591
+ * @param {number} index - Event index
1592
+ */
1593
+ showAgentSpecificDetails(event, index) {
1594
+ if (!event) {
1595
+ this.showEmptyState();
1596
+ return;
1597
+ }
1598
+
1599
+ // Get agent inference to determine which agent this is
1600
+ const agentInference = window.dashboard?.agentInference;
1601
+ const eventViewer = window.dashboard?.eventViewer;
1602
+
1603
+ if (!agentInference || !eventViewer) {
1604
+ console.warn('AgentInference or EventViewer not available, falling back to single event view');
1605
+ this.showEventDetails(event);
1606
+ return;
1607
+ }
1608
+
1609
+ const inference = agentInference.getInferredAgentForEvent(event);
1610
+ const agentName = inference?.agentName || this.extractAgent(event) || 'Unknown';
1611
+
1612
+ // Get all events from this agent
1613
+ const allEvents = eventViewer.events || [];
1614
+ const agentEvents = this.getAgentSpecificEvents(allEvents, agentName, agentInference);
1615
+
1616
+ console.log(`Showing details for agent: ${agentName}, found ${agentEvents.length} related events`);
1617
+
1618
+ // Extract agent-specific data
1619
+ const agentData = this.extractAgentSpecificData(agentName, agentEvents);
1620
+
1621
+ // Render agent-specific view
1622
+ this.renderAgentSpecificView(agentName, agentData, event);
1623
+ }
1624
+
1625
+ /**
1626
+ * Get all events related to a specific agent
1627
+ * @param {Array} allEvents - All events
1628
+ * @param {string} agentName - Name of the agent to filter for
1629
+ * @param {Object} agentInference - Agent inference system
1630
+ * @returns {Array} - Events related to this agent
1631
+ */
1632
+ getAgentSpecificEvents(allEvents, agentName, agentInference) {
1633
+ return allEvents.filter(event => {
1634
+ // Use agent inference to determine if this event belongs to the agent
1635
+ const inference = agentInference.getInferredAgentForEvent(event);
1636
+ const eventAgentName = inference?.agentName || this.extractAgent(event) || 'Unknown';
1637
+
1638
+ // Match agent names (case insensitive)
1639
+ return eventAgentName.toLowerCase() === agentName.toLowerCase();
1640
+ });
1641
+ }
1642
+
1643
+ /**
1644
+ * Extract agent-specific data from events
1645
+ * @param {string} agentName - Name of the agent
1646
+ * @param {Array} agentEvents - Events from this agent
1647
+ * @returns {Object} - Extracted agent data
1648
+ */
1649
+ extractAgentSpecificData(agentName, agentEvents) {
1650
+ const data = {
1651
+ agentName: agentName,
1652
+ totalEvents: agentEvents.length,
1653
+ prompt: null,
1654
+ todos: [],
1655
+ toolsCalled: [],
1656
+ sessions: new Set(),
1657
+ firstSeen: null,
1658
+ lastSeen: null,
1659
+ eventTypes: new Set()
1660
+ };
1661
+
1662
+ agentEvents.forEach(event => {
1663
+ const eventData = event.data || {};
1664
+ const timestamp = new Date(event.timestamp);
1665
+
1666
+ // Track timing
1667
+ if (!data.firstSeen || timestamp < data.firstSeen) {
1668
+ data.firstSeen = timestamp;
1669
+ }
1670
+ if (!data.lastSeen || timestamp > data.lastSeen) {
1671
+ data.lastSeen = timestamp;
1672
+ }
1673
+
1674
+ // Track sessions
1675
+ if (event.session_id || eventData.session_id) {
1676
+ data.sessions.add(event.session_id || eventData.session_id);
1677
+ }
1678
+
1679
+ // Track event types
1680
+ const eventType = event.hook_event_name || event.type || 'unknown';
1681
+ data.eventTypes.add(eventType);
1682
+
1683
+ // Extract prompt from Task delegation events
1684
+ if (event.type === 'hook' && eventData.tool_name === 'Task' && eventData.tool_parameters) {
1685
+ const taskParams = eventData.tool_parameters;
1686
+ if (taskParams.prompt && !data.prompt) {
1687
+ data.prompt = taskParams.prompt;
1688
+ }
1689
+ if (taskParams.description && !data.description) {
1690
+ data.description = taskParams.description;
1691
+ }
1692
+ if (taskParams.subagent_type === agentName && taskParams.prompt) {
1693
+ // Prefer prompts that match the specific agent
1694
+ data.prompt = taskParams.prompt;
1695
+ }
1696
+ }
1697
+
1698
+ // Also check for agent-specific prompts in other event types
1699
+ if (eventData.prompt && (eventData.agent_type === agentName || eventData.subagent_type === agentName)) {
1700
+ data.prompt = eventData.prompt;
1701
+ }
1702
+
1703
+ // Extract todos from TodoWrite events
1704
+ if (event.type === 'todo' || (event.type === 'hook' && eventData.tool_name === 'TodoWrite')) {
1705
+ const todos = eventData.todos || eventData.tool_parameters?.todos;
1706
+ if (todos && Array.isArray(todos)) {
1707
+ // Merge todos, keeping the most recent status for each
1708
+ todos.forEach(todo => {
1709
+ const existingIndex = data.todos.findIndex(t => t.id === todo.id || t.content === todo.content);
1710
+ if (existingIndex >= 0) {
1711
+ // Update existing todo with newer data
1712
+ data.todos[existingIndex] = { ...data.todos[existingIndex], ...todo, timestamp };
1713
+ } else {
1714
+ // Add new todo
1715
+ data.todos.push({ ...todo, timestamp });
1716
+ }
1717
+ });
1718
+ }
1719
+ }
1720
+
1721
+ // Extract tool calls - collect pre and post events separately first
1722
+ if (event.type === 'hook' && eventData.tool_name) {
1723
+ const phase = event.subtype || eventData.event_type;
1724
+ const toolCallId = this.generateToolCallId(eventData.tool_name, eventData.tool_parameters, timestamp);
1725
+
1726
+ if (phase === 'pre_tool') {
1727
+ // Store pre-tool event data
1728
+ if (!data._preToolEvents) data._preToolEvents = new Map();
1729
+ data._preToolEvents.set(toolCallId, {
1730
+ toolName: eventData.tool_name,
1731
+ timestamp: timestamp,
1732
+ target: this.extractToolTarget(eventData.tool_name, eventData.tool_parameters, null),
1733
+ parameters: eventData.tool_parameters
1734
+ });
1735
+ } else if (phase === 'post_tool') {
1736
+ // Store post-tool event data
1737
+ if (!data._postToolEvents) data._postToolEvents = new Map();
1738
+ data._postToolEvents.set(toolCallId, {
1739
+ toolName: eventData.tool_name,
1740
+ timestamp: timestamp,
1741
+ success: eventData.success,
1742
+ duration: eventData.duration_ms,
1743
+ resultSummary: eventData.result_summary,
1744
+ exitCode: eventData.exit_code
1745
+ });
1746
+ }
1747
+ }
1748
+ });
1749
+
1750
+ // Sort todos by timestamp (most recent first)
1751
+ data.todos.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
1752
+
1753
+ // Consolidate pre and post tool events into single tool calls
1754
+ data.toolsCalled = this.consolidateToolCalls(data._preToolEvents, data._postToolEvents);
1755
+
1756
+ // Clean up temporary data
1757
+ delete data._preToolEvents;
1758
+ delete data._postToolEvents;
1759
+
1760
+ // Sort tools by timestamp (most recent first)
1761
+ data.toolsCalled.sort((a, b) => b.timestamp - a.timestamp);
1762
+
1763
+ return data;
1764
+ }
1765
+
1766
+ /**
1767
+ * Generate a unique ID for a tool call to match pre and post events
1768
+ * @param {string} toolName - Name of the tool
1769
+ * @param {Object} parameters - Tool parameters
1770
+ * @param {Date} timestamp - Timestamp of the event
1771
+ * @returns {string} - Unique tool call ID
1772
+ */
1773
+ generateToolCallId(toolName, parameters, timestamp) {
1774
+ // Create a unique identifier based on tool name, key parameters, and approximate timestamp
1775
+ // Use a wider time window to account for timing differences between pre/post events
1776
+ const timeWindow = Math.floor(timestamp.getTime() / 5000); // Group by 5-second windows
1777
+
1778
+ // Include key parameters that uniquely identify a tool call
1779
+ let paramKey = '';
1780
+ if (parameters) {
1781
+ // Include important parameters that distinguish tool calls
1782
+ const keyParams = [];
1783
+ if (parameters.file_path) keyParams.push(parameters.file_path);
1784
+ if (parameters.command) keyParams.push(parameters.command.substring(0, 50));
1785
+ if (parameters.pattern) keyParams.push(parameters.pattern);
1786
+ if (parameters.subagent_type) keyParams.push(parameters.subagent_type);
1787
+ if (parameters.notebook_path) keyParams.push(parameters.notebook_path);
1788
+ if (parameters.url) keyParams.push(parameters.url);
1789
+ if (parameters.prompt) keyParams.push(parameters.prompt.substring(0, 30));
1790
+
1791
+ paramKey = keyParams.join('|');
1792
+ }
1793
+
1794
+ // If no specific parameters, use just tool name and time window
1795
+ if (!paramKey) {
1796
+ paramKey = 'default';
1797
+ }
1798
+
1799
+ return `${toolName}:${timeWindow}:${paramKey}`;
1800
+ }
1801
+
1802
+ /**
1803
+ * Consolidate pre and post tool events into single consolidated tool calls
1804
+ * @param {Map} preToolEvents - Map of pre-tool events by tool call ID
1805
+ * @param {Map} postToolEvents - Map of post-tool events by tool call ID
1806
+ * @returns {Array} - Array of consolidated tool calls
1807
+ */
1808
+ consolidateToolCalls(preToolEvents, postToolEvents) {
1809
+ const consolidatedCalls = [];
1810
+ const processedIds = new Set();
1811
+
1812
+ if (!preToolEvents) preToolEvents = new Map();
1813
+ if (!postToolEvents) postToolEvents = new Map();
1814
+
1815
+ // Process all pre-tool events first
1816
+ for (const [toolCallId, preEvent] of preToolEvents) {
1817
+ if (processedIds.has(toolCallId)) continue;
1818
+
1819
+ const postEvent = postToolEvents.get(toolCallId);
1820
+
1821
+ // Create consolidated tool call
1822
+ const consolidatedCall = {
1823
+ toolName: preEvent.toolName,
1824
+ timestamp: preEvent.timestamp, // Use pre-tool timestamp as the start time
1825
+ target: preEvent.target,
1826
+ parameters: preEvent.parameters,
1827
+ status: this.determineToolCallStatus(preEvent, postEvent),
1828
+ statusIcon: this.getToolCallStatusIcon(preEvent, postEvent),
1829
+ phase: postEvent ? 'completed' : 'running'
1830
+ };
1831
+
1832
+ // Add post-event data if available
1833
+ if (postEvent) {
1834
+ consolidatedCall.success = postEvent.success;
1835
+ consolidatedCall.duration = postEvent.duration;
1836
+ consolidatedCall.resultSummary = postEvent.resultSummary;
1837
+ consolidatedCall.exitCode = postEvent.exitCode;
1838
+ consolidatedCall.completedAt = postEvent.timestamp;
1839
+ }
1840
+
1841
+ consolidatedCalls.push(consolidatedCall);
1842
+ processedIds.add(toolCallId);
1843
+ }
1844
+
1845
+ // Process any post-tool events that don't have matching pre-tool events (edge case)
1846
+ for (const [toolCallId, postEvent] of postToolEvents) {
1847
+ if (processedIds.has(toolCallId)) continue;
1848
+
1849
+ // This is a post-tool event without a corresponding pre-tool event
1850
+ const consolidatedCall = {
1851
+ toolName: postEvent.toolName,
1852
+ timestamp: postEvent.timestamp,
1853
+ target: 'Unknown target', // We don't have pre-event data
1854
+ parameters: null,
1855
+ status: this.determineToolCallStatus(null, postEvent),
1856
+ statusIcon: this.getToolCallStatusIcon(null, postEvent),
1857
+ phase: 'completed',
1858
+ success: postEvent.success,
1859
+ duration: postEvent.duration,
1860
+ resultSummary: postEvent.resultSummary,
1861
+ exitCode: postEvent.exitCode,
1862
+ completedAt: postEvent.timestamp
1863
+ };
1864
+
1865
+ consolidatedCalls.push(consolidatedCall);
1866
+ processedIds.add(toolCallId);
1867
+ }
1868
+
1869
+ return consolidatedCalls;
1870
+ }
1871
+
1872
+ /**
1873
+ * Determine the status of a tool call based on pre and post events
1874
+ * @param {Object} preEvent - Pre-tool event data
1875
+ * @param {Object} postEvent - Post-tool event data
1876
+ * @returns {string} - Status text
1877
+ */
1878
+ determineToolCallStatus(preEvent, postEvent) {
1879
+ if (!postEvent) {
1880
+ return 'Running...';
1881
+ }
1882
+
1883
+ if (postEvent.success === true) {
1884
+ return 'Success';
1885
+ } else if (postEvent.success === false) {
1886
+ return 'Failed';
1887
+ } else if (postEvent.exitCode === 0) {
1888
+ return 'Completed';
1889
+ } else if (postEvent.exitCode === 2) {
1890
+ return 'Blocked';
1891
+ } else if (postEvent.exitCode !== undefined && postEvent.exitCode !== 0) {
1892
+ return 'Error';
1893
+ }
1894
+
1895
+ return 'Completed';
1896
+ }
1897
+
1898
+ /**
1899
+ * Get the status icon for a tool call
1900
+ * @param {Object} preEvent - Pre-tool event data
1901
+ * @param {Object} postEvent - Post-tool event data
1902
+ * @returns {string} - Status icon
1903
+ */
1904
+ getToolCallStatusIcon(preEvent, postEvent) {
1905
+ if (!postEvent) {
1906
+ return '⏳'; // Still running
1907
+ }
1908
+
1909
+ if (postEvent.success === true) {
1910
+ return '✅'; // Success
1911
+ } else if (postEvent.success === false) {
1912
+ return '❌'; // Failed
1913
+ } else if (postEvent.exitCode === 0) {
1914
+ return '✅'; // Completed successfully
1915
+ } else if (postEvent.exitCode === 2) {
1916
+ return '⚠️'; // Blocked
1917
+ } else if (postEvent.exitCode !== undefined && postEvent.exitCode !== 0) {
1918
+ return '❌'; // Error
1919
+ }
1920
+
1921
+ return '✅'; // Default to success for completed calls
1922
+ }
1923
+
1924
+ /**
1925
+ * Estimate token count for text using a simple approximation
1926
+ * @param {string} text - Text to estimate tokens for
1927
+ * @returns {number} - Estimated token count
1928
+ */
1929
+ estimateTokenCount(text) {
1930
+ if (!text || typeof text !== 'string') return 0;
1931
+
1932
+ // Simple token estimation: words * 1.3 (accounts for subwords)
1933
+ // Alternative: characters / 4 (common rule of thumb)
1934
+ const wordCount = text.trim().split(/\s+/).length;
1935
+ const charBasedEstimate = Math.ceil(text.length / 4);
1936
+
1937
+ // Use the higher of the two estimates for safety
1938
+ return Math.max(wordCount * 1.3, charBasedEstimate);
1939
+ }
1940
+
1941
+ /**
1942
+ * Trim excessive whitespace from text while preserving structure
1943
+ * @param {string} text - Text to trim
1944
+ * @returns {string} - Trimmed text
1945
+ */
1946
+ trimPromptWhitespace(text) {
1947
+ if (!text || typeof text !== 'string') return '';
1948
+
1949
+ // Remove leading/trailing whitespace from the entire text
1950
+ text = text.trim();
1951
+
1952
+ // Reduce multiple consecutive newlines to maximum of 2
1953
+ text = text.replace(/\n\s*\n\s*\n+/g, '\n\n');
1954
+
1955
+ // Trim whitespace from each line while preserving intentional indentation
1956
+ text = text.split('\n').map(line => {
1957
+ // Only trim trailing whitespace, preserve leading whitespace for structure
1958
+ return line.replace(/\s+$/, '');
1959
+ }).join('\n');
1960
+
1961
+ return text;
1962
+ }
1963
+
1964
+ /**
1965
+ * Render agent-specific view with comprehensive data
1966
+ * @param {string} agentName - Name of the agent
1967
+ * @param {Object} agentData - Extracted agent data
1968
+ * @param {Object} originalEvent - The original clicked event
1969
+ */
1970
+ renderAgentSpecificView(agentName, agentData, originalEvent) {
1971
+ // Create contextual header
1972
+ const timestamp = this.formatTimestamp(originalEvent.timestamp);
1973
+ const contextualHeader = `
1974
+ <div class="contextual-header">
1975
+ <h3 class="contextual-header-text">🤖 ${agentName} Agent Details ${timestamp}</h3>
1976
+ </div>
1977
+ `;
1978
+
1979
+ // Build comprehensive agent view
1980
+ let content = `
1981
+ <div class="agent-overview-section">
1982
+ <div class="structured-data">
1983
+ ${this.createProperty('Agent Name', agentName)}
1984
+ ${this.createProperty('Total Events', agentData.totalEvents)}
1985
+ ${this.createProperty('Active Sessions', agentData.sessions.size)}
1986
+ ${this.createProperty('Event Types', Array.from(agentData.eventTypes).join(', '))}
1987
+ ${agentData.firstSeen ? this.createProperty('First Seen', agentData.firstSeen.toLocaleString()) : ''}
1988
+ ${agentData.lastSeen ? this.createProperty('Last Seen', agentData.lastSeen.toLocaleString()) : ''}
1989
+ </div>
1990
+ </div>
1991
+ `;
1992
+
1993
+ // Add prompt section if available
1994
+ if (agentData.prompt) {
1995
+ const trimmedPrompt = this.trimPromptWhitespace(agentData.prompt);
1996
+ const tokenCount = Math.round(this.estimateTokenCount(trimmedPrompt));
1997
+ const wordCount = trimmedPrompt.trim().split(/\s+/).length;
1998
+
1999
+ content += `
2000
+ <div class="agent-prompt-section">
2001
+ <div class="contextual-header">
2002
+ <h3 class="contextual-header-text">📝 Agent Task Prompt</h3>
2003
+ <div class="prompt-stats" style="font-size: 11px; color: #64748b; margin-top: 4px;">
2004
+ ~${tokenCount} tokens • ${wordCount} words • ${trimmedPrompt.length} characters
2005
+ </div>
2006
+ </div>
2007
+ <div class="structured-data">
2008
+ <div class="agent-prompt" style="white-space: pre-wrap; max-height: 300px; overflow-y: auto; padding: 10px; background: #f8fafc; border-radius: 6px; font-family: monospace; font-size: 12px; line-height: 1.4; border: 1px solid #e2e8f0;">
2009
+ ${this.escapeHtml(trimmedPrompt)}
2010
+ </div>
2011
+ </div>
2012
+ </div>
2013
+ `;
2014
+ }
2015
+
2016
+ // Add todos section if available
2017
+ if (agentData.todos.length > 0) {
2018
+ content += `
2019
+ <div class="agent-todos-section">
2020
+ <div class="contextual-header">
2021
+ <h3 class="contextual-header-text">✅ Agent Todo List (${agentData.todos.length} items)</h3>
2022
+ </div>
2023
+ <div class="todo-checklist">
2024
+ ${agentData.todos.map(todo => `
2025
+ <div class="todo-item todo-${todo.status || 'pending'}">
2026
+ <span class="todo-status">${this.getTodoStatusIcon(todo.status)}</span>
2027
+ <span class="todo-content">${todo.content || 'No content'}</span>
2028
+ <span class="todo-priority priority-${todo.priority || 'medium'}">${this.getTodoPriorityIcon(todo.priority)}</span>
2029
+ ${todo.timestamp ? `<span class="todo-timestamp">${new Date(todo.timestamp).toLocaleTimeString()}</span>` : ''}
2030
+ </div>
2031
+ `).join('')}
2032
+ </div>
2033
+ </div>
2034
+ `;
2035
+ }
2036
+
2037
+ // Add tools section if available
2038
+ if (agentData.toolsCalled.length > 0) {
2039
+ content += `
2040
+ <div class="agent-tools-section">
2041
+ <div class="contextual-header">
2042
+ <h3 class="contextual-header-text">🔧 Tools Called by Agent (${agentData.toolsCalled.length} calls)</h3>
2043
+ </div>
2044
+ <div class="tools-list">
2045
+ ${agentData.toolsCalled.map(tool => {
2046
+ // Determine CSS class for status
2047
+ let statusClass = '';
2048
+ if (tool.statusIcon === '✅') statusClass = 'status-success';
2049
+ else if (tool.statusIcon === '❌') statusClass = 'status-failed';
2050
+ else if (tool.statusIcon === '⚠️') statusClass = 'status-blocked';
2051
+ else if (tool.statusIcon === '⏳') statusClass = 'status-running';
2052
+
2053
+ return `
2054
+ <div class="tool-call-item">
2055
+ <div class="tool-call-header">
2056
+ <div style="display: flex; align-items: center; gap: 12px; flex: 1;">
2057
+ <span class="tool-name">🔧 ${tool.toolName}</span>
2058
+ <span class="tool-agent">${agentName}</span>
2059
+ <span class="tool-status-indicator ${statusClass}">${tool.statusIcon} ${tool.status}</span>
2060
+ </div>
2061
+ <span class="tool-timestamp" style="margin-left: auto;">${tool.timestamp.toLocaleTimeString()}</span>
2062
+ </div>
2063
+ <div class="tool-call-details">
2064
+ ${tool.target ? `<span class="tool-target">Target: ${tool.target}</span>` : ''}
2065
+ ${tool.duration ? `<span class="tool-duration">Duration: ${tool.duration}ms</span>` : ''}
2066
+ ${tool.completedAt && tool.completedAt !== tool.timestamp ? `<span class="tool-completed">Completed: ${tool.completedAt.toLocaleTimeString()}</span>` : ''}
2067
+ </div>
2068
+ </div>
2069
+ `;
2070
+ }).join('')}
2071
+ </div>
2072
+ </div>
2073
+ `;
2074
+ }
2075
+
2076
+ // Create collapsible JSON section for agent data
2077
+ const agentJsonData = {
2078
+ agentName: agentName,
2079
+ agentData: agentData,
2080
+ originalEvent: originalEvent
2081
+ };
2082
+ const collapsibleJsonSection = this.createCollapsibleJsonSection(agentJsonData);
2083
+
2084
+ // Show structured data with JSON section in data pane
2085
+ if (this.dataContainer) {
2086
+ this.dataContainer.innerHTML = contextualHeader + content + collapsibleJsonSection;
2087
+ }
2088
+
2089
+ // Initialize JSON toggle functionality
2090
+ this.initializeJsonToggle();
2091
+
2092
+ // Hide JSON pane since data is integrated above
2093
+ // JSON container no longer exists - handled via collapsible sections
2094
+ }
2095
+
2096
+ /**
2097
+ * Create tool result section for backward compatibility with showToolCall method
2098
+ * @param {Object} toolCall - Tool call data
2099
+ * @returns {string} HTML content for tool result section
2100
+ */
2101
+ createToolResultFromToolCall(toolCall) {
2102
+ // Check if we have result data
2103
+ if (!toolCall.result_summary) {
2104
+ return '';
2105
+ }
2106
+
2107
+ // Convert toolCall data to match the format expected by createInlineToolResultContent
2108
+ const mockData = {
2109
+ event_type: 'post_tool',
2110
+ result_summary: toolCall.result_summary,
2111
+ success: toolCall.success,
2112
+ exit_code: toolCall.exit_code
2113
+ };
2114
+
2115
+ // Create a mock event object with proper subtype
2116
+ const mockEvent = {
2117
+ subtype: 'post_tool'
2118
+ };
2119
+
2120
+ // Get inline result content
2121
+ const inlineContent = this.createInlineToolResultContent(mockData, mockEvent);
2122
+
2123
+ // If we have content, wrap it in a simple section
2124
+ if (inlineContent.trim()) {
2125
+ return `
2126
+ <div class="tool-result-inline">
2127
+ <div class="structured-data">
2128
+ ${inlineContent}
2129
+ </div>
2130
+ </div>
2131
+ `;
2132
+ }
2133
+
2134
+ return '';
2135
+ }
2136
+
2137
+ /**
2138
+ * Extract tool target from tool name and parameters
2139
+ * @param {string} toolName - Name of the tool
2140
+ * @param {Object} parameters - Tool parameters
2141
+ * @param {Object} altParameters - Alternative parameters
2142
+ * @returns {string} - Tool target description
2143
+ */
2144
+ extractToolTarget(toolName, parameters, altParameters) {
2145
+ const params = parameters || altParameters || {};
2146
+
2147
+ switch (toolName?.toLowerCase()) {
2148
+ case 'write':
2149
+ case 'read':
2150
+ case 'edit':
2151
+ case 'multiedit':
2152
+ return params.file_path || 'Unknown file';
2153
+ case 'bash':
2154
+ return params.command ? `${params.command.substring(0, 50)}${params.command.length > 50 ? '...' : ''}` : 'Unknown command';
2155
+ case 'grep':
2156
+ return params.pattern ? `Pattern: ${params.pattern}` : 'Unknown pattern';
2157
+ case 'glob':
2158
+ return params.pattern ? `Pattern: ${params.pattern}` : 'Unknown glob';
2159
+ case 'todowrite':
2160
+ return `${params.todos?.length || 0} todos`;
2161
+ case 'task':
2162
+ return params.subagent_type || params.agent_type || 'Subagent delegation';
2163
+ default:
2164
+ // Try to find a meaningful parameter
2165
+ if (params.file_path) return params.file_path;
2166
+ if (params.pattern) return `Pattern: ${params.pattern}`;
2167
+ if (params.command) return `Command: ${params.command.substring(0, 30)}...`;
2168
+ if (params.path) return params.path;
2169
+ return 'Unknown target';
2170
+ }
2171
+ }
2172
+
2173
+
2174
+ /**
2175
+ * Get operation icon for file operations
2176
+ * @param {string} operation - Operation type
2177
+ * @returns {string} - Icon for the operation
2178
+ */
2179
+ getOperationIcon(operation) {
2180
+ const icons = {
2181
+ 'read': '👁️',
2182
+ 'write': '✏️',
2183
+ 'edit': '📝',
2184
+ 'multiedit': '📝',
2185
+ 'create': '🆕',
2186
+ 'delete': '🗑️',
2187
+ 'move': '📦',
2188
+ 'copy': '📋'
2189
+ };
2190
+ return icons[operation?.toLowerCase()] || '📄';
2191
+ }
2192
+
2193
+ /**
2194
+ * Get current event
2195
+ */
2196
+ getCurrentEvent() {
2197
+ return this.currentEvent;
2198
+ }
2199
+
2200
+ /**
2201
+ * Check git tracking status and show track control if needed
2202
+ * @param {string} filePath - Path to the file to check
2203
+ */
2204
+ async checkAndShowTrackControl(filePath) {
2205
+ if (!filePath) return;
2206
+
2207
+ try {
2208
+ // Get the Socket.IO client
2209
+ const socket = window.socket || window.dashboard?.socketClient?.socket;
2210
+ if (!socket) {
2211
+ console.warn('No socket connection available for git tracking check');
2212
+ return;
2213
+ }
2214
+
2215
+ // Get working directory from dashboard with proper fallback
2216
+ let workingDir = window.dashboard?.currentWorkingDir;
2217
+
2218
+ // Don't use 'Unknown' as a working directory
2219
+ if (!workingDir || workingDir === 'Unknown' || workingDir.trim() === '') {
2220
+ // Try to get from footer element
2221
+ const footerDir = document.getElementById('footer-working-dir');
2222
+ if (footerDir?.textContent?.trim() && footerDir.textContent.trim() !== 'Unknown') {
2223
+ workingDir = footerDir.textContent.trim();
2224
+ } else {
2225
+ // Final fallback to current directory
2226
+ workingDir = '.';
2227
+ }
2228
+ console.log('[MODULE-VIEWER-DEBUG] Working directory fallback used:', workingDir);
2229
+ }
2230
+
2231
+ // Set up one-time listener for tracking status response
2232
+ const responsePromise = new Promise((resolve, reject) => {
2233
+ const responseHandler = (data) => {
2234
+ if (data.file_path === filePath) {
2235
+ socket.off('file_tracked_response', responseHandler);
2236
+ resolve(data);
2237
+ }
2238
+ };
2239
+
2240
+ socket.on('file_tracked_response', responseHandler);
2241
+
2242
+ // Timeout after 5 seconds
2243
+ setTimeout(() => {
2244
+ socket.off('file_tracked_response', responseHandler);
2245
+ reject(new Error('Request timeout'));
2246
+ }, 5000);
2247
+ });
2248
+
2249
+ // Send tracking status request
2250
+ socket.emit('check_file_tracked', {
2251
+ file_path: filePath,
2252
+ working_dir: workingDir
2253
+ });
2254
+
2255
+ // Wait for response
2256
+ const result = await responsePromise;
2257
+ this.displayTrackingStatus(filePath, result);
2258
+
2259
+ } catch (error) {
2260
+ console.error('Error checking file tracking status:', error);
2261
+ this.displayTrackingStatus(filePath, {
2262
+ success: false,
2263
+ error: error.message,
2264
+ file_path: filePath
2265
+ });
2266
+ }
2267
+ }
2268
+
2269
+ /**
2270
+ * Display tracking status and show track control if needed
2271
+ * @param {string} filePath - Path to the file
2272
+ * @param {Object} result - Result from tracking status check
2273
+ */
2274
+ displayTrackingStatus(filePath, result) {
2275
+ const statusElementId = `git-track-status-${filePath.replace(/[^a-zA-Z0-9]/g, '-')}`;
2276
+ const statusElement = document.getElementById(statusElementId);
2277
+
2278
+ if (!statusElement) return;
2279
+
2280
+ if (result.success && result.is_tracked === false) {
2281
+ // File is not tracked - show track button
2282
+ statusElement.innerHTML = `
2283
+ <div class="untracked-file-notice">
2284
+ <span class="untracked-icon">⚠️</span>
2285
+ <span class="untracked-text">This file is not tracked by git</span>
2286
+ <button class="track-file-button"
2287
+ onclick="window.moduleViewer.trackFile('${filePath}')"
2288
+ title="Add this file to git tracking">
2289
+ <span class="git-icon">📁</span> Track File
2290
+ </button>
2291
+ </div>
2292
+ `;
2293
+ } else if (result.success && result.is_tracked === true) {
2294
+ // File is tracked - show status
2295
+ statusElement.innerHTML = `
2296
+ <div class="tracked-file-notice">
2297
+ <span class="tracked-icon">✅</span>
2298
+ <span class="tracked-text">This file is tracked by git</span>
2299
+ </div>
2300
+ `;
2301
+ } else if (!result.success) {
2302
+ // Error checking status
2303
+ statusElement.innerHTML = `
2304
+ <div class="tracking-error-notice">
2305
+ <span class="error-icon">❌</span>
2306
+ <span class="error-text">Could not check git status: ${result.error || 'Unknown error'}</span>
2307
+ </div>
2308
+ `;
2309
+ }
2310
+ }
2311
+
2312
+ /**
2313
+ * Track a file using git add
2314
+ * @param {string} filePath - Path to the file to track
2315
+ */
2316
+ async trackFile(filePath) {
2317
+ if (!filePath) return;
2318
+
2319
+ try {
2320
+ // Get the Socket.IO client
2321
+ const socket = window.socket || window.dashboard?.socketClient?.socket;
2322
+ if (!socket) {
2323
+ console.warn('No socket connection available for git add');
2324
+ return;
2325
+ }
2326
+
2327
+ // Get working directory from dashboard with proper fallback
2328
+ let workingDir = window.dashboard?.currentWorkingDir;
2329
+
2330
+ // Don't use 'Unknown' as a working directory
2331
+ if (!workingDir || workingDir === 'Unknown' || workingDir.trim() === '') {
2332
+ // Try to get from footer element
2333
+ const footerDir = document.getElementById('footer-working-dir');
2334
+ if (footerDir?.textContent?.trim() && footerDir.textContent.trim() !== 'Unknown') {
2335
+ workingDir = footerDir.textContent.trim();
2336
+ } else {
2337
+ // Final fallback to current directory
2338
+ workingDir = '.';
2339
+ }
2340
+ console.log('[MODULE-VIEWER-DEBUG] Working directory fallback used:', workingDir);
2341
+ }
2342
+
2343
+ // Update button to show loading state
2344
+ const statusElementId = `git-track-status-${filePath.replace(/[^a-zA-Z0-9]/g, '-')}`;
2345
+ const statusElement = document.getElementById(statusElementId);
2346
+
2347
+ if (statusElement) {
2348
+ statusElement.innerHTML = `
2349
+ <div class="tracking-file-notice">
2350
+ <span class="loading-icon">⏳</span>
2351
+ <span class="loading-text">Adding file to git tracking...</span>
2352
+ </div>
2353
+ `;
2354
+ }
2355
+
2356
+ // Set up one-time listener for git add response
2357
+ const responsePromise = new Promise((resolve, reject) => {
2358
+ const responseHandler = (data) => {
2359
+ if (data.file_path === filePath) {
2360
+ socket.off('git_add_response', responseHandler);
2361
+ resolve(data);
2362
+ }
2363
+ };
2364
+
2365
+ socket.on('git_add_response', responseHandler);
2366
+
2367
+ // Timeout after 10 seconds
2368
+ setTimeout(() => {
2369
+ socket.off('git_add_response', responseHandler);
2370
+ reject(new Error('Request timeout'));
2371
+ }, 10000);
2372
+ });
2373
+
2374
+ // Send git add request
2375
+ socket.emit('git_add_file', {
2376
+ file_path: filePath,
2377
+ working_dir: workingDir
2378
+ });
2379
+
2380
+ console.log('📁 Git add request sent:', {
2381
+ filePath,
2382
+ workingDir
2383
+ });
2384
+
2385
+ // Wait for response
2386
+ const result = await responsePromise;
2387
+ console.log('📦 Git add result:', result);
2388
+
2389
+ // Update UI based on result
2390
+ if (result.success) {
2391
+ if (statusElement) {
2392
+ statusElement.innerHTML = `
2393
+ <div class="tracked-file-notice">
2394
+ <span class="tracked-icon">✅</span>
2395
+ <span class="tracked-text">File successfully added to git tracking</span>
2396
+ </div>
2397
+ `;
2398
+ }
2399
+
2400
+ // Show success notification
2401
+ this.showNotification('File tracked successfully', 'success');
2402
+ } else {
2403
+ if (statusElement) {
2404
+ statusElement.innerHTML = `
2405
+ <div class="tracking-error-notice">
2406
+ <span class="error-icon">❌</span>
2407
+ <span class="error-text">Failed to track file: ${result.error || 'Unknown error'}</span>
2408
+ <button class="track-file-button"
2409
+ onclick="window.moduleViewer.trackFile('${filePath}')"
2410
+ title="Try again">
2411
+ <span class="git-icon">📁</span> Retry
2412
+ </button>
2413
+ </div>
2414
+ `;
2415
+ }
2416
+
2417
+ // Show error notification
2418
+ this.showNotification(`Failed to track file: ${result.error}`, 'error');
2419
+ }
2420
+
2421
+ } catch (error) {
2422
+ console.error('❌ Failed to track file:', error);
2423
+
2424
+ // Update UI to show error
2425
+ const statusElementId = `git-track-status-${filePath.replace(/[^a-zA-Z0-9]/g, '-')}`;
2426
+ const statusElement = document.getElementById(statusElementId);
2427
+
2428
+ if (statusElement) {
2429
+ statusElement.innerHTML = `
2430
+ <div class="tracking-error-notice">
2431
+ <span class="error-icon">❌</span>
2432
+ <span class="error-text">Error: ${error.message}</span>
2433
+ <button class="track-file-button"
2434
+ onclick="window.moduleViewer.trackFile('${filePath}')"
2435
+ title="Try again">
2436
+ <span class="git-icon">📁</span> Retry
2437
+ </button>
2438
+ </div>
2439
+ `;
2440
+ }
2441
+
2442
+ // Show error notification
2443
+ this.showNotification(`Error tracking file: ${error.message}`, 'error');
2444
+ }
2445
+ }
2446
+
2447
+ /**
2448
+ * Check git status and conditionally show git diff icons
2449
+ * Only shows git diff icons if git status check succeeds
2450
+ * @param {string} filePath - Path to the file to check
2451
+ */
2452
+ async checkAndShowGitDiffIcons(filePath) {
2453
+ if (!filePath) {
2454
+ console.debug('[GIT-DIFF-ICONS] No filePath provided, skipping git diff icon check');
2455
+ return;
2456
+ }
2457
+
2458
+ console.debug('[GIT-DIFF-ICONS] Checking git diff icons for file:', filePath);
2459
+
2460
+ try {
2461
+ // Get the Socket.IO client
2462
+ const socket = window.socket || window.dashboard?.socketClient?.socket;
2463
+ if (!socket) {
2464
+ console.warn('[GIT-DIFF-ICONS] No socket connection available for git status check');
2465
+ return;
2466
+ }
2467
+
2468
+ console.debug('[GIT-DIFF-ICONS] Socket connection available, proceeding');
2469
+
2470
+ // Get working directory from dashboard with proper fallback
2471
+ let workingDir = window.dashboard?.currentWorkingDir;
2472
+
2473
+ // Don't use 'Unknown' as a working directory
2474
+ if (!workingDir || workingDir === 'Unknown' || workingDir.trim() === '') {
2475
+ // Try to get from footer element
2476
+ const footerDir = document.getElementById('footer-working-dir');
2477
+ if (footerDir?.textContent?.trim() && footerDir.textContent.trim() !== 'Unknown') {
2478
+ workingDir = footerDir.textContent.trim();
2479
+ } else {
2480
+ // Final fallback to current directory
2481
+ workingDir = '.';
2482
+ }
2483
+ console.log('[GIT-DIFF-ICONS] Working directory fallback used:', workingDir);
2484
+ } else {
2485
+ console.debug('[GIT-DIFF-ICONS] Using working directory:', workingDir);
2486
+ }
2487
+
2488
+ // Set up one-time listener for git status response
2489
+ const responsePromise = new Promise((resolve, reject) => {
2490
+ const responseHandler = (data) => {
2491
+ console.debug('[GIT-DIFF-ICONS] Received git status response:', data);
2492
+ if (data.file_path === filePath) {
2493
+ socket.off('git_status_response', responseHandler);
2494
+ resolve(data);
2495
+ } else {
2496
+ console.debug('[GIT-DIFF-ICONS] Response for different file, ignoring:', data.file_path);
2497
+ }
2498
+ };
2499
+
2500
+ socket.on('git_status_response', responseHandler);
2501
+
2502
+ // Timeout after 3 seconds
2503
+ setTimeout(() => {
2504
+ socket.off('git_status_response', responseHandler);
2505
+ console.warn('[GIT-DIFF-ICONS] Timeout waiting for git status response');
2506
+ reject(new Error('Request timeout'));
2507
+ }, 3000);
2508
+ });
2509
+
2510
+ console.debug('[GIT-DIFF-ICONS] Sending check_git_status event');
2511
+ // Send git status request
2512
+ socket.emit('check_git_status', {
2513
+ file_path: filePath,
2514
+ working_dir: workingDir
2515
+ });
2516
+
2517
+ // Wait for response
2518
+ const result = await responsePromise;
2519
+ console.debug('[GIT-DIFF-ICONS] Git status check result:', result);
2520
+
2521
+ // Only show git diff icons if git status check was successful
2522
+ if (result.success) {
2523
+ console.debug('[GIT-DIFF-ICONS] Git status check successful, showing icons for:', filePath);
2524
+ this.showGitDiffIconsForFile(filePath);
2525
+ } else {
2526
+ console.debug('[GIT-DIFF-ICONS] Git status check failed, icons will remain hidden:', result.error);
2527
+ }
2528
+ // If git status fails, icons remain hidden (display: none)
2529
+
2530
+ } catch (error) {
2531
+ console.warn('[GIT-DIFF-ICONS] Git status check failed, hiding git diff icons:', error.message);
2532
+ // Icons remain hidden on error
2533
+ }
2534
+ }
2535
+
2536
+ /**
2537
+ * Show git diff icons for a specific file after successful git status check
2538
+ * @param {string} filePath - Path to the file
2539
+ */
2540
+ showGitDiffIconsForFile(filePath) {
2541
+ console.debug('[GIT-DIFF-ICONS] Showing git diff icons for file:', filePath);
2542
+
2543
+ // Find all git diff icons for this file path and show them
2544
+ const gitDiffIcons = document.querySelectorAll(`[data-file-path="${filePath}"]`);
2545
+ console.debug('[GIT-DIFF-ICONS] Found', gitDiffIcons.length, 'elements with matching file path');
2546
+
2547
+ let shownCount = 0;
2548
+ gitDiffIcons.forEach((icon, index) => {
2549
+ console.debug('[GIT-DIFF-ICONS] Processing element', index, ':', icon);
2550
+ console.debug('[GIT-DIFF-ICONS] Element classes:', icon.classList.toString());
2551
+
2552
+ if (icon.classList.contains('git-diff-icon')) {
2553
+ console.debug('[GIT-DIFF-ICONS] Setting display to inline for git-diff-icon');
2554
+ icon.style.display = 'inline';
2555
+ shownCount++;
2556
+ } else {
2557
+ console.debug('[GIT-DIFF-ICONS] Element is not a git-diff-icon, skipping');
2558
+ }
2559
+ });
2560
+
2561
+ console.debug('[GIT-DIFF-ICONS] Showed', shownCount, 'git diff icons for file:', filePath);
2562
+ }
2563
+
2564
+ /**
2565
+ * Show notification to user
2566
+ * @param {string} message - Message to show
2567
+ * @param {string} type - Type of notification (success, error, info)
2568
+ */
2569
+ showNotification(message, type = 'info') {
2570
+ // Create notification element
2571
+ const notification = document.createElement('div');
2572
+ notification.className = `notification notification-${type}`;
2573
+ notification.innerHTML = `
2574
+ <span class="notification-icon">${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'}</span>
2575
+ <span class="notification-message">${message}</span>
2576
+ `;
2577
+
2578
+ // Style the notification
2579
+ notification.style.cssText = `
2580
+ position: fixed;
2581
+ top: 20px;
2582
+ right: 20px;
2583
+ background: ${type === 'success' ? '#d4edda' : type === 'error' ? '#f8d7da' : '#d1ecf1'};
2584
+ color: ${type === 'success' ? '#155724' : type === 'error' ? '#721c24' : '#0c5460'};
2585
+ border: 1px solid ${type === 'success' ? '#c3e6cb' : type === 'error' ? '#f5c6cb' : '#bee5eb'};
2586
+ border-radius: 6px;
2587
+ padding: 12px 16px;
2588
+ font-size: 14px;
2589
+ font-weight: 500;
2590
+ z-index: 2000;
2591
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
2592
+ display: flex;
2593
+ align-items: center;
2594
+ gap: 8px;
2595
+ max-width: 400px;
2596
+ animation: slideIn 0.3s ease-out;
2597
+ `;
2598
+
2599
+ // Add animation styles
2600
+ const style = document.createElement('style');
2601
+ style.textContent = `
2602
+ @keyframes slideIn {
2603
+ from { transform: translateX(100%); opacity: 0; }
2604
+ to { transform: translateX(0); opacity: 1; }
2605
+ }
2606
+ @keyframes slideOut {
2607
+ from { transform: translateX(0); opacity: 1; }
2608
+ to { transform: translateX(100%); opacity: 0; }
2609
+ }
2610
+ `;
2611
+ document.head.appendChild(style);
2612
+
2613
+ // Add to page
2614
+ document.body.appendChild(notification);
2615
+
2616
+ // Remove after 5 seconds
2617
+ setTimeout(() => {
2618
+ notification.style.animation = 'slideOut 0.3s ease-in';
2619
+ setTimeout(() => {
2620
+ if (notification.parentNode) {
2621
+ notification.parentNode.removeChild(notification);
2622
+ }
2623
+ if (style.parentNode) {
2624
+ style.parentNode.removeChild(style);
2625
+ }
2626
+ }, 300);
2627
+ }, 5000);
2628
+ }
2629
+
2630
+ /**
2631
+ * Show agent instance details for PM delegations
2632
+ * @param {Object} instance - Agent instance from PM delegation
2633
+ */
2634
+ showAgentInstance(instance) {
2635
+ if (!instance) {
2636
+ this.showEmptyState();
2637
+ return;
2638
+ }
2639
+
2640
+ // Create a synthetic event object to work with existing showAgentSpecificDetails method
2641
+ const syntheticEvent = {
2642
+ type: 'pm_delegation',
2643
+ subtype: instance.agentName,
2644
+ agent_type: instance.agentName,
2645
+ timestamp: instance.timestamp,
2646
+ session_id: instance.sessionId,
2647
+ metadata: {
2648
+ delegation_type: 'explicit',
2649
+ event_count: instance.agentEvents.length,
2650
+ pm_call: instance.pmCall || null,
2651
+ agent_events: instance.agentEvents
2652
+ }
2653
+ };
2654
+
2655
+ console.log('Showing PM delegation details:', instance);
2656
+ this.showAgentSpecificDetails(syntheticEvent, 0);
2657
+ }
2658
+
2659
+ /**
2660
+ * Show implied agent details for agents without explicit PM delegation
2661
+ * @param {Object} impliedInstance - Implied agent instance
2662
+ */
2663
+ showImpliedAgent(impliedInstance) {
2664
+ if (!impliedInstance) {
2665
+ this.showEmptyState();
2666
+ return;
2667
+ }
2668
+
2669
+ // Create a synthetic event object to work with existing showAgentSpecificDetails method
2670
+ const syntheticEvent = {
2671
+ type: 'implied_delegation',
2672
+ subtype: impliedInstance.agentName,
2673
+ agent_type: impliedInstance.agentName,
2674
+ timestamp: impliedInstance.timestamp,
2675
+ session_id: impliedInstance.sessionId,
2676
+ metadata: {
2677
+ delegation_type: 'implied',
2678
+ event_count: impliedInstance.eventCount,
2679
+ pm_call: null,
2680
+ note: 'No explicit PM call found - inferred from agent activity'
2681
+ }
2682
+ };
2683
+
2684
+ console.log('Showing implied agent details:', impliedInstance);
2685
+ this.showAgentSpecificDetails(syntheticEvent, 0);
2686
+ }
2687
+ }
2688
+
2689
+ // Export for global use
2690
+ window.ModuleViewer = ModuleViewer;
2691
+
2692
+ // Debug helper function for troubleshooting tool result display
2693
+ window.enableToolResultDebugging = function() {
2694
+ window.DEBUG_TOOL_RESULTS = true;
2695
+ console.log('🔧 Tool result debugging enabled. Click on tool events to see debug info.');
2696
+ };
2697
+
2698
+ window.disableToolResultDebugging = function() {
2699
+ window.DEBUG_TOOL_RESULTS = false;
2700
+ console.log('🔧 Tool result debugging disabled.');
2701
+ };