claude-mpm 3.4.13__py3-none-any.whl → 3.4.16__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 (27) hide show
  1. claude_mpm/dashboard/index.html +13 -0
  2. claude_mpm/dashboard/static/css/dashboard.css +2722 -0
  3. claude_mpm/dashboard/static/js/components/agent-inference.js +619 -0
  4. claude_mpm/dashboard/static/js/components/event-processor.js +641 -0
  5. claude_mpm/dashboard/static/js/components/event-viewer.js +914 -0
  6. claude_mpm/dashboard/static/js/components/export-manager.js +362 -0
  7. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +611 -0
  8. claude_mpm/dashboard/static/js/components/hud-library-loader.js +211 -0
  9. claude_mpm/dashboard/static/js/components/hud-manager.js +671 -0
  10. claude_mpm/dashboard/static/js/components/hud-visualizer.js +1718 -0
  11. claude_mpm/dashboard/static/js/components/module-viewer.js +2701 -0
  12. claude_mpm/dashboard/static/js/components/session-manager.js +520 -0
  13. claude_mpm/dashboard/static/js/components/socket-manager.js +343 -0
  14. claude_mpm/dashboard/static/js/components/ui-state-manager.js +427 -0
  15. claude_mpm/dashboard/static/js/components/working-directory.js +866 -0
  16. claude_mpm/dashboard/static/js/dashboard-original.js +4134 -0
  17. claude_mpm/dashboard/static/js/dashboard.js +1978 -0
  18. claude_mpm/dashboard/static/js/socket-client.js +537 -0
  19. claude_mpm/dashboard/templates/index.html +346 -0
  20. claude_mpm/dashboard/test_dashboard.html +372 -0
  21. claude_mpm/services/socketio_server.py +111 -7
  22. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/METADATA +2 -1
  23. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/RECORD +27 -7
  24. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/WHEEL +0 -0
  25. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/entry_points.txt +0 -0
  26. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/licenses/LICENSE +0 -0
  27. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,914 @@
1
+ /**
2
+ * Event Viewer Component
3
+ * Handles event display, filtering, and selection
4
+ */
5
+
6
+ class EventViewer {
7
+ constructor(containerId, socketClient) {
8
+ this.container = document.getElementById(containerId);
9
+ this.socketClient = socketClient;
10
+
11
+ // State
12
+ this.events = [];
13
+ this.filteredEvents = [];
14
+ this.selectedEventIndex = -1;
15
+ this.filteredEventElements = [];
16
+ this.autoScroll = true;
17
+
18
+ // Filters
19
+ this.searchFilter = '';
20
+ this.typeFilter = '';
21
+ this.sessionFilter = '';
22
+
23
+ // Event type tracking
24
+ this.eventTypeCount = {};
25
+ this.availableEventTypes = new Set();
26
+ this.errorCount = 0;
27
+ this.eventsThisMinute = 0;
28
+ this.lastMinute = new Date().getMinutes();
29
+
30
+ this.init();
31
+ }
32
+
33
+ /**
34
+ * Initialize the event viewer
35
+ */
36
+ init() {
37
+ this.setupEventHandlers();
38
+ this.setupKeyboardNavigation();
39
+
40
+ // Subscribe to socket events
41
+ this.socketClient.onEventUpdate((events, sessions) => {
42
+ // Ensure we always have a valid events array
43
+ this.events = Array.isArray(events) ? events : [];
44
+ this.updateDisplay();
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Setup event handlers for UI controls
50
+ */
51
+ setupEventHandlers() {
52
+ // Search input
53
+ const searchInput = document.getElementById('events-search-input');
54
+ if (searchInput) {
55
+ searchInput.addEventListener('input', (e) => {
56
+ this.searchFilter = e.target.value.toLowerCase();
57
+ this.applyFilters();
58
+ });
59
+ }
60
+
61
+ // Type filter
62
+ const typeFilter = document.getElementById('events-type-filter');
63
+ if (typeFilter) {
64
+ typeFilter.addEventListener('change', (e) => {
65
+ this.typeFilter = e.target.value;
66
+ this.applyFilters();
67
+ });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Setup keyboard navigation for events
73
+ * Note: This is now handled by the unified Dashboard navigation system
74
+ */
75
+ setupKeyboardNavigation() {
76
+ // Keyboard navigation is now handled by Dashboard.setupUnifiedKeyboardNavigation()
77
+ // This method is kept for backward compatibility but does nothing
78
+ console.log('EventViewer: Keyboard navigation handled by unified Dashboard system');
79
+ }
80
+
81
+ /**
82
+ * Handle arrow key navigation
83
+ * @param {number} direction - Direction: 1 for down, -1 for up
84
+ */
85
+ handleArrowNavigation(direction) {
86
+ if (this.filteredEventElements.length === 0) return;
87
+
88
+ // Calculate new index
89
+ let newIndex = this.selectedEventIndex + direction;
90
+
91
+ // Wrap around
92
+ if (newIndex >= this.filteredEventElements.length) {
93
+ newIndex = 0;
94
+ } else if (newIndex < 0) {
95
+ newIndex = this.filteredEventElements.length - 1;
96
+ }
97
+
98
+ this.showEventDetails(newIndex);
99
+ }
100
+
101
+ /**
102
+ * Apply filters to events
103
+ */
104
+ applyFilters() {
105
+ // Defensive check to ensure events array exists
106
+ if (!this.events || !Array.isArray(this.events)) {
107
+ console.warn('EventViewer: events array is not initialized, using empty array');
108
+ this.events = [];
109
+ }
110
+
111
+ this.filteredEvents = this.events.filter(event => {
112
+ // Search filter
113
+ if (this.searchFilter) {
114
+ const searchableText = [
115
+ event.type || '',
116
+ event.subtype || '',
117
+ JSON.stringify(event.data || {})
118
+ ].join(' ').toLowerCase();
119
+
120
+ if (!searchableText.includes(this.searchFilter)) {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ // Type filter - now handles full hook types (like "hook.user_prompt") and main types
126
+ if (this.typeFilter) {
127
+ // Use the same logic as formatEventType to get the full event type
128
+ const eventType = event.type && event.type.trim() !== '' ? event.type : '';
129
+ const fullEventType = event.subtype && eventType ? `${eventType}.${event.subtype}` : eventType;
130
+ if (fullEventType !== this.typeFilter) {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ // Session filter
136
+ if (this.sessionFilter && this.sessionFilter !== '') {
137
+ if (!event.data || event.data.session_id !== this.sessionFilter) {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ return true;
143
+ });
144
+
145
+ this.renderEvents();
146
+ this.updateMetrics();
147
+ }
148
+
149
+ /**
150
+ * Update available event types and populate dropdown
151
+ */
152
+ updateEventTypeDropdown() {
153
+ const dropdown = document.getElementById('events-type-filter');
154
+ if (!dropdown) return;
155
+
156
+ // Extract unique event types from current events
157
+ // Use the same logic as formatEventType to get full event type names
158
+ const eventTypes = new Set();
159
+ // Defensive check to ensure events array exists
160
+ if (!this.events || !Array.isArray(this.events)) {
161
+ console.warn('EventViewer: events array is not initialized in updateEventTypeDropdown');
162
+ this.events = [];
163
+ }
164
+
165
+ this.events.forEach(event => {
166
+ if (event.type && event.type.trim() !== '') {
167
+ // Combine type and subtype if subtype exists, otherwise just use type
168
+ const fullType = event.subtype ? `${event.type}.${event.subtype}` : event.type;
169
+ eventTypes.add(fullType);
170
+ }
171
+ });
172
+
173
+ // Check if event types have changed
174
+ const currentTypes = Array.from(eventTypes).sort();
175
+ const previousTypes = Array.from(this.availableEventTypes).sort();
176
+
177
+ if (JSON.stringify(currentTypes) === JSON.stringify(previousTypes)) {
178
+ return; // No change needed
179
+ }
180
+
181
+ // Update our tracking
182
+ this.availableEventTypes = eventTypes;
183
+
184
+ // Store the current selection
185
+ const currentSelection = dropdown.value;
186
+
187
+ // Clear existing options except "All Events"
188
+ dropdown.innerHTML = '<option value="">All Events</option>';
189
+
190
+ // Add new options sorted alphabetically
191
+ const sortedTypes = Array.from(eventTypes).sort();
192
+ sortedTypes.forEach(type => {
193
+ const option = document.createElement('option');
194
+ option.value = type;
195
+ option.textContent = type;
196
+ dropdown.appendChild(option);
197
+ });
198
+
199
+ // Restore selection if it still exists
200
+ if (currentSelection && eventTypes.has(currentSelection)) {
201
+ dropdown.value = currentSelection;
202
+ } else if (currentSelection && !eventTypes.has(currentSelection)) {
203
+ // If the previously selected type no longer exists, clear the filter
204
+ dropdown.value = '';
205
+ this.typeFilter = '';
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Update the display with current events
211
+ */
212
+ updateDisplay() {
213
+ this.updateEventTypeDropdown();
214
+ this.applyFilters();
215
+ }
216
+
217
+ /**
218
+ * Render events in the UI
219
+ */
220
+ renderEvents() {
221
+ const eventsList = document.getElementById('events-list');
222
+ if (!eventsList) return;
223
+
224
+ if (this.filteredEvents.length === 0) {
225
+ eventsList.innerHTML = `
226
+ <div class="no-events">
227
+ ${this.events.length === 0 ?
228
+ 'Connect to Socket.IO server to see events...' :
229
+ 'No events match current filters...'}
230
+ </div>
231
+ `;
232
+ this.filteredEventElements = [];
233
+ return;
234
+ }
235
+
236
+ const html = this.filteredEvents.map((event, index) => {
237
+ const timestamp = new Date(event.timestamp).toLocaleTimeString();
238
+ const eventClass = event.type ? `event-${event.type}` : 'event-default';
239
+ const isSelected = index === this.selectedEventIndex;
240
+
241
+ // Get main content and timestamp separately
242
+ const mainContent = this.formatSingleRowEventContent(event);
243
+
244
+ // Check if this is an Edit/MultiEdit tool event and add diff viewer
245
+ const diffViewer = this.createInlineEditDiffViewer(event, index);
246
+
247
+ return `
248
+ <div class="event-item single-row ${eventClass} ${isSelected ? 'selected' : ''}"
249
+ onclick="eventViewer.showEventDetails(${index})"
250
+ data-index="${index}">
251
+ <span class="event-single-row-content">
252
+ <span class="event-content-main">${mainContent}</span>
253
+ <span class="event-timestamp">${timestamp}</span>
254
+ </span>
255
+ ${diffViewer}
256
+ </div>
257
+ `;
258
+ }).join('');
259
+
260
+ eventsList.innerHTML = html;
261
+
262
+ // Update filtered elements reference
263
+ this.filteredEventElements = Array.from(eventsList.querySelectorAll('.event-item'));
264
+
265
+ // Update Dashboard navigation items if we're in the events tab
266
+ if (window.dashboard && window.dashboard.currentTab === 'events' &&
267
+ window.dashboard.tabNavigation && window.dashboard.tabNavigation.events) {
268
+ window.dashboard.tabNavigation.events.items = this.filteredEventElements;
269
+ }
270
+
271
+ // Auto-scroll to bottom if enabled
272
+ if (this.autoScroll && this.filteredEvents.length > 0) {
273
+ eventsList.scrollTop = eventsList.scrollHeight;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Format event type for display
279
+ * @param {Object} event - Event object
280
+ * @returns {string} Formatted event type
281
+ */
282
+ formatEventType(event) {
283
+ if (event.subtype) {
284
+ return `${event.type}.${event.subtype}`;
285
+ }
286
+ return event.type || 'unknown';
287
+ }
288
+
289
+ /**
290
+ * Format event data for display
291
+ * @param {Object} event - Event object
292
+ * @returns {string} Formatted event data
293
+ */
294
+ formatEventData(event) {
295
+ if (!event.data) return 'No data';
296
+
297
+ // Special formatting for different event types
298
+ switch (event.type) {
299
+ case 'session':
300
+ return this.formatSessionEvent(event);
301
+ case 'claude':
302
+ return this.formatClaudeEvent(event);
303
+ case 'agent':
304
+ return this.formatAgentEvent(event);
305
+ case 'hook':
306
+ return this.formatHookEvent(event);
307
+ case 'todo':
308
+ return this.formatTodoEvent(event);
309
+ case 'memory':
310
+ return this.formatMemoryEvent(event);
311
+ case 'log':
312
+ return this.formatLogEvent(event);
313
+ default:
314
+ return this.formatGenericEvent(event);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Format session event data
320
+ */
321
+ formatSessionEvent(event) {
322
+ const data = event.data;
323
+ if (event.subtype === 'started') {
324
+ return `<strong>Session started:</strong> ${data.session_id || 'Unknown'}`;
325
+ } else if (event.subtype === 'ended') {
326
+ return `<strong>Session ended:</strong> ${data.session_id || 'Unknown'}`;
327
+ }
328
+ return `<strong>Session:</strong> ${JSON.stringify(data)}`;
329
+ }
330
+
331
+ /**
332
+ * Format Claude event data
333
+ */
334
+ formatClaudeEvent(event) {
335
+ const data = event.data;
336
+ if (event.subtype === 'request') {
337
+ const prompt = data.prompt || data.message || '';
338
+ const truncated = prompt.length > 100 ? prompt.substring(0, 100) + '...' : prompt;
339
+ return `<strong>Request:</strong> ${truncated}`;
340
+ } else if (event.subtype === 'response') {
341
+ const response = data.response || data.content || '';
342
+ const truncated = response.length > 100 ? response.substring(0, 100) + '...' : response;
343
+ return `<strong>Response:</strong> ${truncated}`;
344
+ }
345
+ return `<strong>Claude:</strong> ${JSON.stringify(data)}`;
346
+ }
347
+
348
+ /**
349
+ * Format agent event data
350
+ */
351
+ formatAgentEvent(event) {
352
+ const data = event.data;
353
+ if (event.subtype === 'loaded') {
354
+ return `<strong>Agent loaded:</strong> ${data.agent_type || data.name || 'Unknown'}`;
355
+ } else if (event.subtype === 'executed') {
356
+ return `<strong>Agent executed:</strong> ${data.agent_type || data.name || 'Unknown'}`;
357
+ }
358
+ return `<strong>Agent:</strong> ${JSON.stringify(data)}`;
359
+ }
360
+
361
+ /**
362
+ * Format hook event data
363
+ */
364
+ formatHookEvent(event) {
365
+ const data = event.data;
366
+ const eventType = data.event_type || event.subtype || 'unknown';
367
+
368
+ // Format based on specific hook event type
369
+ switch (eventType) {
370
+ case 'user_prompt':
371
+ const prompt = data.prompt_text || data.prompt_preview || '';
372
+ const truncated = prompt.length > 80 ? prompt.substring(0, 80) + '...' : prompt;
373
+ return `<strong>User Prompt:</strong> ${truncated || 'No prompt text'}`;
374
+
375
+ case 'pre_tool':
376
+ const toolName = data.tool_name || 'Unknown tool';
377
+ const operation = data.operation_type || 'operation';
378
+ return `<strong>Pre-Tool (${operation}):</strong> ${toolName}`;
379
+
380
+ case 'post_tool':
381
+ const postToolName = data.tool_name || 'Unknown tool';
382
+ const status = data.success ? 'success' : data.status || 'failed';
383
+ const duration = data.duration_ms ? ` (${data.duration_ms}ms)` : '';
384
+ return `<strong>Post-Tool (${status}):</strong> ${postToolName}${duration}`;
385
+
386
+ case 'notification':
387
+ const notifType = data.notification_type || 'notification';
388
+ const message = data.message_preview || data.message || 'No message';
389
+ return `<strong>Notification (${notifType}):</strong> ${message}`;
390
+
391
+ case 'stop':
392
+ const reason = data.reason || 'unknown';
393
+ const stopType = data.stop_type || 'normal';
394
+ return `<strong>Stop (${stopType}):</strong> ${reason}`;
395
+
396
+ case 'subagent_stop':
397
+ const agentType = data.agent_type || 'unknown agent';
398
+ const stopReason = data.reason || 'unknown';
399
+ return `<strong>Subagent Stop (${agentType}):</strong> ${stopReason}`;
400
+
401
+ default:
402
+ // Fallback to original logic for unknown hook types
403
+ const hookName = data.hook_name || data.name || data.event_type || 'Unknown';
404
+ const phase = event.subtype || eventType;
405
+ return `<strong>Hook ${phase}:</strong> ${hookName}`;
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Format todo event data
411
+ */
412
+ formatTodoEvent(event) {
413
+ const data = event.data;
414
+ if (data.todos && Array.isArray(data.todos)) {
415
+ const count = data.todos.length;
416
+ return `<strong>Todo updated:</strong> ${count} item${count !== 1 ? 's' : ''}`;
417
+ }
418
+ return `<strong>Todo:</strong> ${JSON.stringify(data)}`;
419
+ }
420
+
421
+ /**
422
+ * Format memory event data
423
+ */
424
+ formatMemoryEvent(event) {
425
+ const data = event.data;
426
+ const operation = data.operation || 'unknown';
427
+ return `<strong>Memory ${operation}:</strong> ${data.key || 'Unknown key'}`;
428
+ }
429
+
430
+ /**
431
+ * Format log event data
432
+ */
433
+ formatLogEvent(event) {
434
+ const data = event.data;
435
+ const level = data.level || 'info';
436
+ const message = data.message || '';
437
+ const truncated = message.length > 80 ? message.substring(0, 80) + '...' : message;
438
+ return `<strong>[${level.toUpperCase()}]</strong> ${truncated}`;
439
+ }
440
+
441
+ /**
442
+ * Format generic event data
443
+ */
444
+ formatGenericEvent(event) {
445
+ const data = event.data;
446
+ if (typeof data === 'string') {
447
+ return data.length > 100 ? data.substring(0, 100) + '...' : data;
448
+ }
449
+ return JSON.stringify(data);
450
+ }
451
+
452
+ /**
453
+ * Format event content for single-row display (without timestamp)
454
+ * Format: "hook.pre_tool Pre-Tool (task_management): TodoWrite"
455
+ * @param {Object} event - Event object
456
+ * @returns {string} Formatted single-row event content string
457
+ */
458
+ formatSingleRowEventContent(event) {
459
+ const eventType = this.formatEventType(event);
460
+ const data = event.data || {};
461
+
462
+ // Extract event details for different event types
463
+ let eventDetails = '';
464
+ let category = '';
465
+ let action = '';
466
+
467
+ switch (event.type) {
468
+ case 'hook':
469
+ // Hook events: extract tool name and hook type
470
+ const toolName = event.tool_name || data.tool_name || 'Unknown';
471
+ const hookType = event.subtype || 'Unknown';
472
+ const hookDisplayName = this.getHookDisplayName(hookType, data);
473
+ category = this.getEventCategory(event);
474
+ eventDetails = `${hookDisplayName} (${category}): ${toolName}`;
475
+ break;
476
+
477
+ case 'agent':
478
+ // Agent events
479
+ const agentName = event.subagent_type || data.subagent_type || 'PM';
480
+ const agentAction = event.subtype || 'action';
481
+ category = 'agent_operations';
482
+ eventDetails = `${agentName} ${agentAction}`;
483
+ break;
484
+
485
+ case 'todo':
486
+ // Todo events
487
+ const todoCount = data.todos ? data.todos.length : 0;
488
+ category = 'task_management';
489
+ eventDetails = `TodoWrite (${todoCount} items)`;
490
+ break;
491
+
492
+ case 'memory':
493
+ // Memory events
494
+ const operation = data.operation || 'unknown';
495
+ const key = data.key || 'unknown';
496
+ category = 'memory_operations';
497
+ eventDetails = `${operation} ${key}`;
498
+ break;
499
+
500
+ case 'session':
501
+ // Session events
502
+ const sessionAction = event.subtype || 'unknown';
503
+ category = 'session_management';
504
+ eventDetails = `Session ${sessionAction}`;
505
+ break;
506
+
507
+ case 'claude':
508
+ // Claude events
509
+ const claudeAction = event.subtype || 'interaction';
510
+ category = 'claude_interactions';
511
+ eventDetails = `Claude ${claudeAction}`;
512
+ break;
513
+
514
+ default:
515
+ // Generic events
516
+ category = 'general';
517
+ eventDetails = event.type || 'Unknown Event';
518
+ break;
519
+ }
520
+
521
+ // Return formatted string: "type.subtype DisplayName (category): Details"
522
+ return `${eventType} ${eventDetails}`;
523
+ }
524
+
525
+ /**
526
+ * Get display name for hook types
527
+ * @param {string} hookType - Hook subtype
528
+ * @param {Object} data - Event data
529
+ * @returns {string} Display name
530
+ */
531
+ getHookDisplayName(hookType, data) {
532
+ const hookNames = {
533
+ 'pre_tool': 'Pre-Tool',
534
+ 'post_tool': 'Post-Tool',
535
+ 'user_prompt': 'User-Prompt',
536
+ 'stop': 'Stop',
537
+ 'subagent_stop': 'Subagent-Stop',
538
+ 'notification': 'Notification'
539
+ };
540
+
541
+ return hookNames[hookType] || hookType.replace('_', '-');
542
+ }
543
+
544
+ /**
545
+ * Get event category for display
546
+ * @param {Object} event - Event object
547
+ * @returns {string} Category
548
+ */
549
+ getEventCategory(event) {
550
+ const data = event.data || {};
551
+ const toolName = event.tool_name || data.tool_name || '';
552
+
553
+ // Categorize based on tool type
554
+ if (['Read', 'Write', 'Edit', 'MultiEdit'].includes(toolName)) {
555
+ return 'file_operations';
556
+ } else if (['Bash', 'grep', 'Glob'].includes(toolName)) {
557
+ return 'system_operations';
558
+ } else if (toolName === 'TodoWrite') {
559
+ return 'task_management';
560
+ } else if (toolName === 'Task') {
561
+ return 'agent_delegation';
562
+ } else if (event.subtype === 'stop' || event.subtype === 'subagent_stop') {
563
+ return 'session_control';
564
+ }
565
+
566
+ return 'general';
567
+ }
568
+
569
+ /**
570
+ * Show event details and update selection
571
+ * @param {number} index - Index of event to show
572
+ */
573
+ showEventDetails(index) {
574
+ // Defensive checks
575
+ if (!this.filteredEvents || !Array.isArray(this.filteredEvents)) {
576
+ console.warn('EventViewer: filteredEvents array is not initialized');
577
+ return;
578
+ }
579
+ if (index < 0 || index >= this.filteredEvents.length) return;
580
+
581
+ // Update selection
582
+ this.selectedEventIndex = index;
583
+
584
+ // Get the selected event
585
+ const event = this.filteredEvents[index];
586
+
587
+ // Coordinate with Dashboard unified navigation system
588
+ if (window.dashboard) {
589
+ // Update the dashboard's navigation state for events tab
590
+ if (window.dashboard.tabNavigation && window.dashboard.tabNavigation.events) {
591
+ window.dashboard.tabNavigation.events.selectedIndex = index;
592
+ }
593
+ if (window.dashboard.selectCard) {
594
+ window.dashboard.selectCard('events', index, 'event', event);
595
+ }
596
+ }
597
+
598
+ // Update visual selection (this will be handled by Dashboard.updateCardSelectionUI())
599
+ this.filteredEventElements.forEach((el, i) => {
600
+ el.classList.toggle('selected', i === index);
601
+ });
602
+
603
+ // Notify other components about selection
604
+ document.dispatchEvent(new CustomEvent('eventSelected', {
605
+ detail: { event, index }
606
+ }));
607
+
608
+ // Scroll to selected event if not visible
609
+ const selectedElement = this.filteredEventElements[index];
610
+ if (selectedElement) {
611
+ selectedElement.scrollIntoView({
612
+ behavior: 'smooth',
613
+ block: 'nearest'
614
+ });
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Clear event selection
620
+ */
621
+ clearSelection() {
622
+ this.selectedEventIndex = -1;
623
+ this.filteredEventElements.forEach(el => {
624
+ el.classList.remove('selected');
625
+ });
626
+
627
+ // Coordinate with Dashboard unified navigation system
628
+ if (window.dashboard) {
629
+ if (window.dashboard.tabNavigation && window.dashboard.tabNavigation.events) {
630
+ window.dashboard.tabNavigation.events.selectedIndex = -1;
631
+ }
632
+ if (window.dashboard.clearCardSelection) {
633
+ window.dashboard.clearCardSelection();
634
+ }
635
+ }
636
+
637
+ // Notify other components
638
+ document.dispatchEvent(new CustomEvent('eventSelectionCleared'));
639
+ }
640
+
641
+ /**
642
+ * Update metrics display
643
+ */
644
+ updateMetrics() {
645
+ // Update event type counts
646
+ this.eventTypeCount = {};
647
+ this.errorCount = 0;
648
+
649
+ // Defensive check to ensure events array exists
650
+ if (!this.events || !Array.isArray(this.events)) {
651
+ console.warn('EventViewer: events array is not initialized in updateMetrics');
652
+ this.events = [];
653
+ }
654
+
655
+ this.events.forEach(event => {
656
+ const type = event.type || 'unknown';
657
+ this.eventTypeCount[type] = (this.eventTypeCount[type] || 0) + 1;
658
+
659
+ if (event.type === 'log' &&
660
+ event.data &&
661
+ ['error', 'critical'].includes(event.data.level)) {
662
+ this.errorCount++;
663
+ }
664
+ });
665
+
666
+ // Update events per minute
667
+ const currentMinute = new Date().getMinutes();
668
+ if (currentMinute !== this.lastMinute) {
669
+ this.lastMinute = currentMinute;
670
+ this.eventsThisMinute = 0;
671
+ }
672
+
673
+ // Count events in the last minute
674
+ const oneMinuteAgo = new Date(Date.now() - 60000);
675
+ this.eventsThisMinute = this.events.filter(event =>
676
+ new Date(event.timestamp) > oneMinuteAgo
677
+ ).length;
678
+
679
+ // Update UI
680
+ this.updateMetricsUI();
681
+ }
682
+
683
+ /**
684
+ * Update metrics in the UI
685
+ */
686
+ updateMetricsUI() {
687
+ const totalEventsEl = document.getElementById('total-events');
688
+ const eventsPerMinuteEl = document.getElementById('events-per-minute');
689
+ const uniqueTypesEl = document.getElementById('unique-types');
690
+ const errorCountEl = document.getElementById('error-count');
691
+
692
+ if (totalEventsEl) totalEventsEl.textContent = this.events.length;
693
+ if (eventsPerMinuteEl) eventsPerMinuteEl.textContent = this.eventsThisMinute;
694
+ if (uniqueTypesEl) uniqueTypesEl.textContent = Object.keys(this.eventTypeCount).length;
695
+ if (errorCountEl) errorCountEl.textContent = this.errorCount;
696
+ }
697
+
698
+ /**
699
+ * Export events to JSON
700
+ */
701
+ exportEvents() {
702
+ const dataStr = JSON.stringify(this.filteredEvents, null, 2);
703
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
704
+ const url = URL.createObjectURL(dataBlob);
705
+
706
+ const link = document.createElement('a');
707
+ link.href = url;
708
+ link.download = `claude-mpm-events-${new Date().toISOString().split('T')[0]}.json`;
709
+ link.click();
710
+
711
+ URL.revokeObjectURL(url);
712
+ }
713
+
714
+ /**
715
+ * Clear all events
716
+ */
717
+ clearEvents() {
718
+ this.socketClient.clearEvents();
719
+ this.selectedEventIndex = -1;
720
+ this.updateDisplay();
721
+ }
722
+
723
+ /**
724
+ * Set session filter
725
+ * @param {string} sessionId - Session ID to filter by
726
+ */
727
+ setSessionFilter(sessionId) {
728
+ this.sessionFilter = sessionId;
729
+ this.applyFilters();
730
+ }
731
+
732
+ /**
733
+ * Get current filter state
734
+ * @returns {Object} Current filters
735
+ */
736
+ getFilters() {
737
+ return {
738
+ search: this.searchFilter,
739
+ type: this.typeFilter,
740
+ session: this.sessionFilter
741
+ };
742
+ }
743
+
744
+ /**
745
+ * Get filtered events (used by HUD and other components)
746
+ * @returns {Array} Array of filtered events
747
+ */
748
+ getFilteredEvents() {
749
+ return this.filteredEvents;
750
+ }
751
+
752
+ /**
753
+ * Get all events (unfiltered, used by HUD for complete visualization)
754
+ * @returns {Array} Array of all events
755
+ */
756
+ getAllEvents() {
757
+ return this.events;
758
+ }
759
+
760
+ /**
761
+ * Create inline diff viewer for Edit/MultiEdit tool events
762
+ * WHY: Provides immediate visibility of file changes without needing to open modals
763
+ * DESIGN DECISION: Shows inline diffs only for Edit/MultiEdit events to avoid clutter
764
+ * @param {Object} event - Event object
765
+ * @param {number} index - Event index for unique IDs
766
+ * @returns {string} HTML for inline diff viewer
767
+ */
768
+ createInlineEditDiffViewer(event, index) {
769
+ const data = event.data || {};
770
+ const toolName = event.tool_name || data.tool_name || '';
771
+
772
+ // Only show for Edit and MultiEdit tools
773
+ if (!['Edit', 'MultiEdit'].includes(toolName)) {
774
+ return '';
775
+ }
776
+
777
+ // Extract edit parameters based on tool type
778
+ let edits = [];
779
+ if (toolName === 'Edit') {
780
+ // Single edit
781
+ const parameters = event.tool_parameters || data.tool_parameters || {};
782
+ if (parameters.old_string && parameters.new_string) {
783
+ edits.push({
784
+ old_string: parameters.old_string,
785
+ new_string: parameters.new_string,
786
+ file_path: parameters.file_path || 'unknown'
787
+ });
788
+ }
789
+ } else if (toolName === 'MultiEdit') {
790
+ // Multiple edits
791
+ const parameters = event.tool_parameters || data.tool_parameters || {};
792
+ if (parameters.edits && Array.isArray(parameters.edits)) {
793
+ edits = parameters.edits.map(edit => ({
794
+ ...edit,
795
+ file_path: parameters.file_path || 'unknown'
796
+ }));
797
+ }
798
+ }
799
+
800
+ if (edits.length === 0) {
801
+ return '';
802
+ }
803
+
804
+ // Create collapsible diff section
805
+ const diffId = `edit-diff-${index}`;
806
+ const isMultiEdit = edits.length > 1;
807
+
808
+ let diffContent = '';
809
+ edits.forEach((edit, editIndex) => {
810
+ const editId = `${diffId}-${editIndex}`;
811
+ const diffHtml = this.createDiffHtml(edit.old_string, edit.new_string);
812
+
813
+ diffContent += `
814
+ <div class="edit-diff-section">
815
+ ${isMultiEdit ? `<div class="edit-diff-header">Edit ${editIndex + 1}</div>` : ''}
816
+ <div class="diff-content">${diffHtml}</div>
817
+ </div>
818
+ `;
819
+ });
820
+
821
+ return `
822
+ <div class="inline-edit-diff-viewer">
823
+ <div class="diff-toggle-header" onclick="eventViewer.toggleEditDiff('${diffId}', event)">
824
+ <span class="diff-toggle-icon">📋</span>
825
+ <span class="diff-toggle-text">Show ${isMultiEdit ? edits.length + ' edits' : 'edit'}</span>
826
+ <span class="diff-toggle-arrow">▼</span>
827
+ </div>
828
+ <div id="${diffId}" class="diff-content-container" style="display: none;">
829
+ ${diffContent}
830
+ </div>
831
+ </div>
832
+ `;
833
+ }
834
+
835
+ /**
836
+ * Create HTML diff visualization
837
+ * WHY: Provides clear visual representation of text changes similar to git diff
838
+ * @param {string} oldText - Original text
839
+ * @param {string} newText - Modified text
840
+ * @returns {string} HTML diff content
841
+ */
842
+ createDiffHtml(oldText, newText) {
843
+ // Simple line-by-line diff implementation
844
+ const oldLines = oldText.split('\n');
845
+ const newLines = newText.split('\n');
846
+
847
+ let diffHtml = '';
848
+ let i = 0, j = 0;
849
+
850
+ // Simple diff algorithm - can be enhanced with proper diff library if needed
851
+ while (i < oldLines.length || j < newLines.length) {
852
+ const oldLine = i < oldLines.length ? oldLines[i] : null;
853
+ const newLine = j < newLines.length ? newLines[j] : null;
854
+
855
+ if (oldLine === null) {
856
+ // New line added
857
+ diffHtml += `<div class="diff-line diff-added">+ ${this.escapeHtml(newLine)}</div>`;
858
+ j++;
859
+ } else if (newLine === null) {
860
+ // Old line removed
861
+ diffHtml += `<div class="diff-line diff-removed">- ${this.escapeHtml(oldLine)}</div>`;
862
+ i++;
863
+ } else if (oldLine === newLine) {
864
+ // Lines are the same
865
+ diffHtml += `<div class="diff-line diff-unchanged"> ${this.escapeHtml(oldLine)}</div>`;
866
+ i++;
867
+ j++;
868
+ } else {
869
+ // Lines are different - show both
870
+ diffHtml += `<div class="diff-line diff-removed">- ${this.escapeHtml(oldLine)}</div>`;
871
+ diffHtml += `<div class="diff-line diff-added">+ ${this.escapeHtml(newLine)}</div>`;
872
+ i++;
873
+ j++;
874
+ }
875
+ }
876
+
877
+ return `<div class="diff-container">${diffHtml}</div>`;
878
+ }
879
+
880
+ /**
881
+ * Toggle edit diff visibility
882
+ * @param {string} diffId - Diff container ID
883
+ * @param {Event} event - Click event
884
+ */
885
+ toggleEditDiff(diffId, event) {
886
+ // Prevent event bubbling to parent event item
887
+ event.stopPropagation();
888
+
889
+ const diffContainer = document.getElementById(diffId);
890
+ const arrow = event.currentTarget.querySelector('.diff-toggle-arrow');
891
+
892
+ if (diffContainer) {
893
+ const isVisible = diffContainer.style.display !== 'none';
894
+ diffContainer.style.display = isVisible ? 'none' : 'block';
895
+ if (arrow) {
896
+ arrow.textContent = isVisible ? '▼' : '▲';
897
+ }
898
+ }
899
+ }
900
+
901
+ /**
902
+ * Escape HTML characters for safe display
903
+ * @param {string} text - Text to escape
904
+ * @returns {string} Escaped text
905
+ */
906
+ escapeHtml(text) {
907
+ const div = document.createElement('div');
908
+ div.textContent = text;
909
+ return div.innerHTML;
910
+ }
911
+ }
912
+
913
+ // Export for global use
914
+ window.EventViewer = EventViewer;