claude-mpm 3.4.13__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.
- claude_mpm/dashboard/index.html +13 -0
- claude_mpm/dashboard/static/css/dashboard.css +2722 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +619 -0
- claude_mpm/dashboard/static/js/components/event-processor.js +641 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +914 -0
- claude_mpm/dashboard/static/js/components/export-manager.js +362 -0
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +611 -0
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +211 -0
- claude_mpm/dashboard/static/js/components/hud-manager.js +671 -0
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +1718 -0
- claude_mpm/dashboard/static/js/components/module-viewer.js +2701 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +520 -0
- claude_mpm/dashboard/static/js/components/socket-manager.js +343 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +427 -0
- claude_mpm/dashboard/static/js/components/working-directory.js +866 -0
- claude_mpm/dashboard/static/js/dashboard-original.js +4134 -0
- claude_mpm/dashboard/static/js/dashboard.js +1978 -0
- claude_mpm/dashboard/static/js/socket-client.js +537 -0
- claude_mpm/dashboard/templates/index.html +346 -0
- claude_mpm/dashboard/test_dashboard.html +372 -0
- claude_mpm/services/socketio_server.py +41 -5
- {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.14.dist-info}/METADATA +2 -1
- {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.14.dist-info}/RECORD +27 -7
- {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.14.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.14.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.14.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.14.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;
|