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