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,4134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Dashboard Application
|
|
3
|
+
* Coordinates all components and handles tab management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Dashboard {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Components
|
|
9
|
+
this.socketClient = null;
|
|
10
|
+
this.eventViewer = null;
|
|
11
|
+
this.moduleViewer = null;
|
|
12
|
+
this.sessionManager = null;
|
|
13
|
+
this.hudVisualizer = null;
|
|
14
|
+
|
|
15
|
+
// State
|
|
16
|
+
this.currentTab = 'events';
|
|
17
|
+
this.autoScroll = true;
|
|
18
|
+
this.hudMode = false;
|
|
19
|
+
|
|
20
|
+
// Working directory state - will be set properly during initialization
|
|
21
|
+
this.currentWorkingDir = null;
|
|
22
|
+
|
|
23
|
+
// Selection state - tracks the currently selected card across all tabs
|
|
24
|
+
this.selectedCard = {
|
|
25
|
+
tab: null, // which tab the selection is in
|
|
26
|
+
index: null, // index of selected item in that tab
|
|
27
|
+
type: null, // 'event', 'agent', 'tool', 'file'
|
|
28
|
+
data: null // the actual data object
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Navigation state for each tab
|
|
32
|
+
this.tabNavigation = {
|
|
33
|
+
events: { selectedIndex: -1, items: [] },
|
|
34
|
+
agents: { selectedIndex: -1, items: [] },
|
|
35
|
+
tools: { selectedIndex: -1, items: [] },
|
|
36
|
+
files: { selectedIndex: -1, items: [] }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// File tracking for files tab
|
|
40
|
+
this.fileOperations = new Map(); // Map of file paths to operations
|
|
41
|
+
|
|
42
|
+
// Tool call tracking for tools tab
|
|
43
|
+
this.toolCalls = new Map(); // Map of tool call keys to paired pre/post events
|
|
44
|
+
|
|
45
|
+
// Agent events tracking for agents tab
|
|
46
|
+
this.agentEvents = []; // Array of filtered agent events
|
|
47
|
+
|
|
48
|
+
this.init();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Initialize the dashboard
|
|
53
|
+
*/
|
|
54
|
+
init() {
|
|
55
|
+
// Initialize components
|
|
56
|
+
this.initializeComponents();
|
|
57
|
+
this.setupEventHandlers();
|
|
58
|
+
this.setupTabNavigation();
|
|
59
|
+
this.setupUnifiedKeyboardNavigation();
|
|
60
|
+
this.initializeFromURL();
|
|
61
|
+
|
|
62
|
+
// Initialize agent inference system
|
|
63
|
+
this.initializeAgentInference();
|
|
64
|
+
|
|
65
|
+
// Initialize working directory for current session
|
|
66
|
+
this.initializeWorkingDirectory();
|
|
67
|
+
|
|
68
|
+
// Watch for footer directory changes
|
|
69
|
+
this.watchFooterDirectory();
|
|
70
|
+
|
|
71
|
+
// Initialize HUD button state
|
|
72
|
+
this.updateHUDButtonState();
|
|
73
|
+
|
|
74
|
+
console.log('Claude MPM Dashboard initialized');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize agent inference system
|
|
79
|
+
* Based on docs/design/main-subagent-identification.md
|
|
80
|
+
*/
|
|
81
|
+
initializeAgentInference() {
|
|
82
|
+
// Agent inference state tracking
|
|
83
|
+
this.agentInference = {
|
|
84
|
+
// Track current subagent delegation context
|
|
85
|
+
currentDelegation: null,
|
|
86
|
+
// Map of session_id -> agent context
|
|
87
|
+
sessionAgents: new Map(),
|
|
88
|
+
// Map of event indices -> inferred agent
|
|
89
|
+
eventAgentMap: new Map()
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
console.log('Agent inference system initialized');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Infer agent context from event payload
|
|
97
|
+
* Based on production-ready detection from design document
|
|
98
|
+
* @param {Object} event - Event payload
|
|
99
|
+
* @returns {Object} - {type: 'main_agent'|'subagent', confidence: 'definitive'|'high'|'medium'|'default', agentName: string}
|
|
100
|
+
*/
|
|
101
|
+
inferAgentFromEvent(event) {
|
|
102
|
+
// Handle both direct properties and nested data properties
|
|
103
|
+
const data = event.data || {};
|
|
104
|
+
const sessionId = event.session_id || data.session_id || 'unknown';
|
|
105
|
+
const eventType = event.hook_event_name || data.hook_event_name || event.type || '';
|
|
106
|
+
const subtype = event.subtype || '';
|
|
107
|
+
const toolName = event.tool_name || data.tool_name || '';
|
|
108
|
+
|
|
109
|
+
// Direct event detection (highest confidence) - from design doc
|
|
110
|
+
if (eventType === 'SubagentStop' || subtype === 'subagent_stop') {
|
|
111
|
+
const agentName = this.extractAgentNameFromEvent(event);
|
|
112
|
+
return {
|
|
113
|
+
type: 'subagent',
|
|
114
|
+
confidence: 'definitive',
|
|
115
|
+
agentName: agentName,
|
|
116
|
+
reason: 'SubagentStop event'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (eventType === 'Stop' || subtype === 'stop') {
|
|
121
|
+
return {
|
|
122
|
+
type: 'main_agent',
|
|
123
|
+
confidence: 'definitive',
|
|
124
|
+
agentName: 'PM',
|
|
125
|
+
reason: 'Stop event'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Tool-based detection (high confidence) - from design doc
|
|
130
|
+
if (toolName === 'Task') {
|
|
131
|
+
const agentName = this.extractSubagentTypeFromTask(event);
|
|
132
|
+
if (agentName) {
|
|
133
|
+
return {
|
|
134
|
+
type: 'subagent',
|
|
135
|
+
confidence: 'high',
|
|
136
|
+
agentName: agentName,
|
|
137
|
+
reason: 'Task tool with subagent_type'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Hook event pattern analysis (high confidence)
|
|
143
|
+
if (eventType === 'PreToolUse' && toolName === 'Task') {
|
|
144
|
+
const agentName = this.extractSubagentTypeFromTask(event);
|
|
145
|
+
if (agentName) {
|
|
146
|
+
return {
|
|
147
|
+
type: 'subagent',
|
|
148
|
+
confidence: 'high',
|
|
149
|
+
agentName: agentName,
|
|
150
|
+
reason: 'PreToolUse Task delegation'
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Session pattern analysis (medium confidence) - from design doc
|
|
156
|
+
if (sessionId) {
|
|
157
|
+
const sessionLower = sessionId.toLowerCase();
|
|
158
|
+
if (['subagent', 'task', 'agent-'].some(pattern => sessionLower.includes(pattern))) {
|
|
159
|
+
return {
|
|
160
|
+
type: 'subagent',
|
|
161
|
+
confidence: 'medium',
|
|
162
|
+
agentName: 'Subagent',
|
|
163
|
+
reason: 'Session ID pattern'
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Agent type field analysis
|
|
169
|
+
const agentType = event.agent_type || event.data?.agent_type;
|
|
170
|
+
const subagentType = event.subagent_type || event.data?.subagent_type;
|
|
171
|
+
|
|
172
|
+
if (subagentType && subagentType !== 'unknown') {
|
|
173
|
+
return {
|
|
174
|
+
type: 'subagent',
|
|
175
|
+
confidence: 'high',
|
|
176
|
+
agentName: subagentType,
|
|
177
|
+
reason: 'subagent_type field'
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (agentType && agentType !== 'unknown' && agentType !== 'main') {
|
|
182
|
+
return {
|
|
183
|
+
type: 'subagent',
|
|
184
|
+
confidence: 'medium',
|
|
185
|
+
agentName: agentType,
|
|
186
|
+
reason: 'agent_type field'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Default to main agent (from design doc)
|
|
191
|
+
return {
|
|
192
|
+
type: 'main_agent',
|
|
193
|
+
confidence: 'default',
|
|
194
|
+
agentName: 'PM',
|
|
195
|
+
reason: 'default classification'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extract subagent type from Task tool parameters
|
|
201
|
+
* @param {Object} event - Event with Task tool
|
|
202
|
+
* @returns {string|null} - Subagent type or null
|
|
203
|
+
*/
|
|
204
|
+
extractSubagentTypeFromTask(event) {
|
|
205
|
+
// Check tool_parameters directly
|
|
206
|
+
if (event.tool_parameters?.subagent_type) {
|
|
207
|
+
return event.tool_parameters.subagent_type;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check nested in data.tool_parameters (hook events)
|
|
211
|
+
if (event.data?.tool_parameters?.subagent_type) {
|
|
212
|
+
return event.data.tool_parameters.subagent_type;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check delegation_details (new structure)
|
|
216
|
+
if (event.data?.delegation_details?.agent_type) {
|
|
217
|
+
return event.data.delegation_details.agent_type;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check tool_input fallback
|
|
221
|
+
if (event.tool_input?.subagent_type) {
|
|
222
|
+
return event.tool_input.subagent_type;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract agent name from any event
|
|
230
|
+
* @param {Object} event - Event payload
|
|
231
|
+
* @returns {string} - Agent name
|
|
232
|
+
*/
|
|
233
|
+
extractAgentNameFromEvent(event) {
|
|
234
|
+
// Priority order based on reliability from design doc
|
|
235
|
+
const data = event.data || {};
|
|
236
|
+
|
|
237
|
+
// 1. Task tool subagent_type (highest priority)
|
|
238
|
+
if (event.tool_name === 'Task' || data.tool_name === 'Task') {
|
|
239
|
+
const taskAgent = this.extractSubagentTypeFromTask(event);
|
|
240
|
+
if (taskAgent) return taskAgent;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2. Direct subagent_type field
|
|
244
|
+
if (event.subagent_type && event.subagent_type !== 'unknown') {
|
|
245
|
+
return event.subagent_type;
|
|
246
|
+
}
|
|
247
|
+
if (data.subagent_type && data.subagent_type !== 'unknown') {
|
|
248
|
+
return data.subagent_type;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 3. Agent type fields (but not 'main' or 'unknown')
|
|
252
|
+
if (event.agent_type && !['main', 'unknown'].includes(event.agent_type)) {
|
|
253
|
+
return event.agent_type;
|
|
254
|
+
}
|
|
255
|
+
if (data.agent_type && !['main', 'unknown'].includes(data.agent_type)) {
|
|
256
|
+
return event.agent_type;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (event.data?.agent_type && !['main', 'unknown'].includes(event.data.agent_type)) {
|
|
260
|
+
return event.data.agent_type;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 5. Other fallbacks
|
|
264
|
+
if (event.agent && event.agent !== 'unknown') {
|
|
265
|
+
return event.agent;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (event.name && event.name !== 'unknown') {
|
|
269
|
+
return event.name;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Default fallback
|
|
273
|
+
return 'Unknown';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Process all events and build agent inference context
|
|
278
|
+
* This tracks delegation boundaries and agent context throughout the session
|
|
279
|
+
*/
|
|
280
|
+
processAgentInference() {
|
|
281
|
+
const events = this.eventViewer.events;
|
|
282
|
+
|
|
283
|
+
// Reset inference state
|
|
284
|
+
this.agentInference.currentDelegation = null;
|
|
285
|
+
this.agentInference.sessionAgents.clear();
|
|
286
|
+
this.agentInference.eventAgentMap.clear();
|
|
287
|
+
|
|
288
|
+
console.log('Processing agent inference for', events.length, 'events');
|
|
289
|
+
|
|
290
|
+
// Process events chronologically to track delegation context
|
|
291
|
+
events.forEach((event, index) => {
|
|
292
|
+
const inference = this.inferAgentFromEvent(event);
|
|
293
|
+
const sessionId = event.session_id || 'default';
|
|
294
|
+
|
|
295
|
+
// Track delegation boundaries
|
|
296
|
+
if (event.tool_name === 'Task' && inference.type === 'subagent') {
|
|
297
|
+
// Start of subagent delegation
|
|
298
|
+
this.agentInference.currentDelegation = {
|
|
299
|
+
agentName: inference.agentName,
|
|
300
|
+
sessionId: sessionId,
|
|
301
|
+
startIndex: index,
|
|
302
|
+
endIndex: null
|
|
303
|
+
};
|
|
304
|
+
console.log('Delegation started:', this.agentInference.currentDelegation);
|
|
305
|
+
} else if (inference.confidence === 'definitive' && inference.reason === 'SubagentStop event') {
|
|
306
|
+
// End of subagent delegation
|
|
307
|
+
if (this.agentInference.currentDelegation) {
|
|
308
|
+
this.agentInference.currentDelegation.endIndex = index;
|
|
309
|
+
console.log('Delegation ended:', this.agentInference.currentDelegation);
|
|
310
|
+
this.agentInference.currentDelegation = null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Determine agent for this event based on context
|
|
315
|
+
let finalAgent = inference;
|
|
316
|
+
|
|
317
|
+
// If we're in a delegation context and this event doesn't have high confidence agent info,
|
|
318
|
+
// inherit from delegation context
|
|
319
|
+
if (this.agentInference.currentDelegation &&
|
|
320
|
+
inference.confidence === 'default' &&
|
|
321
|
+
sessionId === this.agentInference.currentDelegation.sessionId) {
|
|
322
|
+
finalAgent = {
|
|
323
|
+
type: 'subagent',
|
|
324
|
+
confidence: 'inherited',
|
|
325
|
+
agentName: this.agentInference.currentDelegation.agentName,
|
|
326
|
+
reason: 'inherited from delegation context'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Store the inference result
|
|
331
|
+
this.agentInference.eventAgentMap.set(index, finalAgent);
|
|
332
|
+
|
|
333
|
+
// Update session agent tracking
|
|
334
|
+
this.agentInference.sessionAgents.set(sessionId, finalAgent);
|
|
335
|
+
|
|
336
|
+
// Debug first few inferences
|
|
337
|
+
if (index < 5) {
|
|
338
|
+
console.log(`Event ${index} agent inference:`, {
|
|
339
|
+
event_type: event.type,
|
|
340
|
+
subtype: event.subtype,
|
|
341
|
+
tool_name: event.tool_name,
|
|
342
|
+
inference: finalAgent
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
console.log('Agent inference processing complete. Results:', {
|
|
348
|
+
total_events: events.length,
|
|
349
|
+
inferred_agents: this.agentInference.eventAgentMap.size,
|
|
350
|
+
unique_sessions: this.agentInference.sessionAgents.size
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get inferred agent for a specific event
|
|
356
|
+
* @param {number} eventIndex - Index of event in events array
|
|
357
|
+
* @returns {Object|null} - Agent inference result or null
|
|
358
|
+
*/
|
|
359
|
+
getInferredAgent(eventIndex) {
|
|
360
|
+
return this.agentInference.eventAgentMap.get(eventIndex) || null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get inferred agent for an event object
|
|
365
|
+
* @param {Object} event - Event object
|
|
366
|
+
* @returns {Object|null} - Agent inference result or null
|
|
367
|
+
*/
|
|
368
|
+
getInferredAgentForEvent(event) {
|
|
369
|
+
const events = this.eventViewer.events;
|
|
370
|
+
const eventIndex = events.indexOf(event);
|
|
371
|
+
if (eventIndex === -1) return null;
|
|
372
|
+
|
|
373
|
+
return this.getInferredAgent(eventIndex);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Initialize all components
|
|
378
|
+
*/
|
|
379
|
+
initializeComponents() {
|
|
380
|
+
// Initialize socket client
|
|
381
|
+
this.socketClient = new SocketClient();
|
|
382
|
+
|
|
383
|
+
// Initialize UI components
|
|
384
|
+
this.eventViewer = new EventViewer('events-list', this.socketClient);
|
|
385
|
+
this.moduleViewer = new ModuleViewer('module-content');
|
|
386
|
+
this.sessionManager = new SessionManager(this.socketClient);
|
|
387
|
+
this.hudVisualizer = new HUDVisualizer();
|
|
388
|
+
|
|
389
|
+
// Store globally for backward compatibility
|
|
390
|
+
window.socketClient = this.socketClient;
|
|
391
|
+
window.eventViewer = this.eventViewer;
|
|
392
|
+
window.moduleViewer = this.moduleViewer;
|
|
393
|
+
window.sessionManager = this.sessionManager;
|
|
394
|
+
window.hudVisualizer = this.hudVisualizer;
|
|
395
|
+
|
|
396
|
+
// Initialize HUD visualizer
|
|
397
|
+
this.hudVisualizer.initialize();
|
|
398
|
+
|
|
399
|
+
// Setup component interactions
|
|
400
|
+
this.setupComponentInteractions();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Setup interactions between components
|
|
405
|
+
*/
|
|
406
|
+
setupComponentInteractions() {
|
|
407
|
+
// Socket connection status is now handled in the header connection status badge
|
|
408
|
+
// Footer now focuses on session-specific information
|
|
409
|
+
|
|
410
|
+
// Listen for socket events to update file operations and tool calls
|
|
411
|
+
this.socketClient.onEventUpdate((events) => {
|
|
412
|
+
this.updateFileOperations(events);
|
|
413
|
+
this.updateToolCalls(events);
|
|
414
|
+
// Process agent inference after events are updated
|
|
415
|
+
this.processAgentInference();
|
|
416
|
+
this.renderCurrentTab();
|
|
417
|
+
|
|
418
|
+
// Process new events for HUD visualization
|
|
419
|
+
if (this.hudMode && this.hudVisualizer && events.length > 0) {
|
|
420
|
+
// Get the most recent event for HUD processing
|
|
421
|
+
const latestEvent = events[events.length - 1];
|
|
422
|
+
this.handleHUDEvent(latestEvent);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Auto-scroll events list if on events tab
|
|
426
|
+
if (this.currentTab === 'events') {
|
|
427
|
+
this.scrollListToBottom('events-list');
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Listen for connection status changes
|
|
432
|
+
document.addEventListener('socketConnectionStatus', (e) => {
|
|
433
|
+
this.updateConnectionStatus(e.detail.status, e.detail.type);
|
|
434
|
+
|
|
435
|
+
// Set up git branch listener when connected
|
|
436
|
+
if (e.detail.type === 'connected' && this.socketClient && this.socketClient.socket) {
|
|
437
|
+
// Remove any existing listener first
|
|
438
|
+
this.socketClient.socket.off('git_branch_response');
|
|
439
|
+
|
|
440
|
+
// Add the listener
|
|
441
|
+
this.socketClient.socket.on('git_branch_response', (data) => {
|
|
442
|
+
if (data.success) {
|
|
443
|
+
const footerBranch = document.getElementById('footer-git-branch');
|
|
444
|
+
if (footerBranch) {
|
|
445
|
+
footerBranch.textContent = data.branch;
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
const footerBranch = document.getElementById('footer-git-branch');
|
|
449
|
+
if (footerBranch) {
|
|
450
|
+
footerBranch.textContent = 'No Git';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Request git branch for current working directory
|
|
456
|
+
this.updateGitBranch(this.currentWorkingDir);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Listen for session filter changes to update dropdown options
|
|
461
|
+
document.addEventListener('sessionFilterChanged', (e) => {
|
|
462
|
+
console.log('Session filter changed, re-rendering current tab:', this.currentTab);
|
|
463
|
+
this.renderCurrentTab();
|
|
464
|
+
// Update HUD button state based on session selection
|
|
465
|
+
this.updateHUDButtonState();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Setup general event handlers
|
|
471
|
+
*/
|
|
472
|
+
setupEventHandlers() {
|
|
473
|
+
// Connection controls
|
|
474
|
+
const connectBtn = document.getElementById('connect-btn');
|
|
475
|
+
const disconnectBtn = document.getElementById('disconnect-btn');
|
|
476
|
+
const portInput = document.getElementById('port-input');
|
|
477
|
+
|
|
478
|
+
if (connectBtn) {
|
|
479
|
+
connectBtn.addEventListener('click', () => {
|
|
480
|
+
const port = portInput ? portInput.value : '8765';
|
|
481
|
+
this.socketClient.connect(port);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (disconnectBtn) {
|
|
486
|
+
disconnectBtn.addEventListener('click', () => {
|
|
487
|
+
this.socketClient.disconnect();
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Connection toggle button
|
|
492
|
+
const connectionToggleBtn = document.getElementById('connection-toggle-btn');
|
|
493
|
+
if (connectionToggleBtn) {
|
|
494
|
+
connectionToggleBtn.addEventListener('click', () => {
|
|
495
|
+
this.toggleConnectionControls();
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Working directory controls
|
|
500
|
+
const changeDirBtn = document.getElementById('change-dir-btn');
|
|
501
|
+
const workingDirPath = document.getElementById('working-dir-path');
|
|
502
|
+
|
|
503
|
+
if (changeDirBtn) {
|
|
504
|
+
changeDirBtn.addEventListener('click', () => {
|
|
505
|
+
this.showChangeDirDialog();
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (workingDirPath) {
|
|
510
|
+
workingDirPath.addEventListener('click', () => {
|
|
511
|
+
this.showChangeDirDialog();
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Action buttons
|
|
516
|
+
const clearBtn = document.querySelector('button[onclick="clearEvents()"]');
|
|
517
|
+
const exportBtn = document.getElementById('export-btn');
|
|
518
|
+
|
|
519
|
+
if (clearBtn) {
|
|
520
|
+
clearBtn.addEventListener('click', () => {
|
|
521
|
+
this.clearEvents();
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (exportBtn) {
|
|
526
|
+
exportBtn.addEventListener('click', () => {
|
|
527
|
+
this.exportEvents();
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// HUD toggle button
|
|
532
|
+
const hudToggleBtn = document.getElementById('hud-toggle-btn');
|
|
533
|
+
if (hudToggleBtn) {
|
|
534
|
+
hudToggleBtn.addEventListener('click', () => {
|
|
535
|
+
this.toggleHUD();
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Clear selection button
|
|
540
|
+
const clearSelectionBtn = document.querySelector('button[onclick="clearSelection()"]');
|
|
541
|
+
if (clearSelectionBtn) {
|
|
542
|
+
clearSelectionBtn.addEventListener('click', () => {
|
|
543
|
+
this.clearSelection();
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Tab-specific filters
|
|
548
|
+
this.setupTabFilters();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Setup filtering for each tab
|
|
553
|
+
*/
|
|
554
|
+
setupTabFilters() {
|
|
555
|
+
// Agents tab filters
|
|
556
|
+
const agentsSearchInput = document.getElementById('agents-search-input');
|
|
557
|
+
const agentsTypeFilter = document.getElementById('agents-type-filter');
|
|
558
|
+
|
|
559
|
+
if (agentsSearchInput) {
|
|
560
|
+
agentsSearchInput.addEventListener('input', () => {
|
|
561
|
+
if (this.currentTab === 'agents') this.renderCurrentTab();
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (agentsTypeFilter) {
|
|
566
|
+
agentsTypeFilter.addEventListener('change', () => {
|
|
567
|
+
if (this.currentTab === 'agents') this.renderCurrentTab();
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Tools tab filters
|
|
572
|
+
const toolsSearchInput = document.getElementById('tools-search-input');
|
|
573
|
+
const toolsTypeFilter = document.getElementById('tools-type-filter');
|
|
574
|
+
|
|
575
|
+
if (toolsSearchInput) {
|
|
576
|
+
toolsSearchInput.addEventListener('input', () => {
|
|
577
|
+
if (this.currentTab === 'tools') this.renderCurrentTab();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (toolsTypeFilter) {
|
|
582
|
+
toolsTypeFilter.addEventListener('change', () => {
|
|
583
|
+
if (this.currentTab === 'tools') this.renderCurrentTab();
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Files tab filters
|
|
588
|
+
const filesSearchInput = document.getElementById('files-search-input');
|
|
589
|
+
const filesTypeFilter = document.getElementById('files-type-filter');
|
|
590
|
+
|
|
591
|
+
if (filesSearchInput) {
|
|
592
|
+
filesSearchInput.addEventListener('input', () => {
|
|
593
|
+
if (this.currentTab === 'files') this.renderCurrentTab();
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (filesTypeFilter) {
|
|
598
|
+
filesTypeFilter.addEventListener('change', () => {
|
|
599
|
+
if (this.currentTab === 'files') this.renderCurrentTab();
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Populate filter dropdown with unique values from data
|
|
606
|
+
* @param {string} selectId - ID of the select element
|
|
607
|
+
* @param {Array} values - Array of unique values to populate
|
|
608
|
+
* @param {string} allOption - Text for the "All" option
|
|
609
|
+
*/
|
|
610
|
+
populateFilterDropdown(selectId, values, allOption) {
|
|
611
|
+
const select = document.getElementById(selectId);
|
|
612
|
+
if (!select) return;
|
|
613
|
+
|
|
614
|
+
// Store current selection
|
|
615
|
+
const currentValue = select.value;
|
|
616
|
+
|
|
617
|
+
// Clear existing options except the first "All" option
|
|
618
|
+
select.innerHTML = `<option value="">${allOption}</option>`;
|
|
619
|
+
|
|
620
|
+
// Add unique values, sorted alphabetically
|
|
621
|
+
const sortedValues = [...values].sort();
|
|
622
|
+
sortedValues.forEach(value => {
|
|
623
|
+
const option = document.createElement('option');
|
|
624
|
+
option.value = value;
|
|
625
|
+
option.textContent = value;
|
|
626
|
+
select.appendChild(option);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Restore selection if it still exists
|
|
630
|
+
if (currentValue && sortedValues.includes(currentValue)) {
|
|
631
|
+
select.value = currentValue;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Setup tab navigation
|
|
637
|
+
*/
|
|
638
|
+
setupTabNavigation() {
|
|
639
|
+
const tabButtons = document.querySelectorAll('.tab-button');
|
|
640
|
+
tabButtons.forEach(button => {
|
|
641
|
+
button.addEventListener('click', () => {
|
|
642
|
+
const tabName = this.getTabNameFromButton(button);
|
|
643
|
+
this.switchTab(tabName);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Setup unified keyboard navigation for all tabs
|
|
650
|
+
*/
|
|
651
|
+
setupUnifiedKeyboardNavigation() {
|
|
652
|
+
document.addEventListener('keydown', (e) => {
|
|
653
|
+
// Only handle navigation if no input is focused
|
|
654
|
+
if (document.activeElement &&
|
|
655
|
+
(document.activeElement.tagName === 'INPUT' ||
|
|
656
|
+
document.activeElement.tagName === 'TEXTAREA' ||
|
|
657
|
+
document.activeElement.tagName === 'SELECT')) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
662
|
+
e.preventDefault();
|
|
663
|
+
this.handleUnifiedArrowNavigation(e.key === 'ArrowDown' ? 1 : -1);
|
|
664
|
+
} else if (e.key === 'Enter') {
|
|
665
|
+
e.preventDefault();
|
|
666
|
+
this.handleUnifiedEnterKey();
|
|
667
|
+
} else if (e.key === 'Escape') {
|
|
668
|
+
e.preventDefault();
|
|
669
|
+
this.clearUnifiedSelection();
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get tab name from button text
|
|
676
|
+
*/
|
|
677
|
+
getTabNameFromButton(button) {
|
|
678
|
+
const text = button.textContent.toLowerCase();
|
|
679
|
+
if (text.includes('events')) return 'events';
|
|
680
|
+
if (text.includes('agents')) return 'agents';
|
|
681
|
+
if (text.includes('tools')) return 'tools';
|
|
682
|
+
if (text.includes('files')) return 'files';
|
|
683
|
+
return 'events';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Initialize from URL parameters
|
|
688
|
+
*/
|
|
689
|
+
initializeFromURL() {
|
|
690
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
691
|
+
const defaultPort = urlParams.get('port') || '8765';
|
|
692
|
+
const autoConnect = urlParams.get('autoconnect');
|
|
693
|
+
|
|
694
|
+
const portInput = document.getElementById('port-input');
|
|
695
|
+
if (portInput) {
|
|
696
|
+
portInput.value = defaultPort;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Auto-connect logic:
|
|
700
|
+
// - Connect if autoconnect=true (explicit)
|
|
701
|
+
// - Connect by default unless autoconnect=false (explicit)
|
|
702
|
+
// - Don't connect if already connected or connecting
|
|
703
|
+
const shouldAutoConnect = autoConnect === 'true' || (autoConnect !== 'false' && autoConnect === null);
|
|
704
|
+
|
|
705
|
+
if (shouldAutoConnect && !this.socketClient.isConnected && !this.socketClient.isConnecting) {
|
|
706
|
+
console.log('Auto-connecting to Socket.IO server on page load...');
|
|
707
|
+
this.socketClient.connect(defaultPort);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Switch to a different tab
|
|
713
|
+
*/
|
|
714
|
+
switchTab(tabName) {
|
|
715
|
+
console.log(`[DEBUG] switchTab called with tabName: ${tabName}`);
|
|
716
|
+
this.currentTab = tabName;
|
|
717
|
+
|
|
718
|
+
// Update tab buttons
|
|
719
|
+
document.querySelectorAll('.tab-button').forEach(btn => {
|
|
720
|
+
btn.classList.remove('active');
|
|
721
|
+
if (this.getTabNameFromButton(btn) === tabName) {
|
|
722
|
+
btn.classList.add('active');
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Update tab content
|
|
727
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
728
|
+
content.classList.remove('active');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const activeTab = document.getElementById(`${tabName}-tab`);
|
|
732
|
+
console.log(`[DEBUG] Active tab element found:`, activeTab);
|
|
733
|
+
if (activeTab) {
|
|
734
|
+
activeTab.classList.add('active');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Render content for the active tab
|
|
738
|
+
console.log(`[DEBUG] About to render current tab: ${tabName}`);
|
|
739
|
+
this.renderCurrentTab();
|
|
740
|
+
|
|
741
|
+
// Auto-scroll to bottom after tab content is rendered
|
|
742
|
+
const listId = `${tabName}-list`;
|
|
743
|
+
console.log(`[DEBUG] About to scroll list with ID: ${listId}`);
|
|
744
|
+
this.scrollListToBottom(listId);
|
|
745
|
+
|
|
746
|
+
// Fallback: Try again with longer delay in case content takes time to render
|
|
747
|
+
setTimeout(() => {
|
|
748
|
+
console.log(`[DEBUG] Fallback scroll attempt for ${listId}`);
|
|
749
|
+
this.scrollListToBottom(listId);
|
|
750
|
+
}, 200);
|
|
751
|
+
|
|
752
|
+
console.log(`[DEBUG] Switched to ${tabName} tab`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Handle unified arrow key navigation across all tabs
|
|
757
|
+
* @param {number} direction - Direction: 1 for down, -1 for up
|
|
758
|
+
*/
|
|
759
|
+
handleUnifiedArrowNavigation(direction) {
|
|
760
|
+
const tabNav = this.tabNavigation[this.currentTab];
|
|
761
|
+
if (!tabNav) return;
|
|
762
|
+
|
|
763
|
+
// Update items list for current tab
|
|
764
|
+
this.updateTabNavigationItems();
|
|
765
|
+
|
|
766
|
+
if (tabNav.items.length === 0) return;
|
|
767
|
+
|
|
768
|
+
// Calculate new index
|
|
769
|
+
let newIndex = tabNav.selectedIndex + direction;
|
|
770
|
+
|
|
771
|
+
// Wrap around
|
|
772
|
+
if (newIndex >= tabNav.items.length) {
|
|
773
|
+
newIndex = 0;
|
|
774
|
+
} else if (newIndex < 0) {
|
|
775
|
+
newIndex = tabNav.items.length - 1;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Update selection
|
|
779
|
+
this.selectCardByIndex(this.currentTab, newIndex);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Handle unified Enter key across all tabs
|
|
784
|
+
*/
|
|
785
|
+
handleUnifiedEnterKey() {
|
|
786
|
+
const tabNav = this.tabNavigation[this.currentTab];
|
|
787
|
+
if (!tabNav || tabNav.selectedIndex === -1) return;
|
|
788
|
+
|
|
789
|
+
// Trigger click on the selected item
|
|
790
|
+
const selectedElement = tabNav.items[tabNav.selectedIndex];
|
|
791
|
+
if (selectedElement && selectedElement.onclick) {
|
|
792
|
+
selectedElement.click();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Clear unified selection across all tabs
|
|
798
|
+
*/
|
|
799
|
+
clearUnifiedSelection() {
|
|
800
|
+
// Clear all tab navigation states
|
|
801
|
+
Object.keys(this.tabNavigation).forEach(tabName => {
|
|
802
|
+
this.tabNavigation[tabName].selectedIndex = -1;
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Clear card selection
|
|
806
|
+
this.clearCardSelection();
|
|
807
|
+
|
|
808
|
+
// Clear EventViewer selection if it exists
|
|
809
|
+
if (this.eventViewer) {
|
|
810
|
+
this.eventViewer.clearSelection();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Clear module viewer
|
|
814
|
+
if (this.moduleViewer) {
|
|
815
|
+
this.moduleViewer.clear();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Update items list for current tab navigation
|
|
821
|
+
*/
|
|
822
|
+
updateTabNavigationItems() {
|
|
823
|
+
const tabNav = this.tabNavigation[this.currentTab];
|
|
824
|
+
if (!tabNav) return;
|
|
825
|
+
|
|
826
|
+
let containerSelector;
|
|
827
|
+
switch (this.currentTab) {
|
|
828
|
+
case 'events':
|
|
829
|
+
containerSelector = '#events-list .event-item';
|
|
830
|
+
break;
|
|
831
|
+
case 'agents':
|
|
832
|
+
containerSelector = '#agents-list .event-item';
|
|
833
|
+
break;
|
|
834
|
+
case 'tools':
|
|
835
|
+
containerSelector = '#tools-list .event-item';
|
|
836
|
+
break;
|
|
837
|
+
case 'files':
|
|
838
|
+
containerSelector = '#files-list .file-item, #files-list .event-item';
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (containerSelector) {
|
|
843
|
+
tabNav.items = Array.from(document.querySelectorAll(containerSelector));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Select a card by index in the specified tab
|
|
849
|
+
* @param {string} tabName - Tab name
|
|
850
|
+
* @param {number} index - Index of item to select
|
|
851
|
+
*/
|
|
852
|
+
selectCardByIndex(tabName, index) {
|
|
853
|
+
const tabNav = this.tabNavigation[tabName];
|
|
854
|
+
if (!tabNav || index < 0 || index >= tabNav.items.length) return;
|
|
855
|
+
|
|
856
|
+
// Update navigation state
|
|
857
|
+
tabNav.selectedIndex = index;
|
|
858
|
+
|
|
859
|
+
// Update visual selection
|
|
860
|
+
this.updateUnifiedSelectionUI();
|
|
861
|
+
|
|
862
|
+
// Scroll selected item into view
|
|
863
|
+
const selectedElement = tabNav.items[index];
|
|
864
|
+
if (selectedElement) {
|
|
865
|
+
selectedElement.scrollIntoView({
|
|
866
|
+
behavior: 'smooth',
|
|
867
|
+
block: 'nearest'
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Update details view based on tab
|
|
872
|
+
this.showCardDetails(tabName, index);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Update visual selection UI for current tab
|
|
877
|
+
*/
|
|
878
|
+
updateUnifiedSelectionUI() {
|
|
879
|
+
const tabNav = this.tabNavigation[this.currentTab];
|
|
880
|
+
if (!tabNav) return;
|
|
881
|
+
|
|
882
|
+
// Clear all selections in current tab
|
|
883
|
+
tabNav.items.forEach((item, index) => {
|
|
884
|
+
item.classList.toggle('selected', index === tabNav.selectedIndex);
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Show card details based on tab and index
|
|
890
|
+
* @param {string} tabName - Tab name
|
|
891
|
+
* @param {number} index - Index of item
|
|
892
|
+
*/
|
|
893
|
+
showCardDetails(tabName, index) {
|
|
894
|
+
switch (tabName) {
|
|
895
|
+
case 'events':
|
|
896
|
+
// Use EventViewer's existing method
|
|
897
|
+
if (this.eventViewer) {
|
|
898
|
+
this.eventViewer.showEventDetails(index);
|
|
899
|
+
}
|
|
900
|
+
break;
|
|
901
|
+
case 'agents':
|
|
902
|
+
this.showAgentDetailsByIndex(index);
|
|
903
|
+
break;
|
|
904
|
+
case 'tools':
|
|
905
|
+
this.showToolDetailsByIndex(index);
|
|
906
|
+
break;
|
|
907
|
+
case 'files':
|
|
908
|
+
this.showFileDetailsByIndex(index);
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Select a card and update the UI
|
|
915
|
+
* @param {string} tabName - Tab name (events, agents, tools, files)
|
|
916
|
+
* @param {number} index - Index of the item in that tab
|
|
917
|
+
* @param {string} type - Type of item (event, agent, tool, file)
|
|
918
|
+
* @param {Object} data - The data object for the selected item
|
|
919
|
+
*/
|
|
920
|
+
selectCard(tabName, index, type, data) {
|
|
921
|
+
// Clear previous selection
|
|
922
|
+
this.clearCardSelection();
|
|
923
|
+
|
|
924
|
+
// Update selection state
|
|
925
|
+
this.selectedCard = {
|
|
926
|
+
tab: tabName,
|
|
927
|
+
index: index,
|
|
928
|
+
type: type,
|
|
929
|
+
data: data
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// Update visual selection in the current tab
|
|
933
|
+
this.updateCardSelectionUI();
|
|
934
|
+
|
|
935
|
+
console.log('Card selected:', this.selectedCard);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Clear card selection
|
|
940
|
+
*/
|
|
941
|
+
clearCardSelection() {
|
|
942
|
+
// Clear visual selection from all tabs
|
|
943
|
+
document.querySelectorAll('.event-item.selected, .file-item.selected').forEach(el => {
|
|
944
|
+
el.classList.remove('selected');
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// Reset selection state
|
|
948
|
+
this.selectedCard = {
|
|
949
|
+
tab: null,
|
|
950
|
+
index: null,
|
|
951
|
+
type: null,
|
|
952
|
+
data: null
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Update visual selection in the current tab
|
|
958
|
+
*/
|
|
959
|
+
updateCardSelectionUI() {
|
|
960
|
+
if (!this.selectedCard.tab || this.selectedCard.index === null) return;
|
|
961
|
+
|
|
962
|
+
// Get the list container for the selected tab
|
|
963
|
+
let listContainer;
|
|
964
|
+
switch (this.selectedCard.tab) {
|
|
965
|
+
case 'events':
|
|
966
|
+
listContainer = document.getElementById('events-list');
|
|
967
|
+
break;
|
|
968
|
+
case 'agents':
|
|
969
|
+
listContainer = document.getElementById('agents-list');
|
|
970
|
+
break;
|
|
971
|
+
case 'tools':
|
|
972
|
+
listContainer = document.getElementById('tools-list');
|
|
973
|
+
break;
|
|
974
|
+
case 'files':
|
|
975
|
+
listContainer = document.getElementById('files-list');
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (listContainer) {
|
|
980
|
+
const cards = listContainer.querySelectorAll('.event-item, .file-item');
|
|
981
|
+
cards.forEach((card, index) => {
|
|
982
|
+
card.classList.toggle('selected', index === this.selectedCard.index);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Show agent details by index in the current filtered list
|
|
989
|
+
* @param {number} index - Index in the filtered agents list
|
|
990
|
+
*/
|
|
991
|
+
showAgentDetailsByIndex(index) {
|
|
992
|
+
// Use stored filtered agent events instead of recalculating
|
|
993
|
+
const agentEvents = this.agentEvents;
|
|
994
|
+
|
|
995
|
+
if (index < 0 || index >= agentEvents.length) {
|
|
996
|
+
console.warn('Invalid agent index:', index, 'Available agents:', agentEvents.length);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const event = agentEvents[index];
|
|
1001
|
+
const eventIndex = this.eventViewer.events.indexOf(event);
|
|
1002
|
+
this.showAgentDetails(index, eventIndex);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Show tool details by index in the current filtered list
|
|
1007
|
+
* @param {number} index - Index in the filtered tool calls list
|
|
1008
|
+
*/
|
|
1009
|
+
showToolDetailsByIndex(index) {
|
|
1010
|
+
// Get filtered tool calls array (same as renderTools)
|
|
1011
|
+
let toolCallsArray = Array.from(this.toolCalls.entries())
|
|
1012
|
+
.filter(([key, toolCall]) => {
|
|
1013
|
+
return toolCall.tool_name && (toolCall.pre_event || toolCall.post_event);
|
|
1014
|
+
})
|
|
1015
|
+
.sort((a, b) => {
|
|
1016
|
+
const timeA = new Date(a[1].timestamp || 0);
|
|
1017
|
+
const timeB = new Date(b[1].timestamp || 0);
|
|
1018
|
+
return timeA - timeB;
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
// Apply tab-specific filters
|
|
1022
|
+
toolCallsArray = this.applyToolCallFilters(toolCallsArray);
|
|
1023
|
+
|
|
1024
|
+
if (index < 0 || index >= toolCallsArray.length) return;
|
|
1025
|
+
|
|
1026
|
+
const [toolCallKey] = toolCallsArray[index];
|
|
1027
|
+
this.showToolCallDetails(toolCallKey);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Show file details by index in the current filtered list
|
|
1032
|
+
* @param {number} index - Index in the filtered files list
|
|
1033
|
+
*/
|
|
1034
|
+
showFileDetailsByIndex(index) {
|
|
1035
|
+
let filesArray = Array.from(this.fileOperations.entries())
|
|
1036
|
+
.filter(([filePath, fileData]) => {
|
|
1037
|
+
return fileData.operations && fileData.operations.length > 0;
|
|
1038
|
+
})
|
|
1039
|
+
.sort((a, b) => {
|
|
1040
|
+
const timeA = a[1].lastOperation ? new Date(a[1].lastOperation) : new Date(0);
|
|
1041
|
+
const timeB = b[1].lastOperation ? new Date(b[1].lastOperation) : new Date(0);
|
|
1042
|
+
return timeA - timeB;
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
filesArray = this.applyFilesFilters(filesArray);
|
|
1046
|
+
|
|
1047
|
+
if (index < 0 || index >= filesArray.length) return;
|
|
1048
|
+
|
|
1049
|
+
const [filePath] = filesArray[index];
|
|
1050
|
+
this.showFileDetails(filePath);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Render content for the current tab
|
|
1055
|
+
*/
|
|
1056
|
+
renderCurrentTab() {
|
|
1057
|
+
switch (this.currentTab) {
|
|
1058
|
+
case 'events':
|
|
1059
|
+
// Events are automatically rendered by EventViewer
|
|
1060
|
+
break;
|
|
1061
|
+
case 'agents':
|
|
1062
|
+
this.renderAgents();
|
|
1063
|
+
break;
|
|
1064
|
+
case 'tools':
|
|
1065
|
+
this.renderTools();
|
|
1066
|
+
break;
|
|
1067
|
+
case 'files':
|
|
1068
|
+
this.renderFiles();
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Update navigation items for the current tab after rendering
|
|
1073
|
+
this.updateTabNavigationItems();
|
|
1074
|
+
|
|
1075
|
+
// Restore selection after rendering if it's in the current tab
|
|
1076
|
+
if (this.selectedCard.tab === this.currentTab) {
|
|
1077
|
+
this.updateCardSelectionUI();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Update unified selection UI to maintain consistency
|
|
1081
|
+
this.updateUnifiedSelectionUI();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Render agents tab
|
|
1086
|
+
*/
|
|
1087
|
+
renderAgents() {
|
|
1088
|
+
console.log('=== RENDERAGENTS DEBUG START ===');
|
|
1089
|
+
console.log('1. Function called, checking agentsList element...');
|
|
1090
|
+
|
|
1091
|
+
const agentsList = document.getElementById('agents-list');
|
|
1092
|
+
if (!agentsList) {
|
|
1093
|
+
console.error('agentsList element not found!');
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
console.log('2. agentsList element found:', agentsList);
|
|
1097
|
+
|
|
1098
|
+
const events = this.getFilteredEventsForTab('agents');
|
|
1099
|
+
console.log('3. Total events from getFilteredEventsForTab:', events.length);
|
|
1100
|
+
|
|
1101
|
+
// Enhanced debugging: log first few events to understand structure
|
|
1102
|
+
if (events.length > 0) {
|
|
1103
|
+
console.log('Agent tab - sample events for analysis:');
|
|
1104
|
+
events.slice(0, 3).forEach((event, i) => {
|
|
1105
|
+
console.log(` Event ${i}:`, {
|
|
1106
|
+
type: event.type,
|
|
1107
|
+
subtype: event.subtype,
|
|
1108
|
+
tool_name: event.tool_name,
|
|
1109
|
+
agent_type: event.agent_type,
|
|
1110
|
+
subagent_type: event.subagent_type,
|
|
1111
|
+
tool_parameters: event.tool_parameters,
|
|
1112
|
+
delegation_details: event.delegation_details,
|
|
1113
|
+
data: event.data ? {
|
|
1114
|
+
agent_type: event.data.agent_type,
|
|
1115
|
+
subagent_type: event.data.subagent_type,
|
|
1116
|
+
event_type: event.data.event_type,
|
|
1117
|
+
tool_name: event.data.tool_name,
|
|
1118
|
+
tool_parameters: event.data.tool_parameters,
|
|
1119
|
+
delegation_details: event.data.delegation_details
|
|
1120
|
+
} : 'no data field'
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// Count events by type and tool_name for debugging
|
|
1125
|
+
const eventCounts = {};
|
|
1126
|
+
const toolCounts = {};
|
|
1127
|
+
const agentCounts = {};
|
|
1128
|
+
events.forEach(event => {
|
|
1129
|
+
const key = `${event.type}.${event.subtype || 'none'}`;
|
|
1130
|
+
eventCounts[key] = (eventCounts[key] || 0) + 1;
|
|
1131
|
+
|
|
1132
|
+
if (event.tool_name) {
|
|
1133
|
+
toolCounts[event.tool_name] = (toolCounts[event.tool_name] || 0) + 1;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Count agent types from multiple sources
|
|
1137
|
+
const agentTypes = [];
|
|
1138
|
+
if (event.agent_type) agentTypes.push(`direct:${event.agent_type}`);
|
|
1139
|
+
if (event.subagent_type) agentTypes.push(`sub:${event.subagent_type}`);
|
|
1140
|
+
if (event.tool_parameters?.subagent_type) agentTypes.push(`tool_param:${event.tool_parameters.subagent_type}`);
|
|
1141
|
+
if (event.data?.agent_type) agentTypes.push(`data:${event.data.agent_type}`);
|
|
1142
|
+
if (event.data?.subagent_type) agentTypes.push(`data_sub:${event.data.subagent_type}`);
|
|
1143
|
+
if (event.data?.delegation_details?.agent_type) agentTypes.push(`delegation:${event.data.delegation_details.agent_type}`);
|
|
1144
|
+
|
|
1145
|
+
agentTypes.forEach(agentType => {
|
|
1146
|
+
agentCounts[agentType] = (agentCounts[agentType] || 0) + 1;
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
console.log('Agent tab - event type breakdown:', eventCounts);
|
|
1150
|
+
console.log('Agent tab - tool breakdown:', toolCounts);
|
|
1151
|
+
console.log('Agent tab - agent type breakdown:', agentCounts);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Use agent inference to filter events instead of hardcoded logic
|
|
1155
|
+
let agentEvents = events
|
|
1156
|
+
.map((event, index) => ({ event, index, inference: this.inferAgentFromEvent(event) }))
|
|
1157
|
+
.filter(({ event, index, inference }) => {
|
|
1158
|
+
// Show events that have meaningful agent context
|
|
1159
|
+
if (!inference) return false;
|
|
1160
|
+
|
|
1161
|
+
// Include events that are definitely agent-related
|
|
1162
|
+
const isAgentRelated = inference.type === 'subagent' ||
|
|
1163
|
+
(inference.type === 'main_agent' && inference.confidence !== 'default') ||
|
|
1164
|
+
event.tool_name === 'Task' ||
|
|
1165
|
+
event.hook_event_name === 'SubagentStop' ||
|
|
1166
|
+
event.subtype === 'subagent_stop';
|
|
1167
|
+
|
|
1168
|
+
// Debug first few events
|
|
1169
|
+
if (index < 5) {
|
|
1170
|
+
console.log(`Agent filter [${index}] - ${isAgentRelated ? 'MATCHED' : 'SKIPPED'}:`, {
|
|
1171
|
+
type: event.type,
|
|
1172
|
+
subtype: event.subtype,
|
|
1173
|
+
tool_name: event.tool_name,
|
|
1174
|
+
inference: inference,
|
|
1175
|
+
isAgentRelated
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return isAgentRelated;
|
|
1180
|
+
})
|
|
1181
|
+
.map(({ event, inference }) => ({ event, inference }))
|
|
1182
|
+
.sort((a, b) => new Date(a.event.timestamp) - new Date(b.event.timestamp));
|
|
1183
|
+
|
|
1184
|
+
// Extract unique agent types from the data for filter dropdown
|
|
1185
|
+
const uniqueAgentTypes = new Set();
|
|
1186
|
+
agentEvents.forEach(({ event, inference }) => {
|
|
1187
|
+
if (inference && inference.agentName && inference.agentName !== 'Unknown') {
|
|
1188
|
+
uniqueAgentTypes.add(inference.agentName);
|
|
1189
|
+
}
|
|
1190
|
+
// Also check for agent_type in the event data
|
|
1191
|
+
if (event.agent_type && event.agent_type !== 'unknown' && event.agent_type !== 'main') {
|
|
1192
|
+
uniqueAgentTypes.add(event.agent_type);
|
|
1193
|
+
}
|
|
1194
|
+
if (event.subagent_type) {
|
|
1195
|
+
uniqueAgentTypes.add(event.subagent_type);
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Populate the agents filter dropdown
|
|
1200
|
+
this.populateFilterDropdown('agents-type-filter', Array.from(uniqueAgentTypes), 'All Agents');
|
|
1201
|
+
|
|
1202
|
+
// Apply tab-specific filters to the agentEvents array while preserving inference data
|
|
1203
|
+
let filteredAgentEvents = agentEvents.filter(({ event }) => {
|
|
1204
|
+
// Create a temporary array with just events for the existing filter function
|
|
1205
|
+
const singleEventArray = [event];
|
|
1206
|
+
const filteredSingleEvent = this.applyAgentsFilters(singleEventArray);
|
|
1207
|
+
return filteredSingleEvent.length > 0;
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// Store filtered agent events with inference data in class property
|
|
1211
|
+
this.agentEventsWithInference = filteredAgentEvents;
|
|
1212
|
+
|
|
1213
|
+
// Also store just the events for backward compatibility
|
|
1214
|
+
this.agentEvents = filteredAgentEvents.map(({ event }) => event);
|
|
1215
|
+
|
|
1216
|
+
console.log('4. Agent tab - filtering summary:', {
|
|
1217
|
+
total_events: events.length,
|
|
1218
|
+
agent_events_found: filteredAgentEvents.length,
|
|
1219
|
+
percentage: filteredAgentEvents.length > 0 ? ((filteredAgentEvents.length / events.length) * 100).toFixed(1) + '%' : '0%'
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
if (filteredAgentEvents.length === 0) {
|
|
1223
|
+
console.log('5. No agent events found, showing empty message');
|
|
1224
|
+
agentsList.innerHTML = '<div class="no-events">No agent events found...</div>';
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
console.log('Rendering', filteredAgentEvents.length, 'agent events');
|
|
1229
|
+
|
|
1230
|
+
const agentsHtml = filteredAgentEvents.map(({ event, inference }, index) => {
|
|
1231
|
+
const timestamp = new Date(event.timestamp).toLocaleTimeString();
|
|
1232
|
+
|
|
1233
|
+
let agentName = inference ? inference.agentName : 'Unknown';
|
|
1234
|
+
let operation = 'operation';
|
|
1235
|
+
let prompt = '';
|
|
1236
|
+
let description = '';
|
|
1237
|
+
let taskPreview = '';
|
|
1238
|
+
let confidence = inference ? inference.confidence : 'unknown';
|
|
1239
|
+
let reason = inference ? inference.reason : 'no inference';
|
|
1240
|
+
|
|
1241
|
+
// Extract Task tool information if present
|
|
1242
|
+
const data = event.data || {};
|
|
1243
|
+
if (event.tool_name === 'Task' || data.tool_name === 'Task') {
|
|
1244
|
+
operation = 'delegation';
|
|
1245
|
+
|
|
1246
|
+
// Try different sources for Task tool data
|
|
1247
|
+
const taskParams = event.tool_parameters || data.tool_parameters || data.delegation_details || {};
|
|
1248
|
+
|
|
1249
|
+
if (taskParams.prompt) {
|
|
1250
|
+
prompt = taskParams.prompt;
|
|
1251
|
+
taskPreview = prompt.length > 200 ? prompt.substring(0, 200) + '...' : prompt;
|
|
1252
|
+
}
|
|
1253
|
+
if (taskParams.description) {
|
|
1254
|
+
description = taskParams.description;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Extract operation from event type/subtype
|
|
1259
|
+
if (event.subtype) {
|
|
1260
|
+
operation = event.subtype.replace(/_/g, ' ');
|
|
1261
|
+
} else {
|
|
1262
|
+
operation = this.extractOperation(event.type) || 'operation';
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Add confidence indicator
|
|
1266
|
+
const confidenceIcon = {
|
|
1267
|
+
'definitive': '🎯',
|
|
1268
|
+
'high': '✅',
|
|
1269
|
+
'medium': '⚠️',
|
|
1270
|
+
'inherited': '📋',
|
|
1271
|
+
'default': '❓',
|
|
1272
|
+
'unknown': '❔'
|
|
1273
|
+
}[confidence] || '❔';
|
|
1274
|
+
|
|
1275
|
+
const onclickString = `dashboard.selectCard('agents', ${index}, 'agent', ${index}); dashboard.showAgentDetailsByIndex(${index});`;
|
|
1276
|
+
|
|
1277
|
+
return `
|
|
1278
|
+
<div class="event-item event-agent" onclick="${onclickString}">
|
|
1279
|
+
<div class="event-header">
|
|
1280
|
+
<span class="event-type">🤖 ${agentName}</span>
|
|
1281
|
+
<span class="confidence-indicator" title="Confidence: ${confidence} (${reason})">${confidenceIcon}</span>
|
|
1282
|
+
<span class="event-timestamp">${timestamp}</span>
|
|
1283
|
+
</div>
|
|
1284
|
+
<div class="event-data">
|
|
1285
|
+
<strong>Operation:</strong> ${operation}
|
|
1286
|
+
<strong>Inference:</strong> ${inference ? inference.type : 'unknown'} (${confidence})
|
|
1287
|
+
${taskPreview ? `<br><strong>Task Preview:</strong> ${taskPreview}` : ''}
|
|
1288
|
+
${description ? `<br><strong>Description:</strong> ${description}` : ''}
|
|
1289
|
+
${event.session_id || data.session_id ? `<br><strong>Session:</strong> ${(event.session_id || data.session_id).substring(0, 8)}...` : ''}
|
|
1290
|
+
</div>
|
|
1291
|
+
</div>
|
|
1292
|
+
`;
|
|
1293
|
+
}).join('');
|
|
1294
|
+
|
|
1295
|
+
console.log('9. Generated HTML length:', agentsHtml.length);
|
|
1296
|
+
console.log('10. Sample HTML (first 500 chars):', agentsHtml.substring(0, 500));
|
|
1297
|
+
|
|
1298
|
+
agentsList.innerHTML = agentsHtml;
|
|
1299
|
+
|
|
1300
|
+
// Check if the HTML was actually set
|
|
1301
|
+
console.log('11. HTML set in DOM, innerHTML length:', agentsList.innerHTML.length);
|
|
1302
|
+
console.log('12. Number of event-agent elements:', agentsList.querySelectorAll('.event-agent').length);
|
|
1303
|
+
|
|
1304
|
+
// Test onclick on first element if exists
|
|
1305
|
+
const firstAgent = agentsList.querySelector('.event-agent');
|
|
1306
|
+
if (firstAgent) {
|
|
1307
|
+
console.log('13. First agent element found:', firstAgent);
|
|
1308
|
+
console.log('14. First agent onclick attribute:', firstAgent.getAttribute('onclick'));
|
|
1309
|
+
|
|
1310
|
+
// Add a test click event listener as well
|
|
1311
|
+
firstAgent.addEventListener('click', function(e) {
|
|
1312
|
+
console.log('15. CLICK EVENT DETECTED on agent element!', e.target);
|
|
1313
|
+
});
|
|
1314
|
+
} else {
|
|
1315
|
+
console.log('13. No .event-agent elements found in DOM after setting innerHTML');
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
console.log('=== RENDERAGENTS DEBUG END ===');
|
|
1319
|
+
this.scrollListToBottom('agents-list');
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Render tools tab - shows paired tool calls instead of individual events
|
|
1324
|
+
*/
|
|
1325
|
+
renderTools() {
|
|
1326
|
+
const toolsList = document.getElementById('tools-list');
|
|
1327
|
+
if (!toolsList) return;
|
|
1328
|
+
|
|
1329
|
+
console.log('Tools tab - total tool calls:', this.toolCalls.size);
|
|
1330
|
+
|
|
1331
|
+
if (this.toolCalls.size === 0) {
|
|
1332
|
+
toolsList.innerHTML = '<div class="no-events">No tool calls found...</div>';
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Convert to array and sort by timestamp
|
|
1337
|
+
let toolCallsArray = Array.from(this.toolCalls.entries())
|
|
1338
|
+
.filter(([key, toolCall]) => {
|
|
1339
|
+
// Ensure we have valid data
|
|
1340
|
+
return toolCall.tool_name && (toolCall.pre_event || toolCall.post_event);
|
|
1341
|
+
})
|
|
1342
|
+
.sort((a, b) => {
|
|
1343
|
+
const timeA = new Date(a[1].timestamp || 0);
|
|
1344
|
+
const timeB = new Date(b[1].timestamp || 0);
|
|
1345
|
+
return timeA - timeB;
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
console.log('Tools tab - after filtering:', toolCallsArray.length, 'tool calls');
|
|
1349
|
+
|
|
1350
|
+
// Extract unique tool names from the data for filter dropdown
|
|
1351
|
+
const uniqueToolNames = new Set();
|
|
1352
|
+
toolCallsArray.forEach(([key, toolCall]) => {
|
|
1353
|
+
if (toolCall.tool_name) {
|
|
1354
|
+
uniqueToolNames.add(toolCall.tool_name);
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
// Populate the tools filter dropdown
|
|
1359
|
+
this.populateFilterDropdown('tools-type-filter', Array.from(uniqueToolNames), 'All Tools');
|
|
1360
|
+
|
|
1361
|
+
// Apply tab-specific filters to tool calls
|
|
1362
|
+
toolCallsArray = this.applyToolCallFilters(toolCallsArray);
|
|
1363
|
+
|
|
1364
|
+
console.log('Tools tab - after search/type filters:', toolCallsArray.length, 'tool calls');
|
|
1365
|
+
|
|
1366
|
+
if (toolCallsArray.length === 0) {
|
|
1367
|
+
toolsList.innerHTML = '<div class="no-events">No tool calls match current filters...</div>';
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const toolsHtml = toolCallsArray.map(([key, toolCall], index) => {
|
|
1372
|
+
const timestamp = new Date(toolCall.timestamp).toLocaleTimeString();
|
|
1373
|
+
const toolName = toolCall.tool_name || 'Unknown Tool';
|
|
1374
|
+
|
|
1375
|
+
// Use inferred agent data instead of hardcoded 'PM'
|
|
1376
|
+
let agentName = 'PM';
|
|
1377
|
+
let confidence = 'default';
|
|
1378
|
+
|
|
1379
|
+
// Try to get inference from pre_event first, then post_event
|
|
1380
|
+
const preEvent = toolCall.pre_event;
|
|
1381
|
+
const postEvent = toolCall.post_event;
|
|
1382
|
+
|
|
1383
|
+
if (preEvent) {
|
|
1384
|
+
const eventIndex = this.eventViewer.events.indexOf(preEvent);
|
|
1385
|
+
const inference = this.getInferredAgent(eventIndex);
|
|
1386
|
+
if (inference) {
|
|
1387
|
+
agentName = inference.agentName;
|
|
1388
|
+
confidence = inference.confidence;
|
|
1389
|
+
}
|
|
1390
|
+
} else if (postEvent) {
|
|
1391
|
+
const eventIndex = this.eventViewer.events.indexOf(postEvent);
|
|
1392
|
+
const inference = this.getInferredAgent(eventIndex);
|
|
1393
|
+
if (inference) {
|
|
1394
|
+
agentName = inference.agentName;
|
|
1395
|
+
confidence = inference.confidence;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Fallback to existing logic if no inference available
|
|
1400
|
+
if (agentName === 'PM' && confidence === 'default') {
|
|
1401
|
+
agentName = toolCall.agent_type || 'PM';
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Extract tool target/parameters from pre_event
|
|
1405
|
+
const target = preEvent ? this.extractToolTarget(toolName, preEvent.tool_parameters, preEvent.tool_parameters) : 'Unknown target';
|
|
1406
|
+
|
|
1407
|
+
// Determine status and duration
|
|
1408
|
+
let statusInfo = '';
|
|
1409
|
+
let statusClass = '';
|
|
1410
|
+
|
|
1411
|
+
if (toolCall.post_event) {
|
|
1412
|
+
// We have completion data
|
|
1413
|
+
const duration = toolCall.duration_ms ? `${toolCall.duration_ms}ms` : 'Unknown duration';
|
|
1414
|
+
const success = toolCall.success !== undefined ? toolCall.success : 'Unknown';
|
|
1415
|
+
|
|
1416
|
+
if (success === true) {
|
|
1417
|
+
statusInfo = `✅ Success (${duration})`;
|
|
1418
|
+
statusClass = 'tool-success';
|
|
1419
|
+
} else if (success === false) {
|
|
1420
|
+
statusInfo = `❌ Failed (${duration})`;
|
|
1421
|
+
statusClass = 'tool-failure';
|
|
1422
|
+
} else {
|
|
1423
|
+
statusInfo = `⏳ Completed (${duration})`;
|
|
1424
|
+
statusClass = 'tool-completed';
|
|
1425
|
+
}
|
|
1426
|
+
} else {
|
|
1427
|
+
// Only pre_event - still running or incomplete
|
|
1428
|
+
statusInfo = '⏳ Running...';
|
|
1429
|
+
statusClass = 'tool-running';
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Add confidence indicator for agent inference
|
|
1433
|
+
const confidenceIcon = {
|
|
1434
|
+
'definitive': '🎯',
|
|
1435
|
+
'high': '✅',
|
|
1436
|
+
'medium': '⚠️',
|
|
1437
|
+
'inherited': '📋',
|
|
1438
|
+
'default': '❓',
|
|
1439
|
+
'unknown': '❔'
|
|
1440
|
+
}[confidence] || '❔';
|
|
1441
|
+
|
|
1442
|
+
return `
|
|
1443
|
+
<div class="event-item event-tool ${statusClass}" onclick="dashboard.selectCard('tools', ${index}, 'toolCall', '${key}'); dashboard.showToolCallDetails('${key}')">
|
|
1444
|
+
<div class="event-header">
|
|
1445
|
+
<span class="event-type">🔧 ${toolName}</span>
|
|
1446
|
+
<span class="event-timestamp">${timestamp}</span>
|
|
1447
|
+
</div>
|
|
1448
|
+
<div class="event-data">
|
|
1449
|
+
<strong>Agent:</strong> ${agentName} (${confidence})<br>
|
|
1450
|
+
<strong>Status:</strong> ${statusInfo}<br>
|
|
1451
|
+
<strong>Target:</strong> ${target}
|
|
1452
|
+
${toolCall.session_id ? `<br><strong>Session:</strong> ${toolCall.session_id.substring(0, 8)}...` : ''}
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>
|
|
1455
|
+
`;
|
|
1456
|
+
}).join('');
|
|
1457
|
+
|
|
1458
|
+
toolsList.innerHTML = toolsHtml;
|
|
1459
|
+
this.scrollListToBottom('tools-list');
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Render files tab with file-centric view
|
|
1464
|
+
*/
|
|
1465
|
+
renderFiles() {
|
|
1466
|
+
const filesList = document.getElementById('files-list');
|
|
1467
|
+
if (!filesList) return;
|
|
1468
|
+
|
|
1469
|
+
console.log('Files tab - file operations:', this.fileOperations.size);
|
|
1470
|
+
console.log('Files tab - operations map:', this.fileOperations);
|
|
1471
|
+
|
|
1472
|
+
if (this.fileOperations.size === 0) {
|
|
1473
|
+
filesList.innerHTML = '<div class="no-events">No file operations found...</div>';
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Convert to array and sort by most recent operations at bottom (chronological order)
|
|
1478
|
+
let filesArray = Array.from(this.fileOperations.entries())
|
|
1479
|
+
.filter(([filePath, fileData]) => {
|
|
1480
|
+
// Ensure we have valid data
|
|
1481
|
+
return fileData.operations && fileData.operations.length > 0;
|
|
1482
|
+
})
|
|
1483
|
+
.sort((a, b) => {
|
|
1484
|
+
const timeA = a[1].lastOperation ? new Date(a[1].lastOperation) : new Date(0);
|
|
1485
|
+
const timeB = b[1].lastOperation ? new Date(b[1].lastOperation) : new Date(0);
|
|
1486
|
+
return timeA - timeB;
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
console.log('Files tab - after filtering:', filesArray.length, 'files');
|
|
1490
|
+
|
|
1491
|
+
// Extract unique operations from the data for filter dropdown
|
|
1492
|
+
const uniqueOperations = new Set();
|
|
1493
|
+
filesArray.forEach(([filePath, fileData]) => {
|
|
1494
|
+
if (fileData.operations && fileData.operations.length > 0) {
|
|
1495
|
+
fileData.operations.forEach(operation => {
|
|
1496
|
+
if (operation.operation) {
|
|
1497
|
+
uniqueOperations.add(operation.operation);
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
// Populate the files filter dropdown
|
|
1504
|
+
this.populateFilterDropdown('files-type-filter', Array.from(uniqueOperations), 'All Operations');
|
|
1505
|
+
|
|
1506
|
+
// Apply tab-specific filters
|
|
1507
|
+
filesArray = this.applyFilesFilters(filesArray);
|
|
1508
|
+
|
|
1509
|
+
console.log('Files tab - after search/type filters:', filesArray.length, 'files');
|
|
1510
|
+
|
|
1511
|
+
if (filesArray.length === 0) {
|
|
1512
|
+
filesList.innerHTML = '<div class="no-events">No files match current filters...</div>';
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const filesHtml = filesArray.map(([filePath, fileData], index) => {
|
|
1517
|
+
if (!fileData.operations || fileData.operations.length === 0) {
|
|
1518
|
+
console.warn('File with no operations:', filePath);
|
|
1519
|
+
return '';
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const icon = this.getFileOperationIcon(fileData.operations);
|
|
1523
|
+
const lastOp = fileData.operations[fileData.operations.length - 1];
|
|
1524
|
+
const timestamp = new Date(lastOp.timestamp).toLocaleTimeString();
|
|
1525
|
+
|
|
1526
|
+
// Get unique operations as text, joined with |
|
|
1527
|
+
const uniqueOperations = [...new Set(fileData.operations.map(op => op.operation))];
|
|
1528
|
+
const operationsText = uniqueOperations.join('|');
|
|
1529
|
+
|
|
1530
|
+
return `
|
|
1531
|
+
<div class="event-item file-item" onclick="dashboard.selectCard('files', ${index}, 'file', '${filePath}'); dashboard.showFileDetails('${filePath}')">
|
|
1532
|
+
<div class="event-header">
|
|
1533
|
+
<span class="event-type">${icon}</span>
|
|
1534
|
+
<span class="file-path">${this.getRelativeFilePath(filePath)}</span>
|
|
1535
|
+
<span class="event-timestamp">${timestamp}</span>
|
|
1536
|
+
</div>
|
|
1537
|
+
<div class="event-data">
|
|
1538
|
+
<strong>Operations:</strong> ${operationsText}<br>
|
|
1539
|
+
<strong>Agent:</strong> ${lastOp.agent} ${lastOp.confidence ? `(${lastOp.confidence})` : ''}
|
|
1540
|
+
</div>
|
|
1541
|
+
</div>
|
|
1542
|
+
`;
|
|
1543
|
+
}).join('');
|
|
1544
|
+
|
|
1545
|
+
filesList.innerHTML = filesHtml;
|
|
1546
|
+
this.scrollListToBottom('files-list');
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Show agent details in module viewer
|
|
1551
|
+
*/
|
|
1552
|
+
showAgentDetails(agentIndex, eventIndex) {
|
|
1553
|
+
console.log('showAgentDetails called with agentIndex:', agentIndex, 'eventIndex:', eventIndex);
|
|
1554
|
+
|
|
1555
|
+
// Use stored filtered agent events with inference data if available
|
|
1556
|
+
const agentEventsWithInference = this.agentEventsWithInference || [];
|
|
1557
|
+
const agentEvents = this.agentEvents;
|
|
1558
|
+
|
|
1559
|
+
let event, inference;
|
|
1560
|
+
|
|
1561
|
+
// Try to get event and inference data together
|
|
1562
|
+
if (agentEventsWithInference[agentIndex]) {
|
|
1563
|
+
event = agentEventsWithInference[agentIndex].event;
|
|
1564
|
+
inference = agentEventsWithInference[agentIndex].inference;
|
|
1565
|
+
} else if (agentEvents[agentIndex]) {
|
|
1566
|
+
// Fallback to just event data
|
|
1567
|
+
event = agentEvents[agentIndex];
|
|
1568
|
+
inference = null;
|
|
1569
|
+
} else {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Extract agent information using inference data first, then fallback to event data
|
|
1574
|
+
let agentName = 'Unknown Agent';
|
|
1575
|
+
let prompt = '';
|
|
1576
|
+
let description = '';
|
|
1577
|
+
let fullPrompt = '';
|
|
1578
|
+
|
|
1579
|
+
// Use inference data for agent name if available
|
|
1580
|
+
if (inference && inference.agentName && inference.agentName !== 'Unknown') {
|
|
1581
|
+
agentName = inference.agentName;
|
|
1582
|
+
} else if (event.tool_name === 'Task' && event.tool_parameters?.subagent_type) {
|
|
1583
|
+
agentName = event.tool_parameters.subagent_type;
|
|
1584
|
+
} else if (event.subagent_type) {
|
|
1585
|
+
agentName = event.subagent_type;
|
|
1586
|
+
} else if (event.agent_type && event.agent_type !== 'unknown') {
|
|
1587
|
+
agentName = event.agent_type;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Extract task information
|
|
1591
|
+
if (event.tool_name === 'Task' && event.tool_parameters) {
|
|
1592
|
+
prompt = event.tool_parameters.prompt || '';
|
|
1593
|
+
description = event.tool_parameters.description || '';
|
|
1594
|
+
fullPrompt = prompt;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// Add debug logging
|
|
1598
|
+
console.log('showAgentDetails called with:', {
|
|
1599
|
+
agentIndex,
|
|
1600
|
+
eventIndex,
|
|
1601
|
+
event,
|
|
1602
|
+
inference,
|
|
1603
|
+
agentName: agentName,
|
|
1604
|
+
hasInferenceData: !!inference
|
|
1605
|
+
});
|
|
1606
|
+
console.log('moduleViewer available:', !!this.moduleViewer);
|
|
1607
|
+
|
|
1608
|
+
// Create enhanced event object with inference data for module viewer
|
|
1609
|
+
const enhancedEvent = {
|
|
1610
|
+
...event,
|
|
1611
|
+
_inference: inference, // Add inference data as a private property
|
|
1612
|
+
_agentName: agentName // Add resolved agent name
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
// Use the module viewer's ingest method to properly display the agent event
|
|
1616
|
+
if (this.moduleViewer) {
|
|
1617
|
+
console.log('Calling moduleViewer.ingest with enhanced event:', enhancedEvent);
|
|
1618
|
+
this.moduleViewer.ingest(enhancedEvent);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Also show the event details in EventViewer
|
|
1622
|
+
if (eventIndex >= 0) {
|
|
1623
|
+
this.eventViewer.showEventDetails(eventIndex);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Toggle prompt expansion
|
|
1629
|
+
*/
|
|
1630
|
+
togglePromptExpansion(button) {
|
|
1631
|
+
const promptDiv = button.parentElement.previousElementSibling;
|
|
1632
|
+
const isExpanded = promptDiv.style.maxHeight !== '300px';
|
|
1633
|
+
|
|
1634
|
+
if (isExpanded) {
|
|
1635
|
+
promptDiv.style.maxHeight = '300px';
|
|
1636
|
+
button.textContent = 'Show More';
|
|
1637
|
+
} else {
|
|
1638
|
+
promptDiv.style.maxHeight = 'none';
|
|
1639
|
+
button.textContent = 'Show Less';
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Show tool details in module viewer
|
|
1645
|
+
*/
|
|
1646
|
+
showToolDetails(toolIndex, eventIndex) {
|
|
1647
|
+
// Get the tool event
|
|
1648
|
+
const events = this.getFilteredEventsForTab('tools');
|
|
1649
|
+
const toolEvents = this.applyToolsFilters(events.filter(event => {
|
|
1650
|
+
const type = event.type || '';
|
|
1651
|
+
const subtype = event.subtype || '';
|
|
1652
|
+
|
|
1653
|
+
const isHookToolEvent = type === 'hook' && (
|
|
1654
|
+
subtype.includes('tool') ||
|
|
1655
|
+
subtype.includes('pre_') ||
|
|
1656
|
+
subtype.includes('post_')
|
|
1657
|
+
);
|
|
1658
|
+
const hasToolName = event.tool_name;
|
|
1659
|
+
const hasToolsArray = event.tools && Array.isArray(event.tools);
|
|
1660
|
+
const isLegacyHookEvent = type.startsWith('hook.') && (
|
|
1661
|
+
type.includes('tool') ||
|
|
1662
|
+
type.includes('pre') ||
|
|
1663
|
+
type.includes('post')
|
|
1664
|
+
);
|
|
1665
|
+
|
|
1666
|
+
return isHookToolEvent || hasToolName || hasToolsArray || isLegacyHookEvent;
|
|
1667
|
+
}));
|
|
1668
|
+
|
|
1669
|
+
const event = toolEvents[toolIndex];
|
|
1670
|
+
if (!event) return;
|
|
1671
|
+
|
|
1672
|
+
// Extract tool information
|
|
1673
|
+
let toolName = event.tool_name || 'Unknown Tool';
|
|
1674
|
+
if (event.tools && Array.isArray(event.tools) && event.tools.length > 0) {
|
|
1675
|
+
toolName = event.tools[0];
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
let agentName = 'PM';
|
|
1679
|
+
if (event.subagent_type) {
|
|
1680
|
+
agentName = event.subagent_type;
|
|
1681
|
+
} else if (event.agent_type && event.agent_type !== 'main' && event.agent_type !== 'unknown') {
|
|
1682
|
+
agentName = event.agent_type;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
const target = this.extractToolTarget(toolName, event.tool_parameters, event.tool_parameters);
|
|
1686
|
+
|
|
1687
|
+
let operation = 'execution';
|
|
1688
|
+
if (event.subtype) {
|
|
1689
|
+
if (event.subtype.includes('pre_')) {
|
|
1690
|
+
operation = 'pre-execution';
|
|
1691
|
+
} else if (event.subtype.includes('post_')) {
|
|
1692
|
+
operation = 'post-execution';
|
|
1693
|
+
} else {
|
|
1694
|
+
operation = event.subtype.replace(/_/g, ' ');
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const content = `
|
|
1699
|
+
<div class="structured-view-section">
|
|
1700
|
+
<div class="structured-view-header">
|
|
1701
|
+
<h4>🔧 Tool Details</h4>
|
|
1702
|
+
</div>
|
|
1703
|
+
<div class="tool-details">
|
|
1704
|
+
<div class="tool-info">
|
|
1705
|
+
<div class="structured-field">
|
|
1706
|
+
<strong>Tool Name:</strong> ${toolName}
|
|
1707
|
+
</div>
|
|
1708
|
+
<div class="structured-field">
|
|
1709
|
+
<strong>Agent:</strong> ${agentName}
|
|
1710
|
+
</div>
|
|
1711
|
+
<div class="structured-field">
|
|
1712
|
+
<strong>Operation:</strong> ${operation}
|
|
1713
|
+
</div>
|
|
1714
|
+
<div class="structured-field">
|
|
1715
|
+
<strong>Target:</strong> ${target}
|
|
1716
|
+
</div>
|
|
1717
|
+
<div class="structured-field">
|
|
1718
|
+
<strong>Timestamp:</strong> ${new Date(event.timestamp).toLocaleString()}
|
|
1719
|
+
</div>
|
|
1720
|
+
<div class="structured-field">
|
|
1721
|
+
<strong>Event Type:</strong> ${event.type}.${event.subtype || 'default'}
|
|
1722
|
+
</div>
|
|
1723
|
+
${event.session_id ? `
|
|
1724
|
+
<div class="structured-field">
|
|
1725
|
+
<strong>Session ID:</strong> ${event.session_id}
|
|
1726
|
+
</div>
|
|
1727
|
+
` : ''}
|
|
1728
|
+
</div>
|
|
1729
|
+
|
|
1730
|
+
${event.tool_parameters ? `
|
|
1731
|
+
<div class="parameters-section">
|
|
1732
|
+
<div class="structured-view-header">
|
|
1733
|
+
<h4>⚙️ Parameters</h4>
|
|
1734
|
+
</div>
|
|
1735
|
+
<div class="structured-data">
|
|
1736
|
+
<pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px; line-height: 1.4;">${JSON.stringify(event.tool_parameters, null, 2)}</pre>
|
|
1737
|
+
</div>
|
|
1738
|
+
</div>
|
|
1739
|
+
` : ''}
|
|
1740
|
+
</div>
|
|
1741
|
+
</div>
|
|
1742
|
+
`;
|
|
1743
|
+
|
|
1744
|
+
// Use the new dual-pane approach for tools
|
|
1745
|
+
if (this.moduleViewer.dataContainer) {
|
|
1746
|
+
this.moduleViewer.dataContainer.innerHTML = content;
|
|
1747
|
+
}
|
|
1748
|
+
if (this.moduleViewer.jsonContainer && event) {
|
|
1749
|
+
this.moduleViewer.jsonContainer.innerHTML = `<pre>${JSON.stringify(event, null, 2)}</pre>`;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Also show the event details in EventViewer
|
|
1753
|
+
if (eventIndex >= 0) {
|
|
1754
|
+
this.eventViewer.showEventDetails(eventIndex);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Show tool call details in module viewer with combined pre/post data
|
|
1760
|
+
*/
|
|
1761
|
+
showToolCallDetails(toolCallKey) {
|
|
1762
|
+
const toolCall = this.toolCalls.get(toolCallKey);
|
|
1763
|
+
if (!toolCall) return;
|
|
1764
|
+
|
|
1765
|
+
const toolName = toolCall.tool_name || 'Unknown Tool';
|
|
1766
|
+
const agentName = toolCall.agent_type || 'PM';
|
|
1767
|
+
const timestamp = new Date(toolCall.timestamp).toLocaleString();
|
|
1768
|
+
|
|
1769
|
+
// Extract information from pre and post events
|
|
1770
|
+
const preEvent = toolCall.pre_event;
|
|
1771
|
+
const postEvent = toolCall.post_event;
|
|
1772
|
+
|
|
1773
|
+
// Get parameters from pre-event
|
|
1774
|
+
const parameters = preEvent?.tool_parameters || {};
|
|
1775
|
+
const target = preEvent ? this.extractToolTarget(toolName, parameters, parameters) : 'Unknown target';
|
|
1776
|
+
|
|
1777
|
+
// Get execution results from post-event
|
|
1778
|
+
const duration = toolCall.duration_ms ? `${toolCall.duration_ms}ms` : '-';
|
|
1779
|
+
const success = toolCall.success !== undefined ? toolCall.success : null;
|
|
1780
|
+
const exitCode = toolCall.exit_code !== undefined ? toolCall.exit_code : null;
|
|
1781
|
+
// Format result summary properly if it's an object
|
|
1782
|
+
let resultSummary = toolCall.result_summary || 'No summary available';
|
|
1783
|
+
let formattedResultSummary = '';
|
|
1784
|
+
|
|
1785
|
+
if (typeof resultSummary === 'object' && resultSummary !== null) {
|
|
1786
|
+
// Format the result summary object into human-readable text
|
|
1787
|
+
const parts = [];
|
|
1788
|
+
|
|
1789
|
+
if (resultSummary.exit_code !== undefined) {
|
|
1790
|
+
parts.push(`Exit Code: ${resultSummary.exit_code}`);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
if (resultSummary.has_output !== undefined) {
|
|
1794
|
+
parts.push(`Has Output: ${resultSummary.has_output ? 'Yes' : 'No'}`);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (resultSummary.has_error !== undefined) {
|
|
1798
|
+
parts.push(`Has Error: ${resultSummary.has_error ? 'Yes' : 'No'}`);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (resultSummary.output_lines !== undefined) {
|
|
1802
|
+
parts.push(`Output Lines: ${resultSummary.output_lines}`);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
if (resultSummary.output_preview) {
|
|
1806
|
+
parts.push(`Output Preview: ${resultSummary.output_preview}`);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
if (resultSummary.error_preview) {
|
|
1810
|
+
parts.push(`Error Preview: ${resultSummary.error_preview}`);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
formattedResultSummary = parts.join('\n');
|
|
1814
|
+
} else {
|
|
1815
|
+
formattedResultSummary = String(resultSummary);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// Status information
|
|
1819
|
+
let statusIcon = '⏳';
|
|
1820
|
+
let statusText = 'Running...';
|
|
1821
|
+
let statusClass = 'tool-running';
|
|
1822
|
+
|
|
1823
|
+
if (postEvent) {
|
|
1824
|
+
if (success === true) {
|
|
1825
|
+
statusIcon = '✅';
|
|
1826
|
+
statusText = 'Success';
|
|
1827
|
+
statusClass = 'tool-success';
|
|
1828
|
+
} else if (success === false) {
|
|
1829
|
+
statusIcon = '❌';
|
|
1830
|
+
statusText = 'Failed';
|
|
1831
|
+
statusClass = 'tool-failure';
|
|
1832
|
+
} else {
|
|
1833
|
+
statusIcon = '⏳';
|
|
1834
|
+
statusText = 'Completed';
|
|
1835
|
+
statusClass = 'tool-completed';
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const content = `
|
|
1840
|
+
<div class="structured-view-section">
|
|
1841
|
+
<div class="structured-view-header">
|
|
1842
|
+
<h4>🔧 Tool Call Details</h4>
|
|
1843
|
+
</div>
|
|
1844
|
+
<div class="tool-call-details">
|
|
1845
|
+
<div class="tool-call-info ${statusClass}">
|
|
1846
|
+
<div class="structured-field">
|
|
1847
|
+
<strong>Tool Name:</strong> ${toolName}
|
|
1848
|
+
</div>
|
|
1849
|
+
<div class="structured-field">
|
|
1850
|
+
<strong>Agent:</strong> ${agentName}
|
|
1851
|
+
</div>
|
|
1852
|
+
<div class="structured-field">
|
|
1853
|
+
<strong>Status:</strong> ${statusIcon} ${statusText}
|
|
1854
|
+
</div>
|
|
1855
|
+
<div class="structured-field">
|
|
1856
|
+
<strong>Target:</strong> ${target}
|
|
1857
|
+
</div>
|
|
1858
|
+
<div class="structured-field">
|
|
1859
|
+
<strong>Started:</strong> ${timestamp}
|
|
1860
|
+
</div>
|
|
1861
|
+
<div class="structured-field">
|
|
1862
|
+
<strong>Duration:</strong> ${duration}
|
|
1863
|
+
</div>
|
|
1864
|
+
${success !== null ? `
|
|
1865
|
+
<div class="structured-field">
|
|
1866
|
+
<strong>Success:</strong> ${success}
|
|
1867
|
+
</div>
|
|
1868
|
+
` : ''}
|
|
1869
|
+
${exitCode !== null ? `
|
|
1870
|
+
<div class="structured-field">
|
|
1871
|
+
<strong>Exit Code:</strong> ${exitCode}
|
|
1872
|
+
</div>
|
|
1873
|
+
` : ''}
|
|
1874
|
+
${toolCall.session_id ? `
|
|
1875
|
+
<div class="structured-field">
|
|
1876
|
+
<strong>Session ID:</strong> ${toolCall.session_id}
|
|
1877
|
+
</div>
|
|
1878
|
+
` : ''}
|
|
1879
|
+
</div>
|
|
1880
|
+
|
|
1881
|
+
${formattedResultSummary && formattedResultSummary !== 'No summary available' ? `
|
|
1882
|
+
<div class="result-section">
|
|
1883
|
+
<div class="structured-view-header">
|
|
1884
|
+
<h4>📊 Result Summary</h4>
|
|
1885
|
+
</div>
|
|
1886
|
+
<div class="structured-data">
|
|
1887
|
+
<div class="result-summary" style="white-space: pre-wrap; max-height: 200px; overflow-y: auto; padding: 10px; background: #f8fafc; border-radius: 6px; font-family: monospace; font-size: 12px; line-height: 1.4;">
|
|
1888
|
+
${formattedResultSummary}
|
|
1889
|
+
</div>
|
|
1890
|
+
${typeof resultSummary === 'object' && resultSummary !== null ? `
|
|
1891
|
+
<div class="result-summary-json" style="white-space: pre-wrap; max-height: 200px; overflow-y: auto; padding: 10px; background: #f0f9ff; border-radius: 6px; font-family: monospace; font-size: 11px; line-height: 1.3; margin-top: 10px;">
|
|
1892
|
+
<h5 style="margin: 0 0 8px 0; font-size: 11px; color: #4a5568;">Raw JSON:</h5>
|
|
1893
|
+
${JSON.stringify(resultSummary, null, 2)}
|
|
1894
|
+
</div>
|
|
1895
|
+
` : ''}
|
|
1896
|
+
</div>
|
|
1897
|
+
</div>
|
|
1898
|
+
` : ''}
|
|
1899
|
+
|
|
1900
|
+
${toolName === 'TodoWrite' && parameters.todos ? `
|
|
1901
|
+
<div class="todos-section">
|
|
1902
|
+
<div class="todos-list">
|
|
1903
|
+
${parameters.todos.map(todo => {
|
|
1904
|
+
const statusIcon = {
|
|
1905
|
+
'pending': '⏳',
|
|
1906
|
+
'in_progress': '🔄',
|
|
1907
|
+
'completed': '✅'
|
|
1908
|
+
}[todo.status] || '❓';
|
|
1909
|
+
|
|
1910
|
+
const priorityColor = {
|
|
1911
|
+
'high': '#dc2626',
|
|
1912
|
+
'medium': '#f59e0b',
|
|
1913
|
+
'low': '#10b981'
|
|
1914
|
+
}[todo.priority] || '#6b7280';
|
|
1915
|
+
|
|
1916
|
+
return `
|
|
1917
|
+
<div class="todo-item" style="padding: 8px; margin: 4px 0; border-left: 3px solid ${priorityColor}; background: #f8fafc; border-radius: 4px;">
|
|
1918
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
1919
|
+
<span style="font-size: 16px;">${statusIcon}</span>
|
|
1920
|
+
<span style="font-weight: 500; color: #374151;">${todo.content}</span>
|
|
1921
|
+
<span style="font-size: 11px; color: ${priorityColor}; text-transform: uppercase; font-weight: 600; margin-left: auto;">${todo.priority}</span>
|
|
1922
|
+
</div>
|
|
1923
|
+
</div>
|
|
1924
|
+
`;
|
|
1925
|
+
}).join('')}
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
` : ''}
|
|
1929
|
+
|
|
1930
|
+
${Object.keys(parameters).length > 0 && toolName !== 'TodoWrite' ? `
|
|
1931
|
+
<div class="parameters-section">
|
|
1932
|
+
<div class="structured-view-header">
|
|
1933
|
+
<h4>⚙️ Parameters</h4>
|
|
1934
|
+
</div>
|
|
1935
|
+
<div class="structured-data">
|
|
1936
|
+
<pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px; line-height: 1.4;">${JSON.stringify(parameters, null, 2)}</pre>
|
|
1937
|
+
</div>
|
|
1938
|
+
</div>
|
|
1939
|
+
` : ''}
|
|
1940
|
+
|
|
1941
|
+
<div class="raw-data-section">
|
|
1942
|
+
<div class="structured-view-header">
|
|
1943
|
+
<h4>🔧 JSON Event Data</h4>
|
|
1944
|
+
</div>
|
|
1945
|
+
<div class="structured-data">
|
|
1946
|
+
<div style="margin-bottom: 15px;">
|
|
1947
|
+
<strong>Pre-execution Event:</strong>
|
|
1948
|
+
<pre style="white-space: pre-wrap; font-family: monospace; font-size: 11px; line-height: 1.3; background: #f0f9ff; padding: 8px; border-radius: 4px; max-height: 300px; overflow-y: auto;">${preEvent ? JSON.stringify(preEvent, null, 2) : 'No pre-event data'}</pre>
|
|
1949
|
+
</div>
|
|
1950
|
+
<div>
|
|
1951
|
+
<strong>Post-execution Event:</strong>
|
|
1952
|
+
<pre style="white-space: pre-wrap; font-family: monospace; font-size: 11px; line-height: 1.3; background: #f0f9ff; padding: 8px; border-radius: 4px; max-height: 300px; overflow-y: auto;">${postEvent ? JSON.stringify(postEvent, null, 2) : 'No post-event data'}</pre>
|
|
1953
|
+
</div>
|
|
1954
|
+
</div>
|
|
1955
|
+
</div>
|
|
1956
|
+
</div>
|
|
1957
|
+
</div>
|
|
1958
|
+
`;
|
|
1959
|
+
|
|
1960
|
+
// Special handling for TodoWrite - show only checklist with standard header
|
|
1961
|
+
if (toolName === 'TodoWrite' && parameters.todos) {
|
|
1962
|
+
// Create contextual header matching module-viewer pattern
|
|
1963
|
+
const contextualHeader = `
|
|
1964
|
+
<div class="contextual-header">
|
|
1965
|
+
<h3 class="contextual-header-text">TodoWrite: ${agentName} ${this.formatTimestamp(toolCall.timestamp)}</h3>
|
|
1966
|
+
</div>
|
|
1967
|
+
`;
|
|
1968
|
+
|
|
1969
|
+
const todoContent = `
|
|
1970
|
+
<div class="todo-checklist">
|
|
1971
|
+
${parameters.todos.map(todo => {
|
|
1972
|
+
const statusIcon = {
|
|
1973
|
+
'pending': '⏳',
|
|
1974
|
+
'in_progress': '🔄',
|
|
1975
|
+
'completed': '✅'
|
|
1976
|
+
}[todo.status] || '❓';
|
|
1977
|
+
|
|
1978
|
+
const priorityIcon = {
|
|
1979
|
+
'high': '🔴',
|
|
1980
|
+
'medium': '🟡',
|
|
1981
|
+
'low': '🟢'
|
|
1982
|
+
}[todo.priority] || '🟡';
|
|
1983
|
+
|
|
1984
|
+
return `
|
|
1985
|
+
<div class="todo-item todo-${todo.status || 'pending'}">
|
|
1986
|
+
<span class="todo-status">${statusIcon}</span>
|
|
1987
|
+
<span class="todo-content">${todo.content || 'No content'}</span>
|
|
1988
|
+
<span class="todo-priority priority-${todo.priority || 'medium'}">${priorityIcon}</span>
|
|
1989
|
+
</div>
|
|
1990
|
+
`;
|
|
1991
|
+
}).join('')}
|
|
1992
|
+
</div>
|
|
1993
|
+
`;
|
|
1994
|
+
|
|
1995
|
+
if (this.moduleViewer.dataContainer) {
|
|
1996
|
+
this.moduleViewer.dataContainer.innerHTML = contextualHeader + todoContent;
|
|
1997
|
+
}
|
|
1998
|
+
if (this.moduleViewer.jsonContainer) {
|
|
1999
|
+
const toolCallData = {
|
|
2000
|
+
toolCall: toolCall,
|
|
2001
|
+
preEvent: preEvent,
|
|
2002
|
+
postEvent: postEvent
|
|
2003
|
+
};
|
|
2004
|
+
this.moduleViewer.jsonContainer.innerHTML = `<pre>${JSON.stringify(toolCallData, null, 2)}</pre>`;
|
|
2005
|
+
}
|
|
2006
|
+
} else {
|
|
2007
|
+
// For other tools, use the module viewer's ingest method with pre-event
|
|
2008
|
+
if (this.moduleViewer && preEvent) {
|
|
2009
|
+
this.moduleViewer.ingest(preEvent);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Show detailed file operations in module viewer
|
|
2018
|
+
*/
|
|
2019
|
+
showFileDetails(filePath) {
|
|
2020
|
+
const fileData = this.fileOperations.get(filePath);
|
|
2021
|
+
if (!fileData) return;
|
|
2022
|
+
|
|
2023
|
+
// Filter operations by selected session if applicable
|
|
2024
|
+
let operations = fileData.operations;
|
|
2025
|
+
if (this.selectedSessionId) {
|
|
2026
|
+
operations = operations.filter(op => op.sessionId === this.selectedSessionId);
|
|
2027
|
+
if (operations.length === 0) {
|
|
2028
|
+
// No operations from this session
|
|
2029
|
+
this.moduleViewer.showErrorMessage(
|
|
2030
|
+
'No Operations in Selected Session',
|
|
2031
|
+
`This file has no operations from the selected session.`
|
|
2032
|
+
);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Get file name from path for header
|
|
2038
|
+
const fileName = filePath.split('/').pop() || filePath;
|
|
2039
|
+
const lastOp = operations[operations.length - 1];
|
|
2040
|
+
const headerTimestamp = this.formatTimestamp(lastOp.timestamp);
|
|
2041
|
+
|
|
2042
|
+
// Create contextual header matching module-viewer pattern
|
|
2043
|
+
const contextualHeader = `
|
|
2044
|
+
<div class="contextual-header">
|
|
2045
|
+
<h3 class="contextual-header-text">File: ${fileName} ${headerTimestamp}</h3>
|
|
2046
|
+
</div>
|
|
2047
|
+
`;
|
|
2048
|
+
|
|
2049
|
+
const content = `
|
|
2050
|
+
<div class="structured-view-section">
|
|
2051
|
+
<div class="file-details">
|
|
2052
|
+
<div class="file-path-display">
|
|
2053
|
+
<strong>Full Path:</strong> ${filePath}
|
|
2054
|
+
</div>
|
|
2055
|
+
<div class="operations-list">
|
|
2056
|
+
${operations.map(op => `
|
|
2057
|
+
<div class="operation-item">
|
|
2058
|
+
<div class="operation-header">
|
|
2059
|
+
<span class="operation-icon">${this.getOperationIcon(op.operation)}</span>
|
|
2060
|
+
<span class="operation-type">${op.operation}</span>
|
|
2061
|
+
<span class="operation-timestamp">${new Date(op.timestamp).toLocaleString()}</span>
|
|
2062
|
+
${(['edit', 'write'].includes(op.operation)) ? `
|
|
2063
|
+
<span class="git-diff-icon"
|
|
2064
|
+
onclick="showGitDiffModal('${filePath}', '${op.timestamp}')"
|
|
2065
|
+
title="View git diff for this file operation"
|
|
2066
|
+
style="margin-left: 8px; cursor: pointer; font-size: 16px;">
|
|
2067
|
+
📋
|
|
2068
|
+
</span>
|
|
2069
|
+
` : ''}
|
|
2070
|
+
</div>
|
|
2071
|
+
<div class="operation-details">
|
|
2072
|
+
<strong>Agent:</strong> ${op.agent}<br>
|
|
2073
|
+
<strong>Session:</strong> ${op.sessionId ? op.sessionId.substring(0, 8) + '...' : 'Unknown'}
|
|
2074
|
+
${op.details ? `<br><strong>Details:</strong> ${op.details}` : ''}
|
|
2075
|
+
</div>
|
|
2076
|
+
</div>
|
|
2077
|
+
`).join('')}
|
|
2078
|
+
</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
`;
|
|
2082
|
+
|
|
2083
|
+
// Use the new dual-pane approach for file details with standard header
|
|
2084
|
+
if (this.moduleViewer.dataContainer) {
|
|
2085
|
+
this.moduleViewer.dataContainer.innerHTML = contextualHeader + content;
|
|
2086
|
+
}
|
|
2087
|
+
if (this.moduleViewer.jsonContainer) {
|
|
2088
|
+
// Show the file data with operations
|
|
2089
|
+
this.moduleViewer.jsonContainer.innerHTML = `<pre>${JSON.stringify(fileData, null, 2)}</pre>`;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
/**
|
|
2094
|
+
* Update file operations from events
|
|
2095
|
+
*/
|
|
2096
|
+
updateFileOperations(events) {
|
|
2097
|
+
// Clear existing data
|
|
2098
|
+
this.fileOperations.clear();
|
|
2099
|
+
|
|
2100
|
+
console.log('updateFileOperations - processing', events.length, 'events');
|
|
2101
|
+
|
|
2102
|
+
// Group events by session and timestamp to match pre/post pairs
|
|
2103
|
+
const eventPairs = new Map(); // Key: session_id + timestamp + tool_name
|
|
2104
|
+
let fileOperationCount = 0;
|
|
2105
|
+
|
|
2106
|
+
// First pass: collect all tool events and group them
|
|
2107
|
+
events.forEach((event, index) => {
|
|
2108
|
+
const isFileOp = this.isFileOperation(event);
|
|
2109
|
+
if (isFileOp) fileOperationCount++;
|
|
2110
|
+
|
|
2111
|
+
if (index < 5) { // Debug first 5 events with more detail
|
|
2112
|
+
console.log(`Event ${index}:`, {
|
|
2113
|
+
type: event.type,
|
|
2114
|
+
subtype: event.subtype,
|
|
2115
|
+
tool_name: event.tool_name,
|
|
2116
|
+
tool_parameters: event.tool_parameters,
|
|
2117
|
+
isFileOp: isFileOp
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
if (isFileOp) {
|
|
2122
|
+
const toolName = event.tool_name;
|
|
2123
|
+
const sessionId = event.session_id || 'unknown';
|
|
2124
|
+
const eventKey = `${sessionId}_${toolName}_${Math.floor(new Date(event.timestamp).getTime() / 1000)}`; // Group by second
|
|
2125
|
+
|
|
2126
|
+
if (!eventPairs.has(eventKey)) {
|
|
2127
|
+
eventPairs.set(eventKey, {
|
|
2128
|
+
pre_event: null,
|
|
2129
|
+
post_event: null,
|
|
2130
|
+
tool_name: toolName,
|
|
2131
|
+
session_id: sessionId
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
const pair = eventPairs.get(eventKey);
|
|
2136
|
+
if (event.subtype === 'pre_tool' || event.type === 'hook' && !event.subtype.includes('post')) {
|
|
2137
|
+
pair.pre_event = event;
|
|
2138
|
+
} else if (event.subtype === 'post_tool' || event.subtype.includes('post')) {
|
|
2139
|
+
pair.post_event = event;
|
|
2140
|
+
} else {
|
|
2141
|
+
// For events without clear pre/post distinction, treat as both
|
|
2142
|
+
pair.pre_event = event;
|
|
2143
|
+
pair.post_event = event;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
console.log('updateFileOperations - found', fileOperationCount, 'file operations in', eventPairs.size, 'event pairs');
|
|
2149
|
+
|
|
2150
|
+
// Second pass: extract file paths and operations from paired events
|
|
2151
|
+
eventPairs.forEach((pair, key) => {
|
|
2152
|
+
const filePath = this.extractFilePathFromPair(pair);
|
|
2153
|
+
|
|
2154
|
+
if (filePath) {
|
|
2155
|
+
console.log('File operation detected for:', filePath, 'from pair:', key);
|
|
2156
|
+
|
|
2157
|
+
if (!this.fileOperations.has(filePath)) {
|
|
2158
|
+
this.fileOperations.set(filePath, {
|
|
2159
|
+
path: filePath,
|
|
2160
|
+
operations: [],
|
|
2161
|
+
lastOperation: null
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const fileData = this.fileOperations.get(filePath);
|
|
2166
|
+
const operation = this.getFileOperationFromPair(pair);
|
|
2167
|
+
const timestamp = pair.post_event?.timestamp || pair.pre_event?.timestamp;
|
|
2168
|
+
|
|
2169
|
+
const agentInfo = this.extractAgentFromPair(pair);
|
|
2170
|
+
const workingDirectory = this.extractWorkingDirectoryFromPair(pair);
|
|
2171
|
+
|
|
2172
|
+
fileData.operations.push({
|
|
2173
|
+
operation: operation,
|
|
2174
|
+
timestamp: timestamp,
|
|
2175
|
+
agent: agentInfo.name,
|
|
2176
|
+
confidence: agentInfo.confidence,
|
|
2177
|
+
sessionId: pair.session_id,
|
|
2178
|
+
details: this.getFileOperationDetailsFromPair(pair),
|
|
2179
|
+
workingDirectory: workingDirectory
|
|
2180
|
+
});
|
|
2181
|
+
fileData.lastOperation = timestamp;
|
|
2182
|
+
} else {
|
|
2183
|
+
console.log('No file path found for pair:', key, pair);
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
console.log('updateFileOperations - final result:', this.fileOperations.size, 'file operations');
|
|
2188
|
+
if (this.fileOperations.size > 0) {
|
|
2189
|
+
console.log('File operations map:', Array.from(this.fileOperations.entries()));
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/**
|
|
2194
|
+
* Update tool calls from events - pairs pre/post tool events into complete tool calls
|
|
2195
|
+
*/
|
|
2196
|
+
updateToolCalls(events) {
|
|
2197
|
+
// Clear existing data
|
|
2198
|
+
this.toolCalls.clear();
|
|
2199
|
+
|
|
2200
|
+
console.log('updateToolCalls - processing', events.length, 'events');
|
|
2201
|
+
|
|
2202
|
+
// Group events by session and timestamp to match pre/post pairs
|
|
2203
|
+
const toolCallPairs = new Map(); // Key: session_id + timestamp + tool_name
|
|
2204
|
+
let toolOperationCount = 0;
|
|
2205
|
+
|
|
2206
|
+
// First pass: collect all tool events and group them
|
|
2207
|
+
events.forEach((event, index) => {
|
|
2208
|
+
const isToolOp = this.isToolOperation(event);
|
|
2209
|
+
if (isToolOp) toolOperationCount++;
|
|
2210
|
+
|
|
2211
|
+
if (index < 5) { // Debug first 5 events with more detail
|
|
2212
|
+
console.log(`Tool Event ${index}:`, {
|
|
2213
|
+
type: event.type,
|
|
2214
|
+
subtype: event.subtype,
|
|
2215
|
+
tool_name: event.tool_name,
|
|
2216
|
+
tool_parameters: event.tool_parameters,
|
|
2217
|
+
isToolOp: isToolOp
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
if (isToolOp) {
|
|
2222
|
+
const toolName = event.tool_name;
|
|
2223
|
+
const sessionId = event.session_id || 'unknown';
|
|
2224
|
+
const eventKey = `${sessionId}_${toolName}_${Math.floor(new Date(event.timestamp).getTime() / 1000)}`; // Group by second
|
|
2225
|
+
|
|
2226
|
+
if (!toolCallPairs.has(eventKey)) {
|
|
2227
|
+
toolCallPairs.set(eventKey, {
|
|
2228
|
+
pre_event: null,
|
|
2229
|
+
post_event: null,
|
|
2230
|
+
tool_name: toolName,
|
|
2231
|
+
session_id: sessionId,
|
|
2232
|
+
operation_type: null,
|
|
2233
|
+
timestamp: null,
|
|
2234
|
+
duration_ms: null,
|
|
2235
|
+
success: null,
|
|
2236
|
+
exit_code: null,
|
|
2237
|
+
result_summary: null,
|
|
2238
|
+
agent_type: null
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
const pair = toolCallPairs.get(eventKey);
|
|
2243
|
+
if (event.subtype === 'pre_tool' || (event.type === 'hook' && !event.subtype.includes('post'))) {
|
|
2244
|
+
pair.pre_event = event;
|
|
2245
|
+
pair.timestamp = event.timestamp;
|
|
2246
|
+
pair.operation_type = event.operation_type || 'tool_execution';
|
|
2247
|
+
pair.agent_type = event.agent_type || event.subagent_type || 'PM';
|
|
2248
|
+
} else if (event.subtype === 'post_tool' || event.subtype.includes('post')) {
|
|
2249
|
+
pair.post_event = event;
|
|
2250
|
+
pair.duration_ms = event.duration_ms;
|
|
2251
|
+
pair.success = event.success;
|
|
2252
|
+
pair.exit_code = event.exit_code;
|
|
2253
|
+
pair.result_summary = event.result_summary;
|
|
2254
|
+
if (!pair.agent_type) {
|
|
2255
|
+
pair.agent_type = event.agent_type || event.subagent_type || 'PM';
|
|
2256
|
+
}
|
|
2257
|
+
} else {
|
|
2258
|
+
// For events without clear pre/post distinction, treat as both
|
|
2259
|
+
pair.pre_event = event;
|
|
2260
|
+
pair.post_event = event;
|
|
2261
|
+
pair.timestamp = event.timestamp;
|
|
2262
|
+
pair.agent_type = event.agent_type || event.subagent_type || 'PM';
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
console.log('updateToolCalls - found', toolOperationCount, 'tool operations in', toolCallPairs.size, 'tool call pairs');
|
|
2268
|
+
|
|
2269
|
+
// Second pass: store complete tool calls
|
|
2270
|
+
toolCallPairs.forEach((pair, key) => {
|
|
2271
|
+
// Ensure we have at least a pre_event or post_event
|
|
2272
|
+
if (pair.pre_event || pair.post_event) {
|
|
2273
|
+
console.log('Tool call detected for:', pair.tool_name, 'from pair:', key);
|
|
2274
|
+
this.toolCalls.set(key, pair);
|
|
2275
|
+
} else {
|
|
2276
|
+
console.log('No valid tool call found for pair:', key, pair);
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
console.log('updateToolCalls - final result:', this.toolCalls.size, 'tool calls');
|
|
2281
|
+
if (this.toolCalls.size > 0) {
|
|
2282
|
+
console.log('Tool calls map:', Array.from(this.toolCalls.entries()));
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
/**
|
|
2287
|
+
* Check if event is a tool operation
|
|
2288
|
+
*/
|
|
2289
|
+
isToolOperation(event) {
|
|
2290
|
+
const type = event.type || '';
|
|
2291
|
+
const subtype = event.subtype || '';
|
|
2292
|
+
|
|
2293
|
+
// Check for hook events with tool subtypes
|
|
2294
|
+
const isHookToolEvent = type === 'hook' && (
|
|
2295
|
+
subtype.includes('tool') ||
|
|
2296
|
+
subtype.includes('pre_') ||
|
|
2297
|
+
subtype.includes('post_')
|
|
2298
|
+
);
|
|
2299
|
+
|
|
2300
|
+
// Events with tool_name
|
|
2301
|
+
const hasToolName = event.tool_name;
|
|
2302
|
+
|
|
2303
|
+
// Events with tools array (multiple tools)
|
|
2304
|
+
const hasToolsArray = event.tools && Array.isArray(event.tools);
|
|
2305
|
+
|
|
2306
|
+
// Legacy hook events with tool patterns (backward compatibility)
|
|
2307
|
+
const isLegacyHookEvent = type.startsWith('hook.') && (
|
|
2308
|
+
type.includes('tool') ||
|
|
2309
|
+
type.includes('pre') ||
|
|
2310
|
+
type.includes('post')
|
|
2311
|
+
);
|
|
2312
|
+
|
|
2313
|
+
return isHookToolEvent || hasToolName || hasToolsArray || isLegacyHookEvent;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/**
|
|
2317
|
+
* Check if event is a file operation
|
|
2318
|
+
*/
|
|
2319
|
+
isFileOperation(event) {
|
|
2320
|
+
const toolName = event.tool_name;
|
|
2321
|
+
const fileTools = ['Read', 'Write', 'Edit', 'MultiEdit', 'Glob', 'LS', 'NotebookRead', 'NotebookEdit', 'Grep'];
|
|
2322
|
+
|
|
2323
|
+
// Check for direct tool name match
|
|
2324
|
+
if (fileTools.includes(toolName)) {
|
|
2325
|
+
console.log('isFileOperation - direct tool match:', toolName);
|
|
2326
|
+
return true;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// Check for hook events that involve file tools (updated for new structure)
|
|
2330
|
+
const type = event.type || '';
|
|
2331
|
+
const subtype = event.subtype || '';
|
|
2332
|
+
|
|
2333
|
+
// Check both legacy format and new format
|
|
2334
|
+
const isHookEvent = type === 'hook' || type.startsWith('hook.');
|
|
2335
|
+
|
|
2336
|
+
if (isHookEvent) {
|
|
2337
|
+
// Check if tool_name indicates file operation
|
|
2338
|
+
if (fileTools.includes(event.tool_name)) {
|
|
2339
|
+
console.log('isFileOperation - hook tool match:', event.tool_name);
|
|
2340
|
+
return true;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// Check if parameters suggest file operation
|
|
2344
|
+
const params = event.tool_parameters || {};
|
|
2345
|
+
const hasFileParams = !!(params.file_path || params.path || params.notebook_path || params.pattern);
|
|
2346
|
+
|
|
2347
|
+
// Also check top-level event for file parameters (flat structure)
|
|
2348
|
+
const hasDirectFileParams = !!(event.file_path || event.path || event.notebook_path || event.pattern);
|
|
2349
|
+
|
|
2350
|
+
const hasAnyFileParams = hasFileParams || hasDirectFileParams;
|
|
2351
|
+
if (hasAnyFileParams) {
|
|
2352
|
+
console.log('isFileOperation - file params match:', { hasFileParams, hasDirectFileParams, params, directParams: { file_path: event.file_path, path: event.path } });
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
return hasAnyFileParams;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* Extract file path from event
|
|
2363
|
+
*/
|
|
2364
|
+
extractFilePath(event) {
|
|
2365
|
+
// Check tool_parameters first
|
|
2366
|
+
const params = event.tool_parameters;
|
|
2367
|
+
if (params) {
|
|
2368
|
+
if (params.file_path) return params.file_path;
|
|
2369
|
+
if (params.path) return params.path;
|
|
2370
|
+
if (params.notebook_path) return params.notebook_path;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// Check top-level event (flat structure)
|
|
2374
|
+
if (event.file_path) return event.file_path;
|
|
2375
|
+
if (event.path) return event.path;
|
|
2376
|
+
if (event.notebook_path) return event.notebook_path;
|
|
2377
|
+
|
|
2378
|
+
// Check tool_input if available (sometimes path is here)
|
|
2379
|
+
if (event.tool_input) {
|
|
2380
|
+
if (event.tool_input.file_path) return event.tool_input.file_path;
|
|
2381
|
+
if (event.tool_input.path) return event.tool_input.path;
|
|
2382
|
+
if (event.tool_input.notebook_path) return event.tool_input.notebook_path;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// Check result/output if available (sometimes path is in result)
|
|
2386
|
+
if (event.result) {
|
|
2387
|
+
if (event.result.file_path) return event.result.file_path;
|
|
2388
|
+
if (event.result.path) return event.result.path;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
return null;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
/**
|
|
2395
|
+
* Extract file path from paired pre/post events
|
|
2396
|
+
*/
|
|
2397
|
+
extractFilePathFromPair(pair) {
|
|
2398
|
+
// Try pre_event first, then post_event
|
|
2399
|
+
const preEvent = pair.pre_event;
|
|
2400
|
+
const postEvent = pair.post_event;
|
|
2401
|
+
|
|
2402
|
+
if (preEvent) {
|
|
2403
|
+
const prePath = this.extractFilePath(preEvent);
|
|
2404
|
+
if (prePath) return prePath;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
if (postEvent) {
|
|
2408
|
+
const postPath = this.extractFilePath(postEvent);
|
|
2409
|
+
if (postPath) return postPath;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
/**
|
|
2416
|
+
* Get file operation type
|
|
2417
|
+
*/
|
|
2418
|
+
getFileOperation(event) {
|
|
2419
|
+
const toolName = event.tool_name;
|
|
2420
|
+
const operationMap = {
|
|
2421
|
+
'Read': 'read',
|
|
2422
|
+
'Write': 'write',
|
|
2423
|
+
'Edit': 'edit',
|
|
2424
|
+
'MultiEdit': 'edit',
|
|
2425
|
+
'Glob': 'search',
|
|
2426
|
+
'LS': 'list',
|
|
2427
|
+
'NotebookRead': 'read',
|
|
2428
|
+
'NotebookEdit': 'edit',
|
|
2429
|
+
'Grep': 'search'
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
return operationMap[toolName] || 'operation';
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/**
|
|
2436
|
+
* Get file operation type from paired events
|
|
2437
|
+
*/
|
|
2438
|
+
getFileOperationFromPair(pair) {
|
|
2439
|
+
const toolName = pair.tool_name;
|
|
2440
|
+
const operationMap = {
|
|
2441
|
+
'Read': 'read',
|
|
2442
|
+
'Write': 'write',
|
|
2443
|
+
'Edit': 'edit',
|
|
2444
|
+
'MultiEdit': 'edit',
|
|
2445
|
+
'Glob': 'search',
|
|
2446
|
+
'LS': 'list',
|
|
2447
|
+
'NotebookRead': 'read',
|
|
2448
|
+
'NotebookEdit': 'edit',
|
|
2449
|
+
'Grep': 'search'
|
|
2450
|
+
};
|
|
2451
|
+
|
|
2452
|
+
return operationMap[toolName] || 'operation';
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
/**
|
|
2456
|
+
* Extract agent from paired events using inference
|
|
2457
|
+
*/
|
|
2458
|
+
extractAgentFromPair(pair) {
|
|
2459
|
+
// Try to get inference from either event
|
|
2460
|
+
const preEvent = pair.pre_event;
|
|
2461
|
+
const postEvent = pair.post_event;
|
|
2462
|
+
|
|
2463
|
+
if (preEvent) {
|
|
2464
|
+
const eventIndex = this.eventViewer.events.indexOf(preEvent);
|
|
2465
|
+
const inference = this.getInferredAgent(eventIndex);
|
|
2466
|
+
if (inference) {
|
|
2467
|
+
return {
|
|
2468
|
+
name: inference.agentName,
|
|
2469
|
+
confidence: inference.confidence
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
if (postEvent) {
|
|
2475
|
+
const eventIndex = this.eventViewer.events.indexOf(postEvent);
|
|
2476
|
+
const inference = this.getInferredAgent(eventIndex);
|
|
2477
|
+
if (inference) {
|
|
2478
|
+
return {
|
|
2479
|
+
name: inference.agentName,
|
|
2480
|
+
confidence: inference.confidence
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// Fallback to legacy logic
|
|
2486
|
+
const preAgent = preEvent?.agent_type || preEvent?.subagent_type;
|
|
2487
|
+
const postAgent = postEvent?.agent_type || postEvent?.subagent_type;
|
|
2488
|
+
|
|
2489
|
+
// Prefer non-'main' and non-'unknown' agents
|
|
2490
|
+
if (preAgent && preAgent !== 'main' && preAgent !== 'unknown') {
|
|
2491
|
+
return { name: preAgent, confidence: 'legacy' };
|
|
2492
|
+
}
|
|
2493
|
+
if (postAgent && postAgent !== 'main' && postAgent !== 'unknown') {
|
|
2494
|
+
return { name: postAgent, confidence: 'legacy' };
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// Fallback to any agent
|
|
2498
|
+
const agentName = preAgent || postAgent || 'PM';
|
|
2499
|
+
return { name: agentName, confidence: 'fallback' };
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* Extract working directory from event pair
|
|
2504
|
+
*/
|
|
2505
|
+
extractWorkingDirectoryFromPair(pair) {
|
|
2506
|
+
// Try to get working directory from either event's data
|
|
2507
|
+
const preEvent = pair.pre_event;
|
|
2508
|
+
const postEvent = pair.post_event;
|
|
2509
|
+
|
|
2510
|
+
// Check pre_event first
|
|
2511
|
+
if (preEvent?.data?.working_directory) {
|
|
2512
|
+
return preEvent.data.working_directory;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// Check post_event
|
|
2516
|
+
if (postEvent?.data?.working_directory) {
|
|
2517
|
+
return postEvent.data.working_directory;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// Check tool_parameters for working directory
|
|
2521
|
+
if (preEvent?.tool_parameters?.working_dir) {
|
|
2522
|
+
return preEvent.tool_parameters.working_dir;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
if (postEvent?.tool_parameters?.working_dir) {
|
|
2526
|
+
return postEvent.tool_parameters.working_dir;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// Fallback to null (will use default behavior in showGitDiffModal)
|
|
2530
|
+
return null;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* Get file operation details
|
|
2535
|
+
*/
|
|
2536
|
+
getFileOperationDetails(event) {
|
|
2537
|
+
const toolName = event.tool_name;
|
|
2538
|
+
const params = event.tool_parameters;
|
|
2539
|
+
|
|
2540
|
+
switch (toolName) {
|
|
2541
|
+
case 'Edit':
|
|
2542
|
+
case 'MultiEdit':
|
|
2543
|
+
return `Modified content`;
|
|
2544
|
+
case 'Write':
|
|
2545
|
+
return `Created/updated file`;
|
|
2546
|
+
case 'Read':
|
|
2547
|
+
return `Read file content`;
|
|
2548
|
+
case 'NotebookRead':
|
|
2549
|
+
return `Read notebook content`;
|
|
2550
|
+
case 'NotebookEdit':
|
|
2551
|
+
return `Modified notebook`;
|
|
2552
|
+
case 'Glob':
|
|
2553
|
+
return `Searched pattern: ${params?.pattern || 'unknown'}`;
|
|
2554
|
+
case 'Grep':
|
|
2555
|
+
return `Searched pattern: ${params?.pattern || 'unknown'}`;
|
|
2556
|
+
case 'LS':
|
|
2557
|
+
return `Listed directory`;
|
|
2558
|
+
default:
|
|
2559
|
+
return '';
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
/**
|
|
2564
|
+
* Get file operation details from paired events
|
|
2565
|
+
*/
|
|
2566
|
+
getFileOperationDetailsFromPair(pair) {
|
|
2567
|
+
const toolName = pair.tool_name;
|
|
2568
|
+
|
|
2569
|
+
// Get parameters from either event
|
|
2570
|
+
const preParams = pair.pre_event?.tool_parameters || {};
|
|
2571
|
+
const postParams = pair.post_event?.tool_parameters || {};
|
|
2572
|
+
const params = { ...preParams, ...postParams };
|
|
2573
|
+
|
|
2574
|
+
switch (toolName) {
|
|
2575
|
+
case 'Edit':
|
|
2576
|
+
case 'MultiEdit':
|
|
2577
|
+
return `Modified content`;
|
|
2578
|
+
case 'Write':
|
|
2579
|
+
return `Created/updated file`;
|
|
2580
|
+
case 'Read':
|
|
2581
|
+
return `Read file content`;
|
|
2582
|
+
case 'NotebookRead':
|
|
2583
|
+
return `Read notebook content`;
|
|
2584
|
+
case 'NotebookEdit':
|
|
2585
|
+
return `Modified notebook`;
|
|
2586
|
+
case 'Glob':
|
|
2587
|
+
return `Searched pattern: ${params?.pattern || 'unknown'}`;
|
|
2588
|
+
case 'Grep':
|
|
2589
|
+
return `Searched pattern: ${params?.pattern || 'unknown'}`;
|
|
2590
|
+
case 'LS':
|
|
2591
|
+
return `Listed directory`;
|
|
2592
|
+
default:
|
|
2593
|
+
return '';
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/**
|
|
2598
|
+
* Get icon for file operations - shows combined icons for read+write
|
|
2599
|
+
*/
|
|
2600
|
+
getFileOperationIcon(operations) {
|
|
2601
|
+
// Check for notebook operations first
|
|
2602
|
+
const hasNotebook = operations.some(op => op.details && (op.details.includes('notebook') || op.details.includes('Notebook')));
|
|
2603
|
+
if (hasNotebook) return '📓';
|
|
2604
|
+
|
|
2605
|
+
const hasWrite = operations.some(op => ['write', 'edit'].includes(op.operation));
|
|
2606
|
+
const hasRead = operations.some(op => op.operation === 'read');
|
|
2607
|
+
const hasSearch = operations.some(op => op.operation === 'search');
|
|
2608
|
+
const hasList = operations.some(op => op.operation === 'list');
|
|
2609
|
+
|
|
2610
|
+
// Show both icons for read+write combinations
|
|
2611
|
+
if (hasWrite && hasRead) return '📖✏️'; // Both read and write
|
|
2612
|
+
if (hasWrite) return '✏️'; // Write only
|
|
2613
|
+
if (hasRead) return '📖'; // Read only
|
|
2614
|
+
if (hasSearch) return '🔍'; // Search only
|
|
2615
|
+
if (hasList) return '📋'; // List only
|
|
2616
|
+
return '📄'; // Default
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Get icon for specific operation
|
|
2621
|
+
*/
|
|
2622
|
+
getOperationIcon(operation) {
|
|
2623
|
+
const icons = {
|
|
2624
|
+
read: '📖',
|
|
2625
|
+
write: '📝',
|
|
2626
|
+
edit: '✏️',
|
|
2627
|
+
search: '🔍',
|
|
2628
|
+
list: '📋'
|
|
2629
|
+
};
|
|
2630
|
+
return icons[operation] || '📄';
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
/**
|
|
2634
|
+
* Format timestamp for display
|
|
2635
|
+
* @param {string|number} timestamp - Timestamp to format
|
|
2636
|
+
* @returns {string} Formatted time
|
|
2637
|
+
*/
|
|
2638
|
+
formatTimestamp(timestamp) {
|
|
2639
|
+
if (!timestamp) return 'Unknown time';
|
|
2640
|
+
|
|
2641
|
+
try {
|
|
2642
|
+
const date = new Date(timestamp);
|
|
2643
|
+
return date.toLocaleTimeString('en-US', {
|
|
2644
|
+
hour: 'numeric',
|
|
2645
|
+
minute: '2-digit',
|
|
2646
|
+
second: '2-digit',
|
|
2647
|
+
hour12: true
|
|
2648
|
+
});
|
|
2649
|
+
} catch (e) {
|
|
2650
|
+
return 'Invalid time';
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* Get relative file path for display
|
|
2656
|
+
*/
|
|
2657
|
+
getRelativeFilePath(filePath) {
|
|
2658
|
+
// Try to make path relative to common base paths
|
|
2659
|
+
const commonPaths = [
|
|
2660
|
+
'/Users/masa/Projects/claude-mpm/',
|
|
2661
|
+
'.'
|
|
2662
|
+
];
|
|
2663
|
+
|
|
2664
|
+
for (const basePath of commonPaths) {
|
|
2665
|
+
if (filePath.startsWith(basePath)) {
|
|
2666
|
+
return filePath.substring(basePath.length).replace(/^\//, '');
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// If no common path found, show last 2-3 path segments
|
|
2671
|
+
const parts = filePath.split('/');
|
|
2672
|
+
if (parts.length > 3) {
|
|
2673
|
+
return '.../' + parts.slice(-2).join('/');
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
return filePath;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/**
|
|
2680
|
+
* Apply agents tab filtering
|
|
2681
|
+
*/
|
|
2682
|
+
applyAgentsFilters(events) {
|
|
2683
|
+
const searchInput = document.getElementById('agents-search-input');
|
|
2684
|
+
const typeFilter = document.getElementById('agents-type-filter');
|
|
2685
|
+
|
|
2686
|
+
const searchText = searchInput ? searchInput.value.toLowerCase() : '';
|
|
2687
|
+
const typeValue = typeFilter ? typeFilter.value : '';
|
|
2688
|
+
|
|
2689
|
+
return events.filter(event => {
|
|
2690
|
+
// Search filter
|
|
2691
|
+
if (searchText) {
|
|
2692
|
+
const searchableText = [
|
|
2693
|
+
event.subagent_type || '',
|
|
2694
|
+
event.agent_type || '',
|
|
2695
|
+
event.name || '',
|
|
2696
|
+
event.type || '',
|
|
2697
|
+
event.subtype || ''
|
|
2698
|
+
].join(' ').toLowerCase();
|
|
2699
|
+
|
|
2700
|
+
if (!searchableText.includes(searchText)) {
|
|
2701
|
+
return false;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// Type filter
|
|
2706
|
+
if (typeValue) {
|
|
2707
|
+
const agentType = event.subagent_type || event.agent_type || 'unknown';
|
|
2708
|
+
if (!agentType.toLowerCase().includes(typeValue.toLowerCase())) {
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
return true;
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
/**
|
|
2718
|
+
* Apply tools tab filtering
|
|
2719
|
+
*/
|
|
2720
|
+
applyToolsFilters(events) {
|
|
2721
|
+
const searchInput = document.getElementById('tools-search-input');
|
|
2722
|
+
const typeFilter = document.getElementById('tools-type-filter');
|
|
2723
|
+
|
|
2724
|
+
const searchText = searchInput ? searchInput.value.toLowerCase() : '';
|
|
2725
|
+
const typeValue = typeFilter ? typeFilter.value : '';
|
|
2726
|
+
|
|
2727
|
+
return events.filter(event => {
|
|
2728
|
+
// Search filter
|
|
2729
|
+
if (searchText) {
|
|
2730
|
+
const searchableText = [
|
|
2731
|
+
event.tool_name || '',
|
|
2732
|
+
event.agent_type || '',
|
|
2733
|
+
event.type || '',
|
|
2734
|
+
event.subtype || ''
|
|
2735
|
+
].join(' ').toLowerCase();
|
|
2736
|
+
|
|
2737
|
+
if (!searchableText.includes(searchText)) {
|
|
2738
|
+
return false;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// Type filter
|
|
2743
|
+
if (typeValue) {
|
|
2744
|
+
const toolName = event.tool_name || '';
|
|
2745
|
+
if (toolName !== typeValue) {
|
|
2746
|
+
return false;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
return true;
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
/**
|
|
2755
|
+
* Apply tools tab filtering for tool calls
|
|
2756
|
+
*/
|
|
2757
|
+
applyToolCallFilters(toolCallsArray) {
|
|
2758
|
+
const searchInput = document.getElementById('tools-search-input');
|
|
2759
|
+
const typeFilter = document.getElementById('tools-type-filter');
|
|
2760
|
+
|
|
2761
|
+
const searchText = searchInput ? searchInput.value.toLowerCase() : '';
|
|
2762
|
+
const typeValue = typeFilter ? typeFilter.value : '';
|
|
2763
|
+
|
|
2764
|
+
return toolCallsArray.filter(([key, toolCall]) => {
|
|
2765
|
+
// Search filter
|
|
2766
|
+
if (searchText) {
|
|
2767
|
+
const searchableText = [
|
|
2768
|
+
toolCall.tool_name || '',
|
|
2769
|
+
toolCall.agent_type || '',
|
|
2770
|
+
'tool_call'
|
|
2771
|
+
].join(' ').toLowerCase();
|
|
2772
|
+
|
|
2773
|
+
if (!searchableText.includes(searchText)) {
|
|
2774
|
+
return false;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
// Type filter
|
|
2779
|
+
if (typeValue) {
|
|
2780
|
+
const toolName = toolCall.tool_name || '';
|
|
2781
|
+
if (toolName !== typeValue) {
|
|
2782
|
+
return false;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
return true;
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
/**
|
|
2791
|
+
* Apply files tab filtering
|
|
2792
|
+
*/
|
|
2793
|
+
applyFilesFilters(fileOperations) {
|
|
2794
|
+
const searchInput = document.getElementById('files-search-input');
|
|
2795
|
+
const typeFilter = document.getElementById('files-type-filter');
|
|
2796
|
+
|
|
2797
|
+
const searchText = searchInput ? searchInput.value.toLowerCase() : '';
|
|
2798
|
+
const typeValue = typeFilter ? typeFilter.value : '';
|
|
2799
|
+
|
|
2800
|
+
return fileOperations.filter(([filePath, fileData]) => {
|
|
2801
|
+
// Session filter - filter operations within each file
|
|
2802
|
+
if (this.selectedSessionId) {
|
|
2803
|
+
// Filter operations for this file by session
|
|
2804
|
+
const sessionOperations = fileData.operations.filter(op =>
|
|
2805
|
+
op.sessionId === this.selectedSessionId
|
|
2806
|
+
);
|
|
2807
|
+
|
|
2808
|
+
// If no operations from this session, exclude the file
|
|
2809
|
+
if (sessionOperations.length === 0) {
|
|
2810
|
+
return false;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// Update the fileData to only include session-specific operations
|
|
2814
|
+
// (Note: This creates a filtered view without modifying the original)
|
|
2815
|
+
fileData = {
|
|
2816
|
+
...fileData,
|
|
2817
|
+
operations: sessionOperations,
|
|
2818
|
+
lastOperation: sessionOperations[sessionOperations.length - 1]?.timestamp || fileData.lastOperation
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// Search filter
|
|
2823
|
+
if (searchText) {
|
|
2824
|
+
const searchableText = [
|
|
2825
|
+
filePath,
|
|
2826
|
+
...fileData.operations.map(op => op.operation),
|
|
2827
|
+
...fileData.operations.map(op => op.agent)
|
|
2828
|
+
].join(' ').toLowerCase();
|
|
2829
|
+
|
|
2830
|
+
if (!searchableText.includes(searchText)) {
|
|
2831
|
+
return false;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// Type filter
|
|
2836
|
+
if (typeValue) {
|
|
2837
|
+
const hasOperationType = fileData.operations.some(op => op.operation === typeValue);
|
|
2838
|
+
if (!hasOperationType) {
|
|
2839
|
+
return false;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
return true;
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
/**
|
|
2848
|
+
* Extract operation from event type
|
|
2849
|
+
*/
|
|
2850
|
+
extractOperation(eventType) {
|
|
2851
|
+
if (!eventType) return 'unknown';
|
|
2852
|
+
|
|
2853
|
+
if (eventType.includes('pre_')) return 'pre-' + eventType.split('pre_')[1];
|
|
2854
|
+
if (eventType.includes('post_')) return 'post-' + eventType.split('post_')[1];
|
|
2855
|
+
if (eventType.includes('delegation')) return 'delegation';
|
|
2856
|
+
if (eventType.includes('start')) return 'started';
|
|
2857
|
+
if (eventType.includes('end')) return 'ended';
|
|
2858
|
+
|
|
2859
|
+
// Extract operation from type like "hook.pre_tool" -> "pre_tool"
|
|
2860
|
+
const parts = eventType.split('.');
|
|
2861
|
+
return parts.length > 1 ? parts[1] : eventType;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
/**
|
|
2865
|
+
* Extract tool name from hook event type
|
|
2866
|
+
*/
|
|
2867
|
+
extractToolFromHook(eventType) {
|
|
2868
|
+
if (!eventType || !eventType.startsWith('hook.')) return null;
|
|
2869
|
+
|
|
2870
|
+
// For hook events, the tool name might be in the data
|
|
2871
|
+
return 'Tool'; // Fallback - actual tool name should be in event.tool_name
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
/**
|
|
2875
|
+
* Extract tool name from subtype
|
|
2876
|
+
*/
|
|
2877
|
+
extractToolFromSubtype(subtype) {
|
|
2878
|
+
if (!subtype) return null;
|
|
2879
|
+
|
|
2880
|
+
// Try to extract tool name from subtype patterns like 'pre_tool' or 'post_tool'
|
|
2881
|
+
if (subtype.includes('tool')) {
|
|
2882
|
+
return 'Tool'; // Generic fallback
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
return null;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
/**
|
|
2889
|
+
* Extract tool target for display
|
|
2890
|
+
*/
|
|
2891
|
+
extractToolTarget(toolName, params, toolParameters) {
|
|
2892
|
+
const allParams = { ...params, ...toolParameters };
|
|
2893
|
+
|
|
2894
|
+
switch (toolName) {
|
|
2895
|
+
case 'Read':
|
|
2896
|
+
case 'Write':
|
|
2897
|
+
case 'Edit':
|
|
2898
|
+
case 'MultiEdit':
|
|
2899
|
+
return allParams.file_path || 'Unknown file';
|
|
2900
|
+
case 'Bash':
|
|
2901
|
+
return allParams.command || 'Unknown command';
|
|
2902
|
+
case 'Glob':
|
|
2903
|
+
return allParams.pattern || 'Unknown pattern';
|
|
2904
|
+
case 'Grep':
|
|
2905
|
+
return `"${allParams.pattern || 'unknown'}" in ${allParams.path || 'unknown path'}`;
|
|
2906
|
+
case 'LS':
|
|
2907
|
+
return allParams.path || 'Unknown path';
|
|
2908
|
+
default:
|
|
2909
|
+
if (Object.keys(allParams).length > 0) {
|
|
2910
|
+
return JSON.stringify(allParams).substring(0, 50) + '...';
|
|
2911
|
+
}
|
|
2912
|
+
return 'No parameters';
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
/**
|
|
2917
|
+
* Get filtered events for a specific tab
|
|
2918
|
+
*/
|
|
2919
|
+
getFilteredEventsForTab(tabName) {
|
|
2920
|
+
// Use ALL events, not the EventViewer's filtered events
|
|
2921
|
+
// Each tab will apply its own filtering logic
|
|
2922
|
+
const events = this.eventViewer.events;
|
|
2923
|
+
console.log(`getFilteredEventsForTab(${tabName}) - using RAW events: ${events.length} total`);
|
|
2924
|
+
|
|
2925
|
+
// Enhanced debugging for empty events
|
|
2926
|
+
if (events.length === 0) {
|
|
2927
|
+
console.log(`❌ NO RAW EVENTS available!`);
|
|
2928
|
+
console.log('EventViewer state:', {
|
|
2929
|
+
total_events: this.eventViewer.events.length,
|
|
2930
|
+
filtered_events: this.eventViewer.filteredEvents.length,
|
|
2931
|
+
search_filter: this.eventViewer.searchFilter,
|
|
2932
|
+
type_filter: this.eventViewer.typeFilter,
|
|
2933
|
+
session_filter: this.eventViewer.sessionFilter
|
|
2934
|
+
});
|
|
2935
|
+
} else {
|
|
2936
|
+
console.log('✅ Raw events available for', tabName, '- sample:', events[0]);
|
|
2937
|
+
console.log('EventViewer filters (IGNORED for tabs):', {
|
|
2938
|
+
search_filter: this.eventViewer.searchFilter,
|
|
2939
|
+
type_filter: this.eventViewer.typeFilter,
|
|
2940
|
+
session_filter: this.eventViewer.sessionFilter
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
return events;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* Scroll a list container to the bottom
|
|
2949
|
+
* @param {string} listId - The ID of the list container element
|
|
2950
|
+
*/
|
|
2951
|
+
scrollListToBottom(listId) {
|
|
2952
|
+
console.log(`[DEBUG] scrollListToBottom called with listId: ${listId}`);
|
|
2953
|
+
|
|
2954
|
+
// Use setTimeout to ensure DOM updates are completed
|
|
2955
|
+
setTimeout(() => {
|
|
2956
|
+
const listElement = document.getElementById(listId);
|
|
2957
|
+
console.log(`[DEBUG] Element found for ${listId}:`, listElement);
|
|
2958
|
+
|
|
2959
|
+
if (listElement) {
|
|
2960
|
+
const scrollHeight = listElement.scrollHeight;
|
|
2961
|
+
const clientHeight = listElement.clientHeight;
|
|
2962
|
+
const currentScrollTop = listElement.scrollTop;
|
|
2963
|
+
const computedStyle = window.getComputedStyle(listElement);
|
|
2964
|
+
|
|
2965
|
+
console.log(`[DEBUG] Scroll metrics for ${listId}:`);
|
|
2966
|
+
console.log(` - scrollHeight: ${scrollHeight}`);
|
|
2967
|
+
console.log(` - clientHeight: ${clientHeight}`);
|
|
2968
|
+
console.log(` - currentScrollTop: ${currentScrollTop}`);
|
|
2969
|
+
console.log(` - needsScroll: ${scrollHeight > clientHeight}`);
|
|
2970
|
+
console.log(` - overflowY: ${computedStyle.overflowY}`);
|
|
2971
|
+
console.log(` - height: ${computedStyle.height}`);
|
|
2972
|
+
console.log(` - maxHeight: ${computedStyle.maxHeight}`);
|
|
2973
|
+
|
|
2974
|
+
if (scrollHeight > clientHeight) {
|
|
2975
|
+
// Method 1: Direct scrollTop assignment
|
|
2976
|
+
listElement.scrollTop = scrollHeight;
|
|
2977
|
+
console.log(`[DEBUG] Method 1 - Scroll applied to ${listId}, new scrollTop: ${listElement.scrollTop}`);
|
|
2978
|
+
|
|
2979
|
+
// Method 2: Force layout recalculation and try again if needed
|
|
2980
|
+
if (listElement.scrollTop !== scrollHeight) {
|
|
2981
|
+
console.log(`[DEBUG] Method 1 failed, trying method 2 with layout recalculation`);
|
|
2982
|
+
listElement.offsetHeight; // Force reflow
|
|
2983
|
+
listElement.scrollTop = scrollHeight;
|
|
2984
|
+
console.log(`[DEBUG] Method 2 - new scrollTop: ${listElement.scrollTop}`);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// Method 3: scrollIntoView on last element if still not working
|
|
2988
|
+
if (listElement.scrollTop < scrollHeight - clientHeight - 10) {
|
|
2989
|
+
console.log(`[DEBUG] Methods 1-2 failed, trying method 3 with scrollIntoView`);
|
|
2990
|
+
const lastElement = listElement.lastElementChild;
|
|
2991
|
+
if (lastElement) {
|
|
2992
|
+
lastElement.scrollIntoView({ behavior: 'instant', block: 'end' });
|
|
2993
|
+
console.log(`[DEBUG] Method 3 - scrollIntoView applied on last element`);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
} else {
|
|
2997
|
+
console.log(`[DEBUG] No scroll needed for ${listId} - content fits in container`);
|
|
2998
|
+
}
|
|
2999
|
+
} else {
|
|
3000
|
+
console.error(`[DEBUG] Element not found for ID: ${listId}`);
|
|
3001
|
+
// Log all elements with similar IDs to help debug
|
|
3002
|
+
const allElements = document.querySelectorAll('[id*="list"]');
|
|
3003
|
+
console.log(`[DEBUG] Available elements with 'list' in ID:`, Array.from(allElements).map(el => el.id));
|
|
3004
|
+
}
|
|
3005
|
+
}, 50); // Small delay to ensure content is rendered
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
/**
|
|
3009
|
+
* Test scroll functionality - adds dummy content and tries to scroll
|
|
3010
|
+
* This is for debugging purposes only
|
|
3011
|
+
* @returns {Promise<string>} Status message indicating test results
|
|
3012
|
+
*/
|
|
3013
|
+
async testScrollFunctionality() {
|
|
3014
|
+
console.log('[DEBUG] Testing scroll functionality...');
|
|
3015
|
+
const results = [];
|
|
3016
|
+
const listId = `${this.currentTab}-list`;
|
|
3017
|
+
const listElement = document.getElementById(listId);
|
|
3018
|
+
|
|
3019
|
+
if (!listElement) {
|
|
3020
|
+
const errorMsg = `❌ Could not find list element: ${listId}`;
|
|
3021
|
+
console.error(`[DEBUG] ${errorMsg}`);
|
|
3022
|
+
|
|
3023
|
+
// Debug: Show available elements
|
|
3024
|
+
const allElements = document.querySelectorAll('[id*="list"]');
|
|
3025
|
+
const availableIds = Array.from(allElements).map(el => el.id);
|
|
3026
|
+
console.log(`[DEBUG] Available elements with 'list' in ID:`, availableIds);
|
|
3027
|
+
results.push(errorMsg);
|
|
3028
|
+
results.push(`Available list IDs: ${availableIds.join(', ')}`);
|
|
3029
|
+
return results.join('\n');
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// Get initial state
|
|
3033
|
+
const initialScrollTop = listElement.scrollTop;
|
|
3034
|
+
const initialScrollHeight = listElement.scrollHeight;
|
|
3035
|
+
const initialClientHeight = listElement.clientHeight;
|
|
3036
|
+
const computedStyle = window.getComputedStyle(listElement);
|
|
3037
|
+
|
|
3038
|
+
results.push(`✅ Found list element: ${listId}`);
|
|
3039
|
+
results.push(`📊 Initial state: scrollTop=${initialScrollTop}, scrollHeight=${initialScrollHeight}, clientHeight=${initialClientHeight}`);
|
|
3040
|
+
results.push(`🎨 CSS: overflowY=${computedStyle.overflowY}, height=${computedStyle.height}, maxHeight=${computedStyle.maxHeight}`);
|
|
3041
|
+
|
|
3042
|
+
// Test 1: Direct scroll without adding content
|
|
3043
|
+
console.log('[DEBUG] Test 1: Direct scroll test');
|
|
3044
|
+
listElement.scrollTop = 999999;
|
|
3045
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
3046
|
+
const directScrollResult = listElement.scrollTop;
|
|
3047
|
+
results.push(`🧪 Test 1 - Direct scroll to 999999: scrollTop=${directScrollResult} ${directScrollResult > 0 ? '✅' : '❌'}`);
|
|
3048
|
+
|
|
3049
|
+
// Reset scroll position
|
|
3050
|
+
listElement.scrollTop = 0;
|
|
3051
|
+
|
|
3052
|
+
// Test 2: Add visible content to force scrolling need
|
|
3053
|
+
console.log('[DEBUG] Test 2: Adding test content');
|
|
3054
|
+
const originalContent = listElement.innerHTML;
|
|
3055
|
+
|
|
3056
|
+
// Add 15 large test items to definitely exceed container height
|
|
3057
|
+
for (let i = 0; i < 15; i++) {
|
|
3058
|
+
const testItem = document.createElement('div');
|
|
3059
|
+
testItem.className = 'event-item test-scroll-item';
|
|
3060
|
+
testItem.style.cssText = 'background: #fffacd !important; border: 2px solid #f39c12 !important; padding: 20px; margin: 10px 0; min-height: 60px;';
|
|
3061
|
+
testItem.innerHTML = `<strong>🧪 Test Item ${i + 1}</strong><br>This is a test item to verify scrolling functionality.<br><em>Item height: ~80px total</em>`;
|
|
3062
|
+
listElement.appendChild(testItem);
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// Force layout recalculation
|
|
3066
|
+
listElement.offsetHeight;
|
|
3067
|
+
|
|
3068
|
+
const afterContentScrollHeight = listElement.scrollHeight;
|
|
3069
|
+
const afterContentClientHeight = listElement.clientHeight;
|
|
3070
|
+
const needsScroll = afterContentScrollHeight > afterContentClientHeight;
|
|
3071
|
+
|
|
3072
|
+
results.push(`📦 Added 15 test items (expected ~1200px total height)`);
|
|
3073
|
+
results.push(`📊 After content: scrollHeight=${afterContentScrollHeight}, clientHeight=${afterContentClientHeight}`);
|
|
3074
|
+
results.push(`🔍 Needs scroll: ${needsScroll ? 'YES ✅' : 'NO ❌'}`);
|
|
3075
|
+
|
|
3076
|
+
if (needsScroll) {
|
|
3077
|
+
// Test 3: Multiple scroll methods
|
|
3078
|
+
console.log('[DEBUG] Test 3: Testing different scroll methods');
|
|
3079
|
+
|
|
3080
|
+
// Method A: Direct scrollTop assignment
|
|
3081
|
+
listElement.scrollTop = afterContentScrollHeight;
|
|
3082
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
3083
|
+
const methodAResult = listElement.scrollTop;
|
|
3084
|
+
const methodASuccess = methodAResult > (afterContentScrollHeight - afterContentClientHeight - 50);
|
|
3085
|
+
results.push(`🔧 Method A - scrollTop assignment: ${methodAResult} ${methodASuccess ? '✅' : '❌'}`);
|
|
3086
|
+
|
|
3087
|
+
// Method B: scrollIntoView on last element
|
|
3088
|
+
const lastElement = listElement.lastElementChild;
|
|
3089
|
+
if (lastElement) {
|
|
3090
|
+
lastElement.scrollIntoView({ behavior: 'instant', block: 'end' });
|
|
3091
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
3092
|
+
const methodBResult = listElement.scrollTop;
|
|
3093
|
+
const methodBSuccess = methodBResult > (afterContentScrollHeight - afterContentClientHeight - 50);
|
|
3094
|
+
results.push(`🔧 Method B - scrollIntoView: ${methodBResult} ${methodBSuccess ? '✅' : '❌'}`);
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// Method C: Using the existing scrollListToBottom method
|
|
3098
|
+
console.log('[DEBUG] Test 4: Using scrollListToBottom method');
|
|
3099
|
+
this.scrollListToBottom(listId);
|
|
3100
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
3101
|
+
const methodCResult = listElement.scrollTop;
|
|
3102
|
+
const methodCSuccess = methodCResult > (afterContentScrollHeight - afterContentClientHeight - 50);
|
|
3103
|
+
results.push(`🔧 Method C - scrollListToBottom: ${methodCResult} ${methodCSuccess ? '✅' : '❌'}`);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// Clean up test content after a visible delay
|
|
3107
|
+
setTimeout(() => {
|
|
3108
|
+
console.log('[DEBUG] Cleaning up test content...');
|
|
3109
|
+
const testItems = listElement.querySelectorAll('.test-scroll-item');
|
|
3110
|
+
testItems.forEach(item => item.remove());
|
|
3111
|
+
results.push(`🧹 Cleaned up ${testItems.length} test items`);
|
|
3112
|
+
}, 2000);
|
|
3113
|
+
|
|
3114
|
+
const finalResult = results.join('\n');
|
|
3115
|
+
console.log('[DEBUG] Test complete. Results:', finalResult);
|
|
3116
|
+
return finalResult;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* Update connection status in UI
|
|
3121
|
+
*/
|
|
3122
|
+
updateConnectionStatus(status, type) {
|
|
3123
|
+
const statusElement = document.getElementById('connection-status');
|
|
3124
|
+
if (statusElement) {
|
|
3125
|
+
statusElement.textContent = status;
|
|
3126
|
+
statusElement.className = `status-badge status-${type}`;
|
|
3127
|
+
|
|
3128
|
+
// Update status indicator
|
|
3129
|
+
const indicator = statusElement.querySelector('span');
|
|
3130
|
+
if (indicator) {
|
|
3131
|
+
indicator.textContent = type === 'connected' ? '●' : '●';
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
/**
|
|
3137
|
+
* Clear all events
|
|
3138
|
+
*/
|
|
3139
|
+
clearEvents() {
|
|
3140
|
+
this.eventViewer.clearEvents();
|
|
3141
|
+
this.fileOperations.clear();
|
|
3142
|
+
this.toolCalls.clear();
|
|
3143
|
+
this.agentEvents = [];
|
|
3144
|
+
this.renderCurrentTab();
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
/**
|
|
3148
|
+
* Export current events
|
|
3149
|
+
*/
|
|
3150
|
+
exportEvents() {
|
|
3151
|
+
this.eventViewer.exportEvents();
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
/**
|
|
3155
|
+
* Toggle connection controls visibility
|
|
3156
|
+
*/
|
|
3157
|
+
toggleConnectionControls() {
|
|
3158
|
+
const controlsRow = document.getElementById('connection-controls-row');
|
|
3159
|
+
const toggleBtn = document.getElementById('connection-toggle-btn');
|
|
3160
|
+
|
|
3161
|
+
if (controlsRow && toggleBtn) {
|
|
3162
|
+
const isVisible = controlsRow.classList.contains('show');
|
|
3163
|
+
|
|
3164
|
+
if (isVisible) {
|
|
3165
|
+
controlsRow.classList.remove('show');
|
|
3166
|
+
controlsRow.style.display = 'none';
|
|
3167
|
+
toggleBtn.textContent = 'Connection Settings';
|
|
3168
|
+
} else {
|
|
3169
|
+
controlsRow.style.display = 'flex';
|
|
3170
|
+
// Use setTimeout to ensure display change is applied before adding animation class
|
|
3171
|
+
setTimeout(() => {
|
|
3172
|
+
controlsRow.classList.add('show');
|
|
3173
|
+
}, 10);
|
|
3174
|
+
toggleBtn.textContent = 'Hide Connection Settings';
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
/**
|
|
3180
|
+
* Clear current selection
|
|
3181
|
+
*/
|
|
3182
|
+
clearSelection() {
|
|
3183
|
+
this.clearCardSelection();
|
|
3184
|
+
this.eventViewer.clearSelection();
|
|
3185
|
+
this.moduleViewer.clear();
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
/**
|
|
3189
|
+
* Show dialog to change working directory
|
|
3190
|
+
*/
|
|
3191
|
+
showChangeDirDialog() {
|
|
3192
|
+
const currentDir = this.currentWorkingDir || this.getDefaultWorkingDir();
|
|
3193
|
+
const newDir = prompt('Enter new working directory:', currentDir);
|
|
3194
|
+
|
|
3195
|
+
if (newDir && newDir !== currentDir) {
|
|
3196
|
+
this.setWorkingDirectory(newDir);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
/**
|
|
3201
|
+
* Set the working directory for the current session
|
|
3202
|
+
* @param {string} dir - New working directory path
|
|
3203
|
+
*/
|
|
3204
|
+
setWorkingDirectory(dir) {
|
|
3205
|
+
this.currentWorkingDir = dir;
|
|
3206
|
+
|
|
3207
|
+
// Update UI
|
|
3208
|
+
const pathElement = document.getElementById('working-dir-path');
|
|
3209
|
+
if (pathElement) {
|
|
3210
|
+
pathElement.textContent = dir;
|
|
3211
|
+
pathElement.title = `Click to change from ${dir}`;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
// Update footer (with flag to prevent observer loop)
|
|
3215
|
+
const footerDir = document.getElementById('footer-working-dir');
|
|
3216
|
+
if (footerDir) {
|
|
3217
|
+
this._updatingFooter = true;
|
|
3218
|
+
footerDir.textContent = dir;
|
|
3219
|
+
// Reset flag after a small delay to ensure observer has processed
|
|
3220
|
+
setTimeout(() => {
|
|
3221
|
+
this._updatingFooter = false;
|
|
3222
|
+
}, 10);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// Store in session data if a session is selected
|
|
3226
|
+
const sessionSelect = document.getElementById('session-select');
|
|
3227
|
+
if (sessionSelect && sessionSelect.value) {
|
|
3228
|
+
const sessionId = sessionSelect.value;
|
|
3229
|
+
// Store working directory per session in localStorage
|
|
3230
|
+
const sessionDirs = JSON.parse(localStorage.getItem('sessionWorkingDirs') || '{}');
|
|
3231
|
+
sessionDirs[sessionId] = dir;
|
|
3232
|
+
localStorage.setItem('sessionWorkingDirs', JSON.stringify(sessionDirs));
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
console.log(`Working directory set to: ${dir}`);
|
|
3236
|
+
|
|
3237
|
+
// Request git branch for the new directory
|
|
3238
|
+
this.updateGitBranch(dir);
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
/**
|
|
3242
|
+
* Update git branch display for current working directory
|
|
3243
|
+
* @param {string} dir - Working directory path
|
|
3244
|
+
*/
|
|
3245
|
+
updateGitBranch(dir) {
|
|
3246
|
+
if (!this.socketClient || !this.socketClient.socket || !this.socketClient.socket.connected) {
|
|
3247
|
+
// Not connected, set to unknown
|
|
3248
|
+
const footerBranch = document.getElementById('footer-git-branch');
|
|
3249
|
+
if (footerBranch) {
|
|
3250
|
+
footerBranch.textContent = 'Not Connected';
|
|
3251
|
+
}
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// Request git branch from server
|
|
3256
|
+
this.socketClient.socket.emit('get_git_branch', dir);
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
/**
|
|
3260
|
+
* Get default working directory
|
|
3261
|
+
*/
|
|
3262
|
+
getDefaultWorkingDir() {
|
|
3263
|
+
// Try to get from footer first (may be set by server)
|
|
3264
|
+
const footerDir = document.getElementById('footer-working-dir');
|
|
3265
|
+
if (footerDir && footerDir.textContent && footerDir.textContent !== 'Unknown') {
|
|
3266
|
+
return footerDir.textContent;
|
|
3267
|
+
}
|
|
3268
|
+
// Fallback to hardcoded default
|
|
3269
|
+
return '/Users/masa/Projects/claude-mpm';
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
/**
|
|
3273
|
+
* Initialize working directory on dashboard load
|
|
3274
|
+
*/
|
|
3275
|
+
initializeWorkingDirectory() {
|
|
3276
|
+
// Check if there's a selected session
|
|
3277
|
+
const sessionSelect = document.getElementById('session-select');
|
|
3278
|
+
if (sessionSelect && sessionSelect.value) {
|
|
3279
|
+
// Load working directory for selected session
|
|
3280
|
+
this.loadWorkingDirectoryForSession(sessionSelect.value);
|
|
3281
|
+
} else {
|
|
3282
|
+
// Set default working directory
|
|
3283
|
+
this.setWorkingDirectory(this.getDefaultWorkingDir());
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
/**
|
|
3288
|
+
* Watch footer directory for changes and sync working directory
|
|
3289
|
+
*/
|
|
3290
|
+
watchFooterDirectory() {
|
|
3291
|
+
const footerDir = document.getElementById('footer-working-dir');
|
|
3292
|
+
if (!footerDir) return;
|
|
3293
|
+
|
|
3294
|
+
// Store observer reference for later use
|
|
3295
|
+
this.footerDirObserver = new MutationObserver((mutations) => {
|
|
3296
|
+
// Skip if we're updating from setWorkingDirectory
|
|
3297
|
+
if (this._updatingFooter) return;
|
|
3298
|
+
|
|
3299
|
+
mutations.forEach((mutation) => {
|
|
3300
|
+
if (mutation.type === 'childList' || mutation.type === 'characterData') {
|
|
3301
|
+
const newDir = footerDir.textContent.trim();
|
|
3302
|
+
// Only update if it's a valid directory path and different from current
|
|
3303
|
+
if (newDir &&
|
|
3304
|
+
newDir !== 'Unknown' &&
|
|
3305
|
+
newDir !== 'Not Connected' &&
|
|
3306
|
+
newDir.startsWith('/') &&
|
|
3307
|
+
newDir !== this.currentWorkingDir) {
|
|
3308
|
+
console.log(`Footer directory changed to: ${newDir}, syncing working directory`);
|
|
3309
|
+
this.setWorkingDirectory(newDir);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
});
|
|
3313
|
+
});
|
|
3314
|
+
|
|
3315
|
+
// Start observing
|
|
3316
|
+
this.footerDirObserver.observe(footerDir, {
|
|
3317
|
+
childList: true,
|
|
3318
|
+
characterData: true,
|
|
3319
|
+
subtree: true
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
/**
|
|
3324
|
+
* Load working directory for a session
|
|
3325
|
+
* @param {string} sessionId - Session ID
|
|
3326
|
+
*/
|
|
3327
|
+
loadWorkingDirectoryForSession(sessionId) {
|
|
3328
|
+
if (!sessionId) {
|
|
3329
|
+
// No session selected, use default
|
|
3330
|
+
this.setWorkingDirectory(this.getDefaultWorkingDir());
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// Load from localStorage
|
|
3335
|
+
const sessionDirs = JSON.parse(localStorage.getItem('sessionWorkingDirs') || '{}');
|
|
3336
|
+
const dir = sessionDirs[sessionId] || this.getDefaultWorkingDir();
|
|
3337
|
+
this.setWorkingDirectory(dir);
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
/**
|
|
3341
|
+
* Toggle HUD visualizer mode
|
|
3342
|
+
*/
|
|
3343
|
+
toggleHUD() {
|
|
3344
|
+
if (!this.isSessionSelected()) {
|
|
3345
|
+
console.log('Cannot toggle HUD: No session selected');
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
this.hudMode = !this.hudMode;
|
|
3350
|
+
this.updateHUDDisplay();
|
|
3351
|
+
|
|
3352
|
+
console.log('HUD mode toggled:', this.hudMode ? 'ON' : 'OFF');
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
/**
|
|
3356
|
+
* Check if a session is currently selected
|
|
3357
|
+
* @returns {boolean} - True if session is selected
|
|
3358
|
+
*/
|
|
3359
|
+
isSessionSelected() {
|
|
3360
|
+
const sessionSelect = document.getElementById('session-select');
|
|
3361
|
+
return sessionSelect && sessionSelect.value && sessionSelect.value !== '';
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
/**
|
|
3365
|
+
* Update HUD display based on current mode
|
|
3366
|
+
*/
|
|
3367
|
+
updateHUDDisplay() {
|
|
3368
|
+
const eventsWrapper = document.querySelector('.events-wrapper');
|
|
3369
|
+
const hudToggleBtn = document.getElementById('hud-toggle-btn');
|
|
3370
|
+
|
|
3371
|
+
if (!eventsWrapper || !hudToggleBtn) return;
|
|
3372
|
+
|
|
3373
|
+
if (this.hudMode) {
|
|
3374
|
+
// Switch to HUD mode
|
|
3375
|
+
eventsWrapper.classList.add('hud-mode');
|
|
3376
|
+
hudToggleBtn.classList.add('btn-hud-active');
|
|
3377
|
+
hudToggleBtn.textContent = 'Normal View';
|
|
3378
|
+
|
|
3379
|
+
// Activate the HUD visualizer (with lazy loading)
|
|
3380
|
+
if (this.hudVisualizer) {
|
|
3381
|
+
this.hudVisualizer.activate().then(() => {
|
|
3382
|
+
// Process existing events after libraries are loaded
|
|
3383
|
+
this.processExistingEventsForHUD();
|
|
3384
|
+
}).catch((error) => {
|
|
3385
|
+
console.error('Failed to activate HUD:', error);
|
|
3386
|
+
// Optionally revert HUD mode on failure
|
|
3387
|
+
// this.hudMode = false;
|
|
3388
|
+
// this.updateHUDDisplay();
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
} else {
|
|
3392
|
+
// Switch to normal mode
|
|
3393
|
+
eventsWrapper.classList.remove('hud-mode');
|
|
3394
|
+
hudToggleBtn.classList.remove('btn-hud-active');
|
|
3395
|
+
hudToggleBtn.textContent = 'HUD';
|
|
3396
|
+
|
|
3397
|
+
// Deactivate the HUD visualizer
|
|
3398
|
+
if (this.hudVisualizer) {
|
|
3399
|
+
this.hudVisualizer.deactivate();
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
/**
|
|
3405
|
+
* Update HUD button state based on session selection
|
|
3406
|
+
*/
|
|
3407
|
+
updateHUDButtonState() {
|
|
3408
|
+
const hudToggleBtn = document.getElementById('hud-toggle-btn');
|
|
3409
|
+
if (!hudToggleBtn) return;
|
|
3410
|
+
|
|
3411
|
+
if (this.isSessionSelected()) {
|
|
3412
|
+
hudToggleBtn.disabled = false;
|
|
3413
|
+
hudToggleBtn.title = 'Toggle HUD visualizer';
|
|
3414
|
+
} else {
|
|
3415
|
+
hudToggleBtn.disabled = true;
|
|
3416
|
+
hudToggleBtn.title = 'Select a session to enable HUD';
|
|
3417
|
+
|
|
3418
|
+
// If HUD is currently active, turn it off
|
|
3419
|
+
if (this.hudMode) {
|
|
3420
|
+
this.hudMode = false;
|
|
3421
|
+
this.updateHUDDisplay();
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
/**
|
|
3427
|
+
* Process existing events for HUD visualization
|
|
3428
|
+
*/
|
|
3429
|
+
processExistingEventsForHUD() {
|
|
3430
|
+
if (!this.hudVisualizer || !this.eventViewer) return;
|
|
3431
|
+
|
|
3432
|
+
console.log('🔄 Processing existing events for HUD visualization...');
|
|
3433
|
+
|
|
3434
|
+
// Clear existing visualization
|
|
3435
|
+
this.hudVisualizer.clear();
|
|
3436
|
+
|
|
3437
|
+
// Get all events (not just filtered ones) to build complete tree structure
|
|
3438
|
+
const allEvents = this.eventViewer.getAllEvents();
|
|
3439
|
+
|
|
3440
|
+
if (allEvents.length === 0) {
|
|
3441
|
+
console.log('❌ No events available for HUD visualization');
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
// Sort events chronologically to ensure proper tree building
|
|
3446
|
+
const sortedEvents = [...allEvents].sort((a, b) => {
|
|
3447
|
+
const timeA = new Date(a.timestamp).getTime();
|
|
3448
|
+
const timeB = new Date(b.timestamp).getTime();
|
|
3449
|
+
return timeA - timeB;
|
|
3450
|
+
});
|
|
3451
|
+
|
|
3452
|
+
console.log(`📊 Processing ${sortedEvents.length} events chronologically for HUD:`);
|
|
3453
|
+
console.log(` • Earliest: ${new Date(sortedEvents[0].timestamp).toLocaleString()}`);
|
|
3454
|
+
console.log(` • Latest: ${new Date(sortedEvents[sortedEvents.length - 1].timestamp).toLocaleString()}`);
|
|
3455
|
+
|
|
3456
|
+
// Process events with enhanced hierarchy building
|
|
3457
|
+
this.hudVisualizer.processExistingEvents(sortedEvents);
|
|
3458
|
+
|
|
3459
|
+
console.log(`✅ Processed ${sortedEvents.length} events for HUD visualization`);
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
/**
|
|
3463
|
+
* Handle new socket events for HUD
|
|
3464
|
+
* @param {Object} event - Socket event data
|
|
3465
|
+
*/
|
|
3466
|
+
handleHUDEvent(event) {
|
|
3467
|
+
if (this.hudMode && this.hudVisualizer) {
|
|
3468
|
+
this.hudVisualizer.processEvent(event);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
// Global functions for backward compatibility
|
|
3474
|
+
window.connectSocket = function() {
|
|
3475
|
+
if (window.dashboard) {
|
|
3476
|
+
const port = document.getElementById('port-input')?.value || '8765';
|
|
3477
|
+
window.dashboard.socketClient.connect(port);
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
|
|
3481
|
+
window.disconnectSocket = function() {
|
|
3482
|
+
if (window.dashboard) {
|
|
3483
|
+
window.dashboard.socketClient.disconnect();
|
|
3484
|
+
}
|
|
3485
|
+
};
|
|
3486
|
+
|
|
3487
|
+
window.clearEvents = function() {
|
|
3488
|
+
if (window.dashboard) {
|
|
3489
|
+
window.dashboard.clearEvents();
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
|
|
3493
|
+
window.exportEvents = function() {
|
|
3494
|
+
if (window.dashboard) {
|
|
3495
|
+
window.dashboard.exportEvents();
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
|
|
3499
|
+
window.clearSelection = function() {
|
|
3500
|
+
if (window.dashboard) {
|
|
3501
|
+
window.dashboard.clearSelection();
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3504
|
+
|
|
3505
|
+
window.switchTab = function(tabName) {
|
|
3506
|
+
if (window.dashboard) {
|
|
3507
|
+
window.dashboard.switchTab(tabName);
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
|
|
3511
|
+
// Detail view functions
|
|
3512
|
+
window.showAgentDetailsByIndex = function(index) {
|
|
3513
|
+
if (window.dashboard) {
|
|
3514
|
+
window.dashboard.showAgentDetailsByIndex(index);
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
|
|
3518
|
+
window.showToolCallDetails = function(toolCallKey) {
|
|
3519
|
+
if (window.dashboard) {
|
|
3520
|
+
window.dashboard.showToolCallDetails(toolCallKey);
|
|
3521
|
+
}
|
|
3522
|
+
};
|
|
3523
|
+
|
|
3524
|
+
window.showFileDetails = function(filePath) {
|
|
3525
|
+
if (window.dashboard) {
|
|
3526
|
+
window.dashboard.showFileDetails(filePath);
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
|
|
3530
|
+
// Debug function for testing scroll functionality
|
|
3531
|
+
window.testScroll = async function() {
|
|
3532
|
+
if (window.dashboard) {
|
|
3533
|
+
console.log('🧪 Starting scroll functionality test...');
|
|
3534
|
+
try {
|
|
3535
|
+
const result = await window.dashboard.testScrollFunctionality();
|
|
3536
|
+
console.log('📋 Test results:\n' + result);
|
|
3537
|
+
|
|
3538
|
+
// Also display results in an alert for easier viewing
|
|
3539
|
+
alert('Scroll Test Results:\n\n' + result);
|
|
3540
|
+
|
|
3541
|
+
return result;
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
const errorMsg = `❌ Test failed with error: ${error.message}`;
|
|
3544
|
+
console.error(errorMsg, error);
|
|
3545
|
+
alert(errorMsg);
|
|
3546
|
+
return errorMsg;
|
|
3547
|
+
}
|
|
3548
|
+
} else {
|
|
3549
|
+
const errorMsg = '❌ Dashboard not initialized';
|
|
3550
|
+
console.error(errorMsg);
|
|
3551
|
+
alert(errorMsg);
|
|
3552
|
+
return errorMsg;
|
|
3553
|
+
}
|
|
3554
|
+
};
|
|
3555
|
+
|
|
3556
|
+
// Simple direct scroll test function
|
|
3557
|
+
window.testDirectScroll = function() {
|
|
3558
|
+
if (!window.dashboard) {
|
|
3559
|
+
console.error('❌ Dashboard not initialized');
|
|
3560
|
+
return 'Dashboard not initialized';
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
const currentTab = window.dashboard.currentTab;
|
|
3564
|
+
const listId = `${currentTab}-list`;
|
|
3565
|
+
const element = document.getElementById(listId);
|
|
3566
|
+
|
|
3567
|
+
if (!element) {
|
|
3568
|
+
const msg = `❌ Element ${listId} not found`;
|
|
3569
|
+
console.error(msg);
|
|
3570
|
+
return msg;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
console.log(`🎯 Direct scroll test on ${listId}`);
|
|
3574
|
+
console.log(`Before: scrollTop=${element.scrollTop}, scrollHeight=${element.scrollHeight}, clientHeight=${element.clientHeight}`);
|
|
3575
|
+
|
|
3576
|
+
// Try direct assignment to maximum scroll
|
|
3577
|
+
element.scrollTop = 999999;
|
|
3578
|
+
|
|
3579
|
+
setTimeout(() => {
|
|
3580
|
+
console.log(`After: scrollTop=${element.scrollTop}, scrollHeight=${element.scrollHeight}, clientHeight=${element.clientHeight}`);
|
|
3581
|
+
const success = element.scrollTop > 0 || element.scrollHeight <= element.clientHeight;
|
|
3582
|
+
const result = `${success ? '✅' : '❌'} Direct scroll test: scrollTop=${element.scrollTop}`;
|
|
3583
|
+
console.log(result);
|
|
3584
|
+
alert(result);
|
|
3585
|
+
return result;
|
|
3586
|
+
}, 50);
|
|
3587
|
+
|
|
3588
|
+
return 'Test running...';
|
|
3589
|
+
};
|
|
3590
|
+
|
|
3591
|
+
// CSS layout diagnostic function
|
|
3592
|
+
window.diagnoseCSSLayout = function() {
|
|
3593
|
+
if (!window.dashboard) {
|
|
3594
|
+
return 'Dashboard not initialized';
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
const currentTab = window.dashboard.currentTab;
|
|
3598
|
+
const listId = `${currentTab}-list`;
|
|
3599
|
+
const results = [];
|
|
3600
|
+
|
|
3601
|
+
// Check the full hierarchy
|
|
3602
|
+
const containers = [
|
|
3603
|
+
'events-wrapper',
|
|
3604
|
+
'events-container',
|
|
3605
|
+
`${currentTab}-tab`,
|
|
3606
|
+
listId
|
|
3607
|
+
];
|
|
3608
|
+
|
|
3609
|
+
results.push(`🔍 CSS Layout Diagnosis for ${currentTab} tab:`);
|
|
3610
|
+
results.push('');
|
|
3611
|
+
|
|
3612
|
+
containers.forEach(id => {
|
|
3613
|
+
const element = document.getElementById(id);
|
|
3614
|
+
if (element) {
|
|
3615
|
+
const computed = window.getComputedStyle(element);
|
|
3616
|
+
const rect = element.getBoundingClientRect();
|
|
3617
|
+
|
|
3618
|
+
results.push(`📦 ${id}:`);
|
|
3619
|
+
results.push(` Display: ${computed.display}`);
|
|
3620
|
+
results.push(` Position: ${computed.position}`);
|
|
3621
|
+
results.push(` Width: ${computed.width} (${rect.width}px)`);
|
|
3622
|
+
results.push(` Height: ${computed.height} (${rect.height}px)`);
|
|
3623
|
+
results.push(` Max-height: ${computed.maxHeight}`);
|
|
3624
|
+
results.push(` Overflow-Y: ${computed.overflowY}`);
|
|
3625
|
+
results.push(` Flex: ${computed.flex}`);
|
|
3626
|
+
results.push(` Flex-direction: ${computed.flexDirection}`);
|
|
3627
|
+
|
|
3628
|
+
if (element.scrollHeight !== element.clientHeight) {
|
|
3629
|
+
results.push(` 📊 ScrollHeight: ${element.scrollHeight}, ClientHeight: ${element.clientHeight}`);
|
|
3630
|
+
results.push(` 📊 ScrollTop: ${element.scrollTop} (can scroll: ${element.scrollHeight > element.clientHeight})`);
|
|
3631
|
+
}
|
|
3632
|
+
results.push('');
|
|
3633
|
+
} else {
|
|
3634
|
+
results.push(`❌ ${id}: Not found`);
|
|
3635
|
+
results.push('');
|
|
3636
|
+
}
|
|
3637
|
+
});
|
|
3638
|
+
|
|
3639
|
+
const diagnosis = results.join('\n');
|
|
3640
|
+
console.log(diagnosis);
|
|
3641
|
+
alert(diagnosis);
|
|
3642
|
+
return diagnosis;
|
|
3643
|
+
};
|
|
3644
|
+
|
|
3645
|
+
// Run all scroll diagnostics
|
|
3646
|
+
window.runScrollDiagnostics = async function() {
|
|
3647
|
+
console.log('🔬 Running complete scroll diagnostics...');
|
|
3648
|
+
|
|
3649
|
+
// Step 1: CSS Layout diagnosis
|
|
3650
|
+
console.log('\n=== STEP 1: CSS Layout Diagnosis ===');
|
|
3651
|
+
const cssResult = window.diagnoseCSSLayout();
|
|
3652
|
+
|
|
3653
|
+
// Step 2: Direct scroll test
|
|
3654
|
+
console.log('\n=== STEP 2: Direct Scroll Test ===');
|
|
3655
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
3656
|
+
const directResult = window.testDirectScroll();
|
|
3657
|
+
|
|
3658
|
+
// Step 3: Full scroll functionality test
|
|
3659
|
+
console.log('\n=== STEP 3: Full Scroll Functionality Test ===');
|
|
3660
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
3661
|
+
const fullResult = await window.testScroll();
|
|
3662
|
+
|
|
3663
|
+
const summary = `
|
|
3664
|
+
🔬 SCROLL DIAGNOSTICS COMPLETE
|
|
3665
|
+
===============================
|
|
3666
|
+
|
|
3667
|
+
Step 1 - CSS Layout: See console for details
|
|
3668
|
+
Step 2 - Direct Scroll: ${directResult}
|
|
3669
|
+
Step 3 - Full Test: See alert for details
|
|
3670
|
+
|
|
3671
|
+
Check browser console for complete logs.
|
|
3672
|
+
`;
|
|
3673
|
+
|
|
3674
|
+
console.log(summary);
|
|
3675
|
+
return summary;
|
|
3676
|
+
};
|
|
3677
|
+
|
|
3678
|
+
// Git Diff Modal Functions
|
|
3679
|
+
window.showGitDiffModal = function(filePath, timestamp, workingDir) {
|
|
3680
|
+
// Use the dashboard's current working directory if not provided
|
|
3681
|
+
if (!workingDir && window.dashboard && window.dashboard.currentWorkingDir) {
|
|
3682
|
+
workingDir = window.dashboard.currentWorkingDir;
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
// Create modal if it doesn't exist
|
|
3686
|
+
let modal = document.getElementById('git-diff-modal');
|
|
3687
|
+
if (!modal) {
|
|
3688
|
+
modal = createGitDiffModal();
|
|
3689
|
+
document.body.appendChild(modal);
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// Update modal content
|
|
3693
|
+
updateGitDiffModal(modal, filePath, timestamp, workingDir);
|
|
3694
|
+
|
|
3695
|
+
// Show the modal as flex container
|
|
3696
|
+
modal.style.display = 'flex';
|
|
3697
|
+
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
|
3698
|
+
};
|
|
3699
|
+
|
|
3700
|
+
window.hideGitDiffModal = function() {
|
|
3701
|
+
const modal = document.getElementById('git-diff-modal');
|
|
3702
|
+
if (modal) {
|
|
3703
|
+
modal.style.display = 'none';
|
|
3704
|
+
document.body.style.overflow = ''; // Restore background scrolling
|
|
3705
|
+
}
|
|
3706
|
+
};
|
|
3707
|
+
|
|
3708
|
+
function createGitDiffModal() {
|
|
3709
|
+
const modal = document.createElement('div');
|
|
3710
|
+
modal.id = 'git-diff-modal';
|
|
3711
|
+
modal.className = 'modal git-diff-modal';
|
|
3712
|
+
|
|
3713
|
+
modal.innerHTML = `
|
|
3714
|
+
<div class="modal-content git-diff-content">
|
|
3715
|
+
<div class="git-diff-header">
|
|
3716
|
+
<h2 class="git-diff-title">
|
|
3717
|
+
<span class="git-diff-icon">📋</span>
|
|
3718
|
+
<span class="git-diff-title-text">Git Diff</span>
|
|
3719
|
+
</h2>
|
|
3720
|
+
<div class="git-diff-meta">
|
|
3721
|
+
<span class="git-diff-file-path"></span>
|
|
3722
|
+
<span class="git-diff-timestamp"></span>
|
|
3723
|
+
</div>
|
|
3724
|
+
<button class="git-diff-close" onclick="hideGitDiffModal()">
|
|
3725
|
+
<span>×</span>
|
|
3726
|
+
</button>
|
|
3727
|
+
</div>
|
|
3728
|
+
<div class="git-diff-body">
|
|
3729
|
+
<div class="git-diff-loading">
|
|
3730
|
+
<div class="loading-spinner"></div>
|
|
3731
|
+
<span>Loading git diff...</span>
|
|
3732
|
+
</div>
|
|
3733
|
+
<div class="git-diff-error" style="display: none;">
|
|
3734
|
+
<div class="error-icon">⚠️</div>
|
|
3735
|
+
<div class="error-message"></div>
|
|
3736
|
+
<div class="error-suggestions"></div>
|
|
3737
|
+
</div>
|
|
3738
|
+
<div class="git-diff-content-area" style="display: none;">
|
|
3739
|
+
<div class="git-diff-toolbar">
|
|
3740
|
+
<div class="git-diff-info">
|
|
3741
|
+
<span class="commit-hash"></span>
|
|
3742
|
+
<span class="diff-method"></span>
|
|
3743
|
+
</div>
|
|
3744
|
+
<div class="git-diff-actions">
|
|
3745
|
+
<button class="git-diff-copy" onclick="copyGitDiff()">
|
|
3746
|
+
📋 Copy
|
|
3747
|
+
</button>
|
|
3748
|
+
</div>
|
|
3749
|
+
</div>
|
|
3750
|
+
<div class="git-diff-scroll-wrapper">
|
|
3751
|
+
<pre class="git-diff-display"><code class="git-diff-code"></code></pre>
|
|
3752
|
+
</div>
|
|
3753
|
+
</div>
|
|
3754
|
+
</div>
|
|
3755
|
+
</div>
|
|
3756
|
+
`;
|
|
3757
|
+
|
|
3758
|
+
// Close modal when clicking outside
|
|
3759
|
+
modal.addEventListener('click', (e) => {
|
|
3760
|
+
if (e.target === modal) {
|
|
3761
|
+
hideGitDiffModal();
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
// Close modal with Escape key
|
|
3766
|
+
document.addEventListener('keydown', (e) => {
|
|
3767
|
+
if (e.key === 'Escape' && modal.style.display === 'block') {
|
|
3768
|
+
hideGitDiffModal();
|
|
3769
|
+
}
|
|
3770
|
+
});
|
|
3771
|
+
|
|
3772
|
+
return modal;
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
async function updateGitDiffModal(modal, filePath, timestamp, workingDir) {
|
|
3776
|
+
// Update header info
|
|
3777
|
+
const filePathElement = modal.querySelector('.git-diff-file-path');
|
|
3778
|
+
const timestampElement = modal.querySelector('.git-diff-timestamp');
|
|
3779
|
+
|
|
3780
|
+
filePathElement.textContent = filePath;
|
|
3781
|
+
timestampElement.textContent = timestamp ? new Date(timestamp).toLocaleString() : 'Latest';
|
|
3782
|
+
|
|
3783
|
+
// Show loading state
|
|
3784
|
+
modal.querySelector('.git-diff-loading').style.display = 'flex';
|
|
3785
|
+
modal.querySelector('.git-diff-error').style.display = 'none';
|
|
3786
|
+
modal.querySelector('.git-diff-content-area').style.display = 'none';
|
|
3787
|
+
|
|
3788
|
+
try {
|
|
3789
|
+
// Get the Socket.IO server port with multiple fallbacks
|
|
3790
|
+
let port = 8765; // Default fallback
|
|
3791
|
+
|
|
3792
|
+
// Try to get port from socketClient first
|
|
3793
|
+
if (window.dashboard && window.dashboard.socketClient && window.dashboard.socketClient.port) {
|
|
3794
|
+
port = window.dashboard.socketClient.port;
|
|
3795
|
+
}
|
|
3796
|
+
// Fallback to port input field if socketClient port is not available
|
|
3797
|
+
else {
|
|
3798
|
+
const portInput = document.getElementById('port-input');
|
|
3799
|
+
if (portInput && portInput.value) {
|
|
3800
|
+
port = portInput.value;
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
|
|
3805
|
+
// Build URL parameters
|
|
3806
|
+
const params = new URLSearchParams({
|
|
3807
|
+
file: filePath
|
|
3808
|
+
});
|
|
3809
|
+
|
|
3810
|
+
if (timestamp) {
|
|
3811
|
+
params.append('timestamp', timestamp);
|
|
3812
|
+
}
|
|
3813
|
+
if (workingDir) {
|
|
3814
|
+
params.append('working_dir', workingDir);
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
const requestUrl = `http://localhost:${port}/api/git-diff?${params}`;
|
|
3818
|
+
console.log('🌐 Making git diff request to:', requestUrl);
|
|
3819
|
+
|
|
3820
|
+
// Test server connectivity first
|
|
3821
|
+
try {
|
|
3822
|
+
const healthResponse = await fetch(`http://localhost:${port}/health`, {
|
|
3823
|
+
method: 'GET',
|
|
3824
|
+
headers: {
|
|
3825
|
+
'Accept': 'application/json',
|
|
3826
|
+
'Content-Type': 'application/json'
|
|
3827
|
+
},
|
|
3828
|
+
mode: 'cors'
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
if (!healthResponse.ok) {
|
|
3832
|
+
throw new Error(`Server health check failed: ${healthResponse.status} ${healthResponse.statusText}`);
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
console.log('✅ Server health check passed');
|
|
3836
|
+
} catch (healthError) {
|
|
3837
|
+
throw new Error(`Cannot reach server at localhost:${port}. Health check failed: ${healthError.message}`);
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// Make the actual git diff request
|
|
3841
|
+
const response = await fetch(requestUrl, {
|
|
3842
|
+
method: 'GET',
|
|
3843
|
+
headers: {
|
|
3844
|
+
'Accept': 'application/json',
|
|
3845
|
+
'Content-Type': 'application/json'
|
|
3846
|
+
},
|
|
3847
|
+
mode: 'cors'
|
|
3848
|
+
});
|
|
3849
|
+
|
|
3850
|
+
if (!response.ok) {
|
|
3851
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
const result = await response.json();
|
|
3855
|
+
console.log('📦 Git diff response:', result);
|
|
3856
|
+
|
|
3857
|
+
// Hide loading
|
|
3858
|
+
modal.querySelector('.git-diff-loading').style.display = 'none';
|
|
3859
|
+
|
|
3860
|
+
if (result.success) {
|
|
3861
|
+
console.log('📊 Displaying successful git diff');
|
|
3862
|
+
// Show successful diff
|
|
3863
|
+
displayGitDiff(modal, result);
|
|
3864
|
+
} else {
|
|
3865
|
+
console.log('⚠️ Displaying git diff error:', result);
|
|
3866
|
+
// Show error
|
|
3867
|
+
displayGitDiffError(modal, result);
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
} catch (error) {
|
|
3871
|
+
console.error('❌ Failed to fetch git diff:', error);
|
|
3872
|
+
console.error('Error details:', {
|
|
3873
|
+
name: error.name,
|
|
3874
|
+
message: error.message,
|
|
3875
|
+
stack: error.stack,
|
|
3876
|
+
filePath,
|
|
3877
|
+
timestamp,
|
|
3878
|
+
workingDir
|
|
3879
|
+
});
|
|
3880
|
+
|
|
3881
|
+
modal.querySelector('.git-diff-loading').style.display = 'none';
|
|
3882
|
+
|
|
3883
|
+
// Create detailed error message based on error type
|
|
3884
|
+
let errorMessage = `Network error: ${error.message}`;
|
|
3885
|
+
let suggestions = [];
|
|
3886
|
+
|
|
3887
|
+
if (error.message.includes('Failed to fetch')) {
|
|
3888
|
+
errorMessage = 'Failed to connect to the monitoring server';
|
|
3889
|
+
suggestions = [
|
|
3890
|
+
'Check if the monitoring server is running on port 8765',
|
|
3891
|
+
'Verify the port configuration in the dashboard',
|
|
3892
|
+
'Check browser console for CORS or network errors',
|
|
3893
|
+
'Try refreshing the page and reconnecting'
|
|
3894
|
+
];
|
|
3895
|
+
} else if (error.message.includes('health check failed')) {
|
|
3896
|
+
errorMessage = error.message;
|
|
3897
|
+
suggestions = [
|
|
3898
|
+
'The server may be starting up - try again in a few seconds',
|
|
3899
|
+
'Check if another process is using port 8765',
|
|
3900
|
+
'Restart the claude-mpm monitoring server'
|
|
3901
|
+
];
|
|
3902
|
+
} else if (error.message.includes('HTTP')) {
|
|
3903
|
+
errorMessage = `Server error: ${error.message}`;
|
|
3904
|
+
suggestions = [
|
|
3905
|
+
'The server encountered an internal error',
|
|
3906
|
+
'Check the server logs for more details',
|
|
3907
|
+
'Try with a different file or working directory'
|
|
3908
|
+
];
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
displayGitDiffError(modal, {
|
|
3912
|
+
error: errorMessage,
|
|
3913
|
+
file_path: filePath,
|
|
3914
|
+
working_dir: workingDir,
|
|
3915
|
+
suggestions: suggestions,
|
|
3916
|
+
debug_info: {
|
|
3917
|
+
error_type: error.name,
|
|
3918
|
+
original_message: error.message,
|
|
3919
|
+
port: window.dashboard?.socketClient?.port || document.getElementById('port-input')?.value || '8765',
|
|
3920
|
+
timestamp: new Date().toISOString()
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
function highlightGitDiff(diffText) {
|
|
3927
|
+
/**
|
|
3928
|
+
* Apply basic syntax highlighting to git diff output
|
|
3929
|
+
* WHY: Git diffs have a standard format that can be highlighted for better readability:
|
|
3930
|
+
* - Lines starting with '+' are additions (green)
|
|
3931
|
+
* - Lines starting with '-' are deletions (red)
|
|
3932
|
+
* - Lines starting with '@@' are context headers (blue)
|
|
3933
|
+
* - File headers and metadata get special formatting
|
|
3934
|
+
*/
|
|
3935
|
+
return diffText
|
|
3936
|
+
.split('\n')
|
|
3937
|
+
.map(line => {
|
|
3938
|
+
// Escape HTML entities
|
|
3939
|
+
const escaped = line
|
|
3940
|
+
.replace(/&/g, '&')
|
|
3941
|
+
.replace(/</g, '<')
|
|
3942
|
+
.replace(/>/g, '>');
|
|
3943
|
+
|
|
3944
|
+
// Apply diff highlighting
|
|
3945
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
3946
|
+
return `<span class="diff-header">${escaped}</span>`;
|
|
3947
|
+
} else if (line.startsWith('@@')) {
|
|
3948
|
+
return `<span class="diff-meta">${escaped}</span>`;
|
|
3949
|
+
} else if (line.startsWith('+')) {
|
|
3950
|
+
return `<span class="diff-addition">${escaped}</span>`;
|
|
3951
|
+
} else if (line.startsWith('-')) {
|
|
3952
|
+
return `<span class="diff-deletion">${escaped}</span>`;
|
|
3953
|
+
} else if (line.startsWith('commit ') || line.startsWith('Author:') || line.startsWith('Date:')) {
|
|
3954
|
+
return `<span class="diff-header">${escaped}</span>`;
|
|
3955
|
+
} else {
|
|
3956
|
+
return `<span class="diff-context">${escaped}</span>`;
|
|
3957
|
+
}
|
|
3958
|
+
})
|
|
3959
|
+
.join('\n');
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
function displayGitDiff(modal, result) {
|
|
3963
|
+
console.log('📝 displayGitDiff called with:', result);
|
|
3964
|
+
const contentArea = modal.querySelector('.git-diff-content-area');
|
|
3965
|
+
const commitHashElement = modal.querySelector('.commit-hash');
|
|
3966
|
+
const methodElement = modal.querySelector('.diff-method');
|
|
3967
|
+
const codeElement = modal.querySelector('.git-diff-code');
|
|
3968
|
+
|
|
3969
|
+
console.log('🔍 Elements found:', {
|
|
3970
|
+
contentArea: !!contentArea,
|
|
3971
|
+
commitHashElement: !!commitHashElement,
|
|
3972
|
+
methodElement: !!methodElement,
|
|
3973
|
+
codeElement: !!codeElement
|
|
3974
|
+
});
|
|
3975
|
+
|
|
3976
|
+
// Update metadata
|
|
3977
|
+
if (commitHashElement) commitHashElement.textContent = `Commit: ${result.commit_hash}`;
|
|
3978
|
+
if (methodElement) methodElement.textContent = `Method: ${result.method}`;
|
|
3979
|
+
|
|
3980
|
+
// Update diff content with basic syntax highlighting
|
|
3981
|
+
if (codeElement && result.diff) {
|
|
3982
|
+
console.log('💡 Setting diff content, length:', result.diff.length);
|
|
3983
|
+
codeElement.innerHTML = highlightGitDiff(result.diff);
|
|
3984
|
+
|
|
3985
|
+
// Force scrolling to work by setting explicit heights
|
|
3986
|
+
const wrapper = modal.querySelector('.git-diff-scroll-wrapper');
|
|
3987
|
+
if (wrapper) {
|
|
3988
|
+
// Give it a moment for content to render
|
|
3989
|
+
setTimeout(() => {
|
|
3990
|
+
const modalContent = modal.querySelector('.modal-content');
|
|
3991
|
+
const header = modal.querySelector('.git-diff-header');
|
|
3992
|
+
const toolbar = modal.querySelector('.git-diff-toolbar');
|
|
3993
|
+
|
|
3994
|
+
const modalHeight = modalContent?.offsetHeight || 0;
|
|
3995
|
+
const headerHeight = header?.offsetHeight || 0;
|
|
3996
|
+
const toolbarHeight = toolbar?.offsetHeight || 0;
|
|
3997
|
+
|
|
3998
|
+
const availableHeight = modalHeight - headerHeight - toolbarHeight - 40; // 40px for padding
|
|
3999
|
+
|
|
4000
|
+
console.log('🎯 Setting explicit scroll height:', {
|
|
4001
|
+
modalHeight,
|
|
4002
|
+
headerHeight,
|
|
4003
|
+
toolbarHeight,
|
|
4004
|
+
availableHeight
|
|
4005
|
+
});
|
|
4006
|
+
|
|
4007
|
+
wrapper.style.maxHeight = `${availableHeight}px`;
|
|
4008
|
+
wrapper.style.overflowY = 'auto';
|
|
4009
|
+
}, 50);
|
|
4010
|
+
}
|
|
4011
|
+
} else {
|
|
4012
|
+
console.warn('⚠️ Missing codeElement or diff data');
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
// Show content area
|
|
4016
|
+
if (contentArea) {
|
|
4017
|
+
contentArea.style.display = 'block';
|
|
4018
|
+
console.log('✅ Content area displayed');
|
|
4019
|
+
|
|
4020
|
+
// Debug height information and force scrolling test
|
|
4021
|
+
setTimeout(() => {
|
|
4022
|
+
const modal = document.querySelector('.modal-content.git-diff-content');
|
|
4023
|
+
const body = document.querySelector('.git-diff-body');
|
|
4024
|
+
const wrapper = document.querySelector('.git-diff-scroll-wrapper');
|
|
4025
|
+
const display = document.querySelector('.git-diff-display');
|
|
4026
|
+
const code = document.querySelector('.git-diff-code');
|
|
4027
|
+
|
|
4028
|
+
console.log('🔍 Height debugging:', {
|
|
4029
|
+
modalHeight: modal?.offsetHeight,
|
|
4030
|
+
bodyHeight: body?.offsetHeight,
|
|
4031
|
+
wrapperHeight: wrapper?.offsetHeight,
|
|
4032
|
+
wrapperScrollHeight: wrapper?.scrollHeight,
|
|
4033
|
+
displayHeight: display?.offsetHeight,
|
|
4034
|
+
displayScrollHeight: display?.scrollHeight,
|
|
4035
|
+
codeHeight: code?.offsetHeight,
|
|
4036
|
+
wrapperStyle: wrapper ? window.getComputedStyle(wrapper).overflow : null,
|
|
4037
|
+
bodyStyle: body ? window.getComputedStyle(body).overflow : null
|
|
4038
|
+
});
|
|
4039
|
+
|
|
4040
|
+
// Force test - add a lot of content to test scrolling
|
|
4041
|
+
if (code && code.textContent.length < 1000) {
|
|
4042
|
+
console.log('🧪 Adding test content for scrolling...');
|
|
4043
|
+
code.innerHTML = code.innerHTML + '\n\n' + '// TEST SCROLLING\n'.repeat(100);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
// Force fix scrolling with inline styles
|
|
4047
|
+
if (wrapper) {
|
|
4048
|
+
console.log('🔧 Applying scrolling fix...');
|
|
4049
|
+
wrapper.style.height = '100%';
|
|
4050
|
+
wrapper.style.overflow = 'auto';
|
|
4051
|
+
wrapper.style.maxHeight = 'calc(100% - 60px)'; // Account for toolbar
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
// Also check parent heights
|
|
4055
|
+
const contentArea = document.querySelector('.git-diff-content-area');
|
|
4056
|
+
if (contentArea) {
|
|
4057
|
+
const computedStyle = window.getComputedStyle(contentArea);
|
|
4058
|
+
console.log('📏 Content area height:', computedStyle.height);
|
|
4059
|
+
if (computedStyle.height === 'auto' || !computedStyle.height) {
|
|
4060
|
+
contentArea.style.height = '100%';
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
}, 100);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
function displayGitDiffError(modal, result) {
|
|
4068
|
+
const errorArea = modal.querySelector('.git-diff-error');
|
|
4069
|
+
const messageElement = modal.querySelector('.error-message');
|
|
4070
|
+
const suggestionsElement = modal.querySelector('.error-suggestions');
|
|
4071
|
+
|
|
4072
|
+
messageElement.textContent = result.error || 'Unknown error occurred';
|
|
4073
|
+
|
|
4074
|
+
if (result.suggestions && result.suggestions.length > 0) {
|
|
4075
|
+
suggestionsElement.innerHTML = `
|
|
4076
|
+
<h4>Suggestions:</h4>
|
|
4077
|
+
<ul>
|
|
4078
|
+
${result.suggestions.map(s => `<li>${s}</li>`).join('')}
|
|
4079
|
+
</ul>
|
|
4080
|
+
`;
|
|
4081
|
+
} else {
|
|
4082
|
+
suggestionsElement.innerHTML = '';
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
errorArea.style.display = 'block';
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
window.copyGitDiff = function() {
|
|
4089
|
+
const modal = document.getElementById('git-diff-modal');
|
|
4090
|
+
if (!modal) return;
|
|
4091
|
+
|
|
4092
|
+
const codeElement = modal.querySelector('.git-diff-code');
|
|
4093
|
+
if (!codeElement) return;
|
|
4094
|
+
|
|
4095
|
+
const text = codeElement.textContent;
|
|
4096
|
+
|
|
4097
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
4098
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
4099
|
+
// Show brief feedback
|
|
4100
|
+
const button = modal.querySelector('.git-diff-copy');
|
|
4101
|
+
const originalText = button.textContent;
|
|
4102
|
+
button.textContent = '✅ Copied!';
|
|
4103
|
+
setTimeout(() => {
|
|
4104
|
+
button.textContent = originalText;
|
|
4105
|
+
}, 2000);
|
|
4106
|
+
}).catch(err => {
|
|
4107
|
+
console.error('Failed to copy text:', err);
|
|
4108
|
+
});
|
|
4109
|
+
} else {
|
|
4110
|
+
// Fallback for older browsers
|
|
4111
|
+
const textarea = document.createElement('textarea');
|
|
4112
|
+
textarea.value = text;
|
|
4113
|
+
document.body.appendChild(textarea);
|
|
4114
|
+
textarea.select();
|
|
4115
|
+
document.execCommand('copy');
|
|
4116
|
+
document.body.removeChild(textarea);
|
|
4117
|
+
|
|
4118
|
+
const button = modal.querySelector('.git-diff-copy');
|
|
4119
|
+
const originalText = button.textContent;
|
|
4120
|
+
button.textContent = '✅ Copied!';
|
|
4121
|
+
setTimeout(() => {
|
|
4122
|
+
button.textContent = originalText;
|
|
4123
|
+
}, 2000);
|
|
4124
|
+
}
|
|
4125
|
+
};
|
|
4126
|
+
|
|
4127
|
+
// Initialize dashboard when DOM is loaded
|
|
4128
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
4129
|
+
window.dashboard = new Dashboard();
|
|
4130
|
+
console.log('Dashboard ready');
|
|
4131
|
+
});
|
|
4132
|
+
|
|
4133
|
+
// Export for use in other modules
|
|
4134
|
+
window.Dashboard = Dashboard;
|