claude-mpm 4.2.39__py3-none-any.whl → 4.2.42__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/VERSION +1 -1
- claude_mpm/agents/BASE_ENGINEER.md +114 -1
- claude_mpm/agents/BASE_OPS.md +156 -1
- claude_mpm/agents/INSTRUCTIONS.md +120 -11
- claude_mpm/agents/WORKFLOW.md +160 -10
- claude_mpm/agents/templates/agentic-coder-optimizer.json +17 -12
- claude_mpm/agents/templates/react_engineer.json +217 -0
- claude_mpm/agents/templates/web_qa.json +40 -4
- claude_mpm/cli/__init__.py +3 -5
- claude_mpm/commands/mpm-browser-monitor.md +370 -0
- claude_mpm/commands/mpm-monitor.md +177 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +1076 -2
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +465 -2
- claude_mpm/dashboard/static/css/dashboard.css +2 -0
- claude_mpm/dashboard/static/js/browser-console-monitor.js +495 -0
- claude_mpm/dashboard/static/js/components/browser-log-viewer.js +763 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +931 -340
- claude_mpm/dashboard/static/js/components/diff-viewer.js +891 -0
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +443 -0
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +690 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +307 -19
- claude_mpm/dashboard/static/js/socket-client.js +2 -2
- claude_mpm/dashboard/static/test-browser-monitor.html +470 -0
- claude_mpm/dashboard/templates/index.html +62 -99
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/monitor/daemon.py +69 -36
- claude_mpm/services/monitor/daemon_manager.py +186 -29
- claude_mpm/services/monitor/handlers/browser.py +451 -0
- claude_mpm/services/monitor/server.py +272 -5
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/RECORD +35 -29
- claude_mpm/agents/templates/agentic-coder-optimizer.md +0 -44
- claude_mpm/agents/templates/agentic_coder_optimizer.json +0 -238
- claude_mpm/agents/templates/test-non-mpm.json +0 -20
- claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/top_level.txt +0 -0
@@ -1,2 +1,1076 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
/**
|
2
|
+
* Code Viewer Component - Claude Activity Tree Viewer
|
3
|
+
*
|
4
|
+
* Shows a D3.js tree visualization of files Claude has viewed or edited,
|
5
|
+
* including AST paths (classes, functions, methods) extracted from the files.
|
6
|
+
* This is NOT a directory viewer but an activity-focused visualization.
|
7
|
+
* Renders in the Claude Tree tab of the dashboard.
|
8
|
+
*/
|
9
|
+
|
10
|
+
class CodeViewer {
|
11
|
+
constructor() {
|
12
|
+
this.container = null;
|
13
|
+
this.svg = null;
|
14
|
+
this.initialized = false;
|
15
|
+
this.fileActivity = new Map(); // Map of file path to activity data
|
16
|
+
this.sessions = new Map();
|
17
|
+
this.currentSession = null;
|
18
|
+
this.treeData = null;
|
19
|
+
this.d3Tree = null;
|
20
|
+
this.d3Root = null;
|
21
|
+
this.selectedNode = null;
|
22
|
+
this.width = 800;
|
23
|
+
this.height = 600;
|
24
|
+
this.nodeRadius = 5;
|
25
|
+
this.renderInProgress = false; // Prevent concurrent renders
|
26
|
+
this.containerObserver = null;
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Initialize the code viewer
|
31
|
+
*/
|
32
|
+
initialize() {
|
33
|
+
if (this.initialized) {
|
34
|
+
console.log('[CodeViewer] Already initialized, skipping');
|
35
|
+
return;
|
36
|
+
}
|
37
|
+
|
38
|
+
console.log('[CodeViewer] Initializing...');
|
39
|
+
this.setupContainer();
|
40
|
+
this.setupEventHandlers();
|
41
|
+
this.subscribeToEvents();
|
42
|
+
this.processExistingEvents();
|
43
|
+
|
44
|
+
this.initialized = true;
|
45
|
+
console.log('[CodeViewer] Code Viewer (Claude Activity Tree) initialized successfully');
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Setup the container in the Claude Tree tab
|
50
|
+
*/
|
51
|
+
setupContainer() {
|
52
|
+
// Find the Claude Tree tab container
|
53
|
+
const treeContainer = document.getElementById('claude-tree-container');
|
54
|
+
if (!treeContainer) {
|
55
|
+
console.error('Claude Tree container not found');
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
|
59
|
+
// Store the container reference
|
60
|
+
this.container = treeContainer;
|
61
|
+
|
62
|
+
// Setup the activity tree interface
|
63
|
+
this.renderInterface();
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Render the activity tree interface in the Claude Tree tab
|
68
|
+
*/
|
69
|
+
renderInterface() {
|
70
|
+
if (!this.container) {
|
71
|
+
console.error('[CodeViewer] Container not found, cannot render interface');
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
|
75
|
+
// Prevent concurrent renders
|
76
|
+
if (this.renderInProgress) {
|
77
|
+
console.log('[CodeViewer] Render already in progress, skipping');
|
78
|
+
return;
|
79
|
+
}
|
80
|
+
|
81
|
+
// Check if interface already exists and is intact
|
82
|
+
const existingWrapper = this.container.querySelector('.activity-tree-wrapper');
|
83
|
+
const existingSvg = this.container.querySelector('#claude-activity-tree-svg');
|
84
|
+
if (existingWrapper && existingSvg) {
|
85
|
+
console.log('[CodeViewer] Interface already exists and is intact, skipping render');
|
86
|
+
return;
|
87
|
+
}
|
88
|
+
|
89
|
+
this.renderInProgress = true;
|
90
|
+
console.log('[CodeViewer] Rendering interface in container:', this.container.id);
|
91
|
+
|
92
|
+
// Temporarily disconnect observer to prevent loops
|
93
|
+
if (this.containerObserver) {
|
94
|
+
this.containerObserver.disconnect();
|
95
|
+
}
|
96
|
+
|
97
|
+
// Clear any existing content completely
|
98
|
+
this.container.innerHTML = '';
|
99
|
+
|
100
|
+
// Create the activity tree interface (without redundant session selector)
|
101
|
+
this.container.innerHTML = `
|
102
|
+
<div class="activity-tree-wrapper" style="height: 100%; display: flex; flex-direction: column;">
|
103
|
+
<div class="activity-controls" style="padding: 10px; border-bottom: 1px solid #ddd; background: #f9f9f9; display: flex; align-items: center; gap: 10px;">
|
104
|
+
<button id="claude-expand-all-btn" class="control-btn" style="padding: 4px 8px; font-size: 0.9em;">Expand All</button>
|
105
|
+
<button id="claude-collapse-all-btn" class="control-btn" style="padding: 4px 8px; font-size: 0.9em;">Collapse All</button>
|
106
|
+
<button id="claude-reset-zoom-btn" class="control-btn" style="padding: 4px 8px; font-size: 0.9em;">Reset Zoom</button>
|
107
|
+
<div class="stats" id="claude-tree-stats" style="margin-left: auto; font-size: 0.9em; color: #666;"></div>
|
108
|
+
</div>
|
109
|
+
<div class="tree-container" id="claude-tree-svg-container" style="flex: 1; overflow: hidden; position: relative; background: white;">
|
110
|
+
<svg id="claude-activity-tree-svg" style="width: 100%; height: 100%;"></svg>
|
111
|
+
</div>
|
112
|
+
<div class="legend" style="padding: 5px 10px; border-top: 1px solid #ddd; background: #f9f9f9; font-size: 0.85em; display: flex; gap: 15px;">
|
113
|
+
<span class="legend-item"><span style="color: #4CAF50;">●</span> File</span>
|
114
|
+
<span class="legend-item"><span style="color: #2196F3;">●</span> Class</span>
|
115
|
+
<span class="legend-item"><span style="color: #FF9800;">●</span> Function</span>
|
116
|
+
<span class="legend-item"><span style="color: #9C27B0;">●</span> Method</span>
|
117
|
+
<span class="legend-item"><span style="color: #F44336;">◆</span> Edited</span>
|
118
|
+
<span class="legend-item"><span style="color: #4CAF50;">○</span> Viewed</span>
|
119
|
+
</div>
|
120
|
+
</div>
|
121
|
+
`;
|
122
|
+
|
123
|
+
// Get container dimensions for tree sizing
|
124
|
+
const svgContainer = document.getElementById('claude-tree-svg-container');
|
125
|
+
if (svgContainer) {
|
126
|
+
const rect = svgContainer.getBoundingClientRect();
|
127
|
+
this.width = rect.width || 800;
|
128
|
+
this.height = rect.height || 600;
|
129
|
+
}
|
130
|
+
|
131
|
+
// Mark render as complete and re-enable observer if needed
|
132
|
+
this.renderInProgress = false;
|
133
|
+
|
134
|
+
// Re-enable container protection after render
|
135
|
+
if (this.containerObserver && this.container) {
|
136
|
+
this.containerObserver.observe(this.container, {
|
137
|
+
childList: true,
|
138
|
+
subtree: false // Only watch direct children, not subtree
|
139
|
+
});
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
/**
|
144
|
+
* Show the activity tree (switch to Claude Tree tab and render)
|
145
|
+
*/
|
146
|
+
show() {
|
147
|
+
console.log('[CodeViewer] show() called');
|
148
|
+
|
149
|
+
// Switch to the Claude Tree tab if we're being called from tab switch
|
150
|
+
const claudeTreeTab = document.querySelector('[data-tab="claude-tree"]');
|
151
|
+
const claudeTreeContent = document.getElementById('claude-tree-tab');
|
152
|
+
|
153
|
+
if (claudeTreeTab && claudeTreeContent) {
|
154
|
+
// Only switch tabs if not already active
|
155
|
+
if (!claudeTreeContent.classList.contains('active')) {
|
156
|
+
// Remove active class from all tabs and contents
|
157
|
+
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
158
|
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
159
|
+
|
160
|
+
// Add active class to Claude Tree tab
|
161
|
+
claudeTreeTab.classList.add('active');
|
162
|
+
claudeTreeContent.classList.add('active');
|
163
|
+
}
|
164
|
+
}
|
165
|
+
|
166
|
+
// Get the claude tree container
|
167
|
+
const claudeTreeContainer = document.getElementById('claude-tree-container');
|
168
|
+
if (!claudeTreeContainer) {
|
169
|
+
console.error('[CodeViewer] Claude Tree container not found!');
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
|
173
|
+
// CRITICAL: Prevent other components from writing to this container
|
174
|
+
// Add multiple attributes to mark ownership strongly
|
175
|
+
claudeTreeContainer.setAttribute('data-owner', 'code-viewer');
|
176
|
+
claudeTreeContainer.setAttribute('data-tab-reserved', 'claude-tree');
|
177
|
+
claudeTreeContainer.setAttribute('data-component', 'CodeViewer');
|
178
|
+
|
179
|
+
// Store the container reference if not already set
|
180
|
+
if (!this.container || this.container !== claudeTreeContainer) {
|
181
|
+
this.container = claudeTreeContainer;
|
182
|
+
}
|
183
|
+
|
184
|
+
// Initialize if needed (this will setup container and render interface)
|
185
|
+
if (!this.initialized) {
|
186
|
+
this.initialize();
|
187
|
+
} else {
|
188
|
+
// Only render interface if it doesn't exist
|
189
|
+
const existingWrapper = this.container.querySelector('.activity-tree-wrapper');
|
190
|
+
if (!existingWrapper) {
|
191
|
+
console.log('[CodeViewer] Interface missing, rendering...');
|
192
|
+
this.renderInterface();
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
// Set up mutation observer to protect container (only if not already set)
|
197
|
+
if (!this.containerObserver) {
|
198
|
+
this.protectContainer();
|
199
|
+
}
|
200
|
+
|
201
|
+
// Setup event handlers for the new controls
|
202
|
+
this.setupControlHandlers();
|
203
|
+
|
204
|
+
// Get current session from main selector
|
205
|
+
const mainSessionSelect = document.getElementById('session-select');
|
206
|
+
if (mainSessionSelect) {
|
207
|
+
this.currentSession = mainSessionSelect.value || null;
|
208
|
+
}
|
209
|
+
|
210
|
+
// Build and render tree
|
211
|
+
this.buildTreeData();
|
212
|
+
this.renderTree();
|
213
|
+
|
214
|
+
// Update stats
|
215
|
+
this.updateStats();
|
216
|
+
|
217
|
+
console.log('[CodeViewer] show() completed, container should now have tree interface');
|
218
|
+
}
|
219
|
+
|
220
|
+
/**
|
221
|
+
* Protect the container from being overwritten by other components
|
222
|
+
*/
|
223
|
+
protectContainer() {
|
224
|
+
const container = document.getElementById('claude-tree-container');
|
225
|
+
if (!container) return;
|
226
|
+
|
227
|
+
// Disconnect any existing observer
|
228
|
+
if (this.containerObserver) {
|
229
|
+
this.containerObserver.disconnect();
|
230
|
+
}
|
231
|
+
|
232
|
+
// Flag to prevent re-render loops
|
233
|
+
let reRenderScheduled = false;
|
234
|
+
|
235
|
+
// Create a new observer to watch for unwanted changes
|
236
|
+
this.containerObserver = new MutationObserver((mutations) => {
|
237
|
+
for (const mutation of mutations) {
|
238
|
+
// Check if nodes were added that shouldn't be there
|
239
|
+
for (const node of mutation.addedNodes) {
|
240
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
241
|
+
const element = node;
|
242
|
+
|
243
|
+
// AGGRESSIVE filtering: Block ANY content that's not our tree interface
|
244
|
+
const isUnwantedContent = (
|
245
|
+
element.classList?.contains('event-item') ||
|
246
|
+
element.classList?.contains('events-list') ||
|
247
|
+
element.classList?.contains('no-events') ||
|
248
|
+
element.id === 'events-list' ||
|
249
|
+
(element.textContent && (
|
250
|
+
element.textContent.includes('[hook]') ||
|
251
|
+
element.textContent.includes('hook.user_prompt') ||
|
252
|
+
element.textContent.includes('hook.pre_tool') ||
|
253
|
+
element.textContent.includes('hook.post_tool') ||
|
254
|
+
element.textContent.includes('Connect to Socket.IO') ||
|
255
|
+
element.textContent.includes('No events')
|
256
|
+
)) ||
|
257
|
+
// Block any div without our expected classes
|
258
|
+
(element.tagName === 'DIV' &&
|
259
|
+
!element.classList?.contains('activity-tree-wrapper') &&
|
260
|
+
!element.classList?.contains('activity-controls') &&
|
261
|
+
!element.classList?.contains('tree-container') &&
|
262
|
+
!element.classList?.contains('legend') &&
|
263
|
+
!element.id?.startsWith('claude-'))
|
264
|
+
);
|
265
|
+
|
266
|
+
if (isUnwantedContent) {
|
267
|
+
console.warn('[CodeViewer] BLOCKED unwanted content in Claude Tree container:', element);
|
268
|
+
console.warn('[CodeViewer] Element classes:', element.classList?.toString());
|
269
|
+
console.warn('[CodeViewer] Element text preview:', element.textContent?.substring(0, 100));
|
270
|
+
|
271
|
+
// Remove the unwanted content immediately
|
272
|
+
try {
|
273
|
+
node.remove();
|
274
|
+
} catch (e) {
|
275
|
+
console.warn('[CodeViewer] Failed to remove unwanted node:', e);
|
276
|
+
}
|
277
|
+
|
278
|
+
// Schedule a single re-render if needed
|
279
|
+
if (!reRenderScheduled && !this.renderInProgress) {
|
280
|
+
reRenderScheduled = true;
|
281
|
+
setTimeout(() => {
|
282
|
+
reRenderScheduled = false;
|
283
|
+
if (!container.querySelector('.activity-tree-wrapper')) {
|
284
|
+
console.log('[CodeViewer] Re-rendering interface after blocking unwanted content');
|
285
|
+
this.renderInterface();
|
286
|
+
this.setupControlHandlers();
|
287
|
+
this.buildTreeData();
|
288
|
+
this.renderTree();
|
289
|
+
}
|
290
|
+
}, 50);
|
291
|
+
}
|
292
|
+
}
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
// Also check if our content was removed
|
297
|
+
if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
|
298
|
+
for (const node of mutation.removedNodes) {
|
299
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
300
|
+
const element = node;
|
301
|
+
if (element.classList?.contains('activity-tree-wrapper')) {
|
302
|
+
console.warn('[CodeViewer] Our tree interface was removed! Re-rendering...');
|
303
|
+
if (!reRenderScheduled && !this.renderInProgress) {
|
304
|
+
reRenderScheduled = true;
|
305
|
+
setTimeout(() => {
|
306
|
+
reRenderScheduled = false;
|
307
|
+
this.renderInterface();
|
308
|
+
this.setupControlHandlers();
|
309
|
+
this.buildTreeData();
|
310
|
+
this.renderTree();
|
311
|
+
}, 50);
|
312
|
+
}
|
313
|
+
}
|
314
|
+
}
|
315
|
+
}
|
316
|
+
}
|
317
|
+
}
|
318
|
+
});
|
319
|
+
|
320
|
+
// Start observing only direct children to reduce overhead
|
321
|
+
this.containerObserver.observe(container, {
|
322
|
+
childList: true,
|
323
|
+
subtree: false // Only watch direct children, not entire subtree
|
324
|
+
});
|
325
|
+
|
326
|
+
console.log('[CodeViewer] Container protection enabled with aggressive filtering');
|
327
|
+
}
|
328
|
+
|
329
|
+
/**
|
330
|
+
* Setup event handlers for controls
|
331
|
+
*/
|
332
|
+
setupControlHandlers() {
|
333
|
+
// Listen to main session selector changes
|
334
|
+
const mainSessionSelect = document.getElementById('session-select');
|
335
|
+
if (mainSessionSelect && !mainSessionSelect.hasAttribute('data-tree-listener')) {
|
336
|
+
mainSessionSelect.setAttribute('data-tree-listener', 'true');
|
337
|
+
mainSessionSelect.addEventListener('change', (e) => {
|
338
|
+
this.currentSession = e.target.value || null;
|
339
|
+
console.log('[CodeViewer] Session changed to:', this.currentSession);
|
340
|
+
if (this.isTabActive()) {
|
341
|
+
this.buildTreeData();
|
342
|
+
this.renderTree();
|
343
|
+
this.updateStats();
|
344
|
+
}
|
345
|
+
});
|
346
|
+
}
|
347
|
+
|
348
|
+
// Expand all button
|
349
|
+
const expandBtn = document.getElementById('claude-expand-all-btn');
|
350
|
+
if (expandBtn && !expandBtn.hasAttribute('data-listener')) {
|
351
|
+
expandBtn.setAttribute('data-listener', 'true');
|
352
|
+
expandBtn.addEventListener('click', () => {
|
353
|
+
this.expandAllNodes();
|
354
|
+
});
|
355
|
+
}
|
356
|
+
|
357
|
+
// Collapse all button
|
358
|
+
const collapseBtn = document.getElementById('claude-collapse-all-btn');
|
359
|
+
if (collapseBtn && !collapseBtn.hasAttribute('data-listener')) {
|
360
|
+
collapseBtn.setAttribute('data-listener', 'true');
|
361
|
+
collapseBtn.addEventListener('click', () => {
|
362
|
+
this.collapseAllNodes();
|
363
|
+
});
|
364
|
+
}
|
365
|
+
|
366
|
+
// Reset zoom button
|
367
|
+
const resetBtn = document.getElementById('claude-reset-zoom-btn');
|
368
|
+
if (resetBtn && !resetBtn.hasAttribute('data-listener')) {
|
369
|
+
resetBtn.setAttribute('data-listener', 'true');
|
370
|
+
resetBtn.addEventListener('click', () => {
|
371
|
+
this.resetZoom();
|
372
|
+
});
|
373
|
+
}
|
374
|
+
}
|
375
|
+
|
376
|
+
/**
|
377
|
+
* Setup event handlers
|
378
|
+
*/
|
379
|
+
setupEventHandlers() {
|
380
|
+
// Tab handling is done in show() method
|
381
|
+
}
|
382
|
+
|
383
|
+
/**
|
384
|
+
* Subscribe to events from socket and event bus
|
385
|
+
*/
|
386
|
+
subscribeToEvents() {
|
387
|
+
// Listen for claude events from socket
|
388
|
+
if (window.socket) {
|
389
|
+
window.socket.on('claude_event', (event) => {
|
390
|
+
console.log('[CodeViewer] Received claude_event:', event);
|
391
|
+
if (this.isFileOperationEvent(event)) {
|
392
|
+
this.processClaudeEvent(event);
|
393
|
+
// Only update if the Claude Tree tab is active
|
394
|
+
if (this.isTabActive()) {
|
395
|
+
this.buildTreeData();
|
396
|
+
this.renderTree();
|
397
|
+
this.updateStats();
|
398
|
+
}
|
399
|
+
}
|
400
|
+
});
|
401
|
+
}
|
402
|
+
|
403
|
+
// Listen for events from event bus
|
404
|
+
if (window.eventBus) {
|
405
|
+
window.eventBus.on('claude_event', (event) => {
|
406
|
+
console.log('[CodeViewer] Received claude_event from eventBus:', event);
|
407
|
+
if (this.isFileOperationEvent(event)) {
|
408
|
+
this.processClaudeEvent(event);
|
409
|
+
// Only update if the Claude Tree tab is active
|
410
|
+
if (this.isTabActive()) {
|
411
|
+
this.buildTreeData();
|
412
|
+
this.renderTree();
|
413
|
+
this.updateStats();
|
414
|
+
}
|
415
|
+
}
|
416
|
+
});
|
417
|
+
}
|
418
|
+
}
|
419
|
+
|
420
|
+
/**
|
421
|
+
* Check if Claude Tree tab is active
|
422
|
+
*/
|
423
|
+
isTabActive() {
|
424
|
+
const claudeTreeContent = document.getElementById('claude-tree-tab');
|
425
|
+
return claudeTreeContent && claudeTreeContent.classList.contains('active');
|
426
|
+
}
|
427
|
+
|
428
|
+
/**
|
429
|
+
* Process existing events from dashboard
|
430
|
+
*/
|
431
|
+
processExistingEvents() {
|
432
|
+
if (window.dashboard && window.dashboard.eventStore) {
|
433
|
+
const events = window.dashboard.eventStore.getAllEvents();
|
434
|
+
events.forEach(event => {
|
435
|
+
if (this.isFileOperationEvent(event)) {
|
436
|
+
this.processClaudeEvent(event);
|
437
|
+
}
|
438
|
+
});
|
439
|
+
}
|
440
|
+
}
|
441
|
+
|
442
|
+
/**
|
443
|
+
* Check if an event is a file operation event
|
444
|
+
*/
|
445
|
+
isFileOperationEvent(event) {
|
446
|
+
// Check if this is a hook event with file operation tool
|
447
|
+
if (event.type === 'hook' &&
|
448
|
+
(event.subtype === 'pre_tool' || event.subtype === 'post_tool') &&
|
449
|
+
event.data && event.data.tool_name) {
|
450
|
+
const fileOps = ['Read', 'Write', 'Edit', 'MultiEdit', 'NotebookEdit'];
|
451
|
+
return fileOps.includes(event.data.tool_name);
|
452
|
+
}
|
453
|
+
return false;
|
454
|
+
}
|
455
|
+
|
456
|
+
/**
|
457
|
+
* Check if an event is a file operation (legacy format)
|
458
|
+
*/
|
459
|
+
isFileOperation(event) {
|
460
|
+
const fileOps = ['Read', 'Write', 'Edit', 'MultiEdit', 'NotebookEdit'];
|
461
|
+
return fileOps.includes(event.tool_name);
|
462
|
+
}
|
463
|
+
|
464
|
+
/**
|
465
|
+
* Process a claude event with file operation
|
466
|
+
*/
|
467
|
+
processClaudeEvent(event) {
|
468
|
+
if (!this.isFileOperationEvent(event)) return;
|
469
|
+
|
470
|
+
// Extract data from claude_event structure
|
471
|
+
const data = event.data || {};
|
472
|
+
const tool_name = data.tool_name;
|
473
|
+
const tool_parameters = data.tool_parameters || {};
|
474
|
+
const tool_output = data.tool_output;
|
475
|
+
const timestamp = event.timestamp || new Date().toISOString();
|
476
|
+
const session_id = event.session_id || data.session_id;
|
477
|
+
const working_directory = data.working_directory || '/';
|
478
|
+
|
479
|
+
const filePath = tool_parameters.file_path || tool_parameters.notebook_path;
|
480
|
+
|
481
|
+
console.log('[CodeViewer] Processing file operation:', tool_name, filePath);
|
482
|
+
|
483
|
+
this.processFileOperation({
|
484
|
+
tool_name,
|
485
|
+
tool_parameters,
|
486
|
+
tool_output,
|
487
|
+
timestamp,
|
488
|
+
session_id,
|
489
|
+
working_directory,
|
490
|
+
filePath
|
491
|
+
});
|
492
|
+
}
|
493
|
+
|
494
|
+
/**
|
495
|
+
* Process a file operation event (legacy format)
|
496
|
+
*/
|
497
|
+
processEvent(event) {
|
498
|
+
if (!this.isFileOperation(event)) return;
|
499
|
+
|
500
|
+
const { tool_name, tool_parameters, tool_output, timestamp, session_id, working_directory } = event;
|
501
|
+
const filePath = tool_parameters?.file_path || tool_parameters?.notebook_path;
|
502
|
+
|
503
|
+
this.processFileOperation({
|
504
|
+
tool_name,
|
505
|
+
tool_parameters,
|
506
|
+
tool_output,
|
507
|
+
timestamp,
|
508
|
+
session_id,
|
509
|
+
working_directory,
|
510
|
+
filePath
|
511
|
+
});
|
512
|
+
}
|
513
|
+
|
514
|
+
/**
|
515
|
+
* Process a file operation
|
516
|
+
*/
|
517
|
+
processFileOperation({ tool_name, tool_parameters, tool_output, timestamp, session_id, working_directory, filePath }) {
|
518
|
+
if (!filePath) return;
|
519
|
+
|
520
|
+
// Track session
|
521
|
+
if (session_id && !this.sessions.has(session_id)) {
|
522
|
+
this.sessions.set(session_id, {
|
523
|
+
id: session_id,
|
524
|
+
working_directory: working_directory || '/',
|
525
|
+
files: new Set()
|
526
|
+
});
|
527
|
+
// Update session list when new session is added
|
528
|
+
this.updateSessionList();
|
529
|
+
}
|
530
|
+
|
531
|
+
// Get or create file activity
|
532
|
+
if (!this.fileActivity.has(filePath)) {
|
533
|
+
this.fileActivity.set(filePath, {
|
534
|
+
path: filePath,
|
535
|
+
operations: [],
|
536
|
+
sessions: new Set(),
|
537
|
+
working_directories: new Set(),
|
538
|
+
lastContent: null,
|
539
|
+
astPaths: []
|
540
|
+
});
|
541
|
+
}
|
542
|
+
|
543
|
+
const activity = this.fileActivity.get(filePath);
|
544
|
+
|
545
|
+
// Add operation
|
546
|
+
activity.operations.push({
|
547
|
+
type: tool_name,
|
548
|
+
timestamp: timestamp,
|
549
|
+
parameters: tool_parameters,
|
550
|
+
output: tool_output,
|
551
|
+
session_id: session_id
|
552
|
+
});
|
553
|
+
|
554
|
+
// Track session and working directory
|
555
|
+
if (session_id) {
|
556
|
+
activity.sessions.add(session_id);
|
557
|
+
const session = this.sessions.get(session_id);
|
558
|
+
if (session) {
|
559
|
+
session.files.add(filePath);
|
560
|
+
}
|
561
|
+
}
|
562
|
+
if (working_directory) {
|
563
|
+
activity.working_directories.add(working_directory);
|
564
|
+
}
|
565
|
+
|
566
|
+
// Update content and extract AST if applicable
|
567
|
+
if (tool_name === 'Write' && tool_parameters.content) {
|
568
|
+
activity.lastContent = tool_parameters.content;
|
569
|
+
activity.astPaths = this.extractASTPaths(tool_parameters.content, filePath);
|
570
|
+
} else if (tool_name === 'Read' && tool_output?.content) {
|
571
|
+
activity.lastContent = tool_output.content;
|
572
|
+
activity.astPaths = this.extractASTPaths(tool_output.content, filePath);
|
573
|
+
} else if (tool_name === 'Edit' && activity.lastContent) {
|
574
|
+
// Apply edit to content if we have it
|
575
|
+
const oldString = tool_parameters.old_string;
|
576
|
+
const newString = tool_parameters.new_string;
|
577
|
+
if (oldString && newString) {
|
578
|
+
activity.lastContent = activity.lastContent.replace(oldString, newString);
|
579
|
+
activity.astPaths = this.extractASTPaths(activity.lastContent, filePath);
|
580
|
+
}
|
581
|
+
}
|
582
|
+
|
583
|
+
console.log('[CodeViewer] File activity updated:', filePath, 'Total files:', this.fileActivity.size)
|
584
|
+
}
|
585
|
+
|
586
|
+
/**
|
587
|
+
* Extract AST paths from code content
|
588
|
+
*/
|
589
|
+
extractASTPaths(content, filePath) {
|
590
|
+
if (!content || typeof content !== 'string') return [];
|
591
|
+
|
592
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
593
|
+
const paths = [];
|
594
|
+
|
595
|
+
if (ext === 'py') {
|
596
|
+
// Python: Extract classes, functions, and methods
|
597
|
+
const classRegex = /^class\s+(\w+)/gm;
|
598
|
+
const functionRegex = /^def\s+(\w+)/gm;
|
599
|
+
const methodRegex = /^\s{4,}def\s+(\w+)/gm;
|
600
|
+
|
601
|
+
let match;
|
602
|
+
while ((match = classRegex.exec(content)) !== null) {
|
603
|
+
paths.push({ name: match[1], type: 'class' });
|
604
|
+
}
|
605
|
+
while ((match = functionRegex.exec(content)) !== null) {
|
606
|
+
paths.push({ name: match[1], type: 'function' });
|
607
|
+
}
|
608
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
609
|
+
if (!paths.some(p => p.name === match[1])) {
|
610
|
+
paths.push({ name: match[1], type: 'method' });
|
611
|
+
}
|
612
|
+
}
|
613
|
+
} else if (ext === 'js' || ext === 'jsx' || ext === 'ts' || ext === 'tsx') {
|
614
|
+
// JavaScript/TypeScript: Extract classes, functions, methods
|
615
|
+
const classRegex = /class\s+(\w+)/g;
|
616
|
+
const functionRegex = /function\s+(\w+)/g;
|
617
|
+
const arrowFunctionRegex = /const\s+(\w+)\s*=\s*\([^)]*\)\s*=>/g;
|
618
|
+
const methodRegex = /(\w+)\s*\([^)]*\)\s*\{/g;
|
619
|
+
|
620
|
+
let match;
|
621
|
+
while ((match = classRegex.exec(content)) !== null) {
|
622
|
+
paths.push({ name: match[1], type: 'class' });
|
623
|
+
}
|
624
|
+
while ((match = functionRegex.exec(content)) !== null) {
|
625
|
+
paths.push({ name: match[1], type: 'function' });
|
626
|
+
}
|
627
|
+
while ((match = arrowFunctionRegex.exec(content)) !== null) {
|
628
|
+
paths.push({ name: match[1], type: 'function' });
|
629
|
+
}
|
630
|
+
}
|
631
|
+
|
632
|
+
return paths;
|
633
|
+
}
|
634
|
+
|
635
|
+
/**
|
636
|
+
* Build tree data from file activity
|
637
|
+
*/
|
638
|
+
buildTreeData() {
|
639
|
+
const root = {
|
640
|
+
name: 'Claude Activity',
|
641
|
+
type: 'root',
|
642
|
+
children: []
|
643
|
+
};
|
644
|
+
|
645
|
+
// Group by working directory
|
646
|
+
const dirMap = new Map();
|
647
|
+
|
648
|
+
for (const [filePath, activity] of this.fileActivity.entries()) {
|
649
|
+
// Filter by session if selected
|
650
|
+
if (this.currentSession) {
|
651
|
+
if (!activity.sessions.has(this.currentSession)) {
|
652
|
+
continue;
|
653
|
+
}
|
654
|
+
}
|
655
|
+
|
656
|
+
// Determine working directory
|
657
|
+
const workingDirs = Array.from(activity.working_directories);
|
658
|
+
const workingDir = workingDirs[0] || '/';
|
659
|
+
|
660
|
+
if (!dirMap.has(workingDir)) {
|
661
|
+
dirMap.set(workingDir, {
|
662
|
+
name: workingDir.split('/').pop() || workingDir,
|
663
|
+
path: workingDir,
|
664
|
+
type: 'directory',
|
665
|
+
children: []
|
666
|
+
});
|
667
|
+
}
|
668
|
+
|
669
|
+
// Create file node
|
670
|
+
const fileName = filePath.split('/').pop();
|
671
|
+
const hasEdits = activity.operations.some(op => op.type === 'Edit' || op.type === 'Write');
|
672
|
+
|
673
|
+
const fileNode = {
|
674
|
+
name: fileName,
|
675
|
+
path: filePath,
|
676
|
+
type: 'file',
|
677
|
+
edited: hasEdits,
|
678
|
+
operations: activity.operations.length,
|
679
|
+
children: []
|
680
|
+
};
|
681
|
+
|
682
|
+
// Add AST nodes
|
683
|
+
if (activity.astPaths.length > 0) {
|
684
|
+
activity.astPaths.forEach(ast => {
|
685
|
+
fileNode.children.push({
|
686
|
+
name: ast.name,
|
687
|
+
type: ast.type,
|
688
|
+
path: `${filePath}#${ast.name}`,
|
689
|
+
children: []
|
690
|
+
});
|
691
|
+
});
|
692
|
+
}
|
693
|
+
|
694
|
+
dirMap.get(workingDir).children.push(fileNode);
|
695
|
+
}
|
696
|
+
|
697
|
+
// Add directories to root
|
698
|
+
root.children = Array.from(dirMap.values());
|
699
|
+
|
700
|
+
// If only one directory and it's the root, flatten
|
701
|
+
if (root.children.length === 1 && root.children[0].path === '/') {
|
702
|
+
root.children = root.children[0].children;
|
703
|
+
}
|
704
|
+
|
705
|
+
this.treeData = root;
|
706
|
+
}
|
707
|
+
|
708
|
+
/**
|
709
|
+
* Render the D3 tree
|
710
|
+
*/
|
711
|
+
renderTree() {
|
712
|
+
if (!this.treeData || !this.container) return;
|
713
|
+
|
714
|
+
// Ensure SVG element exists
|
715
|
+
const svgElement = document.getElementById('claude-activity-tree-svg');
|
716
|
+
if (!svgElement) {
|
717
|
+
console.warn('[CodeViewer] SVG element not found, skipping tree render');
|
718
|
+
return;
|
719
|
+
}
|
720
|
+
|
721
|
+
const svg = d3.select(svgElement);
|
722
|
+
if (svg.empty()) {
|
723
|
+
console.warn('[CodeViewer] D3 could not select SVG element');
|
724
|
+
return;
|
725
|
+
}
|
726
|
+
|
727
|
+
svg.selectAll('*').remove();
|
728
|
+
|
729
|
+
// Get actual dimensions
|
730
|
+
const svgContainer = document.getElementById('claude-tree-svg-container');
|
731
|
+
if (svgContainer) {
|
732
|
+
const rect = svgContainer.getBoundingClientRect();
|
733
|
+
this.width = rect.width || 800;
|
734
|
+
this.height = rect.height || 600;
|
735
|
+
}
|
736
|
+
|
737
|
+
// Create container group for zoom/pan
|
738
|
+
const g = svg.append('g');
|
739
|
+
|
740
|
+
// Setup zoom behavior
|
741
|
+
const zoom = d3.zoom()
|
742
|
+
.scaleExtent([0.1, 4])
|
743
|
+
.on('zoom', (event) => {
|
744
|
+
g.attr('transform', event.transform);
|
745
|
+
});
|
746
|
+
|
747
|
+
svg.call(zoom);
|
748
|
+
|
749
|
+
// Create tree layout
|
750
|
+
const treeLayout = d3.tree()
|
751
|
+
.size([this.height - 100, this.width - 200]);
|
752
|
+
|
753
|
+
// Create hierarchy
|
754
|
+
this.d3Root = d3.hierarchy(this.treeData);
|
755
|
+
|
756
|
+
// Apply tree layout
|
757
|
+
treeLayout(this.d3Root);
|
758
|
+
|
759
|
+
// Create links
|
760
|
+
const link = g.selectAll('.link')
|
761
|
+
.data(this.d3Root.links())
|
762
|
+
.enter().append('path')
|
763
|
+
.attr('class', 'link')
|
764
|
+
.attr('d', d3.linkHorizontal()
|
765
|
+
.x(d => d.y + 100)
|
766
|
+
.y(d => d.x + 50))
|
767
|
+
.style('fill', 'none')
|
768
|
+
.style('stroke', '#ccc')
|
769
|
+
.style('stroke-width', 1);
|
770
|
+
|
771
|
+
// Create nodes
|
772
|
+
const node = g.selectAll('.node')
|
773
|
+
.data(this.d3Root.descendants())
|
774
|
+
.enter().append('g')
|
775
|
+
.attr('class', 'node')
|
776
|
+
.attr('transform', d => `translate(${d.y + 100},${d.x + 50})`);
|
777
|
+
|
778
|
+
// Add circles for nodes
|
779
|
+
node.append('circle')
|
780
|
+
.attr('r', this.nodeRadius)
|
781
|
+
.style('fill', d => this.getNodeColor(d.data))
|
782
|
+
.style('stroke', d => d.data.edited ? '#F44336' : '#999')
|
783
|
+
.style('stroke-width', d => d.data.edited ? 2 : 1)
|
784
|
+
.style('cursor', 'pointer')
|
785
|
+
.on('click', (event, d) => this.handleNodeClick(event, d));
|
786
|
+
|
787
|
+
// Add text labels
|
788
|
+
node.append('text')
|
789
|
+
.attr('dy', '.35em')
|
790
|
+
.attr('x', d => d.children ? -10 : 10)
|
791
|
+
.style('text-anchor', d => d.children ? 'end' : 'start')
|
792
|
+
.style('font-size', '12px')
|
793
|
+
.style('cursor', 'pointer')
|
794
|
+
.text(d => d.data.name)
|
795
|
+
.on('click', (event, d) => this.handleNodeClick(event, d));
|
796
|
+
|
797
|
+
// Store tree reference
|
798
|
+
this.d3Tree = { svg, g, zoom };
|
799
|
+
}
|
800
|
+
|
801
|
+
/**
|
802
|
+
* Get node color based on type
|
803
|
+
*/
|
804
|
+
getNodeColor(node) {
|
805
|
+
switch (node.type) {
|
806
|
+
case 'root': return '#666';
|
807
|
+
case 'directory': return '#FFC107';
|
808
|
+
case 'file': return '#4CAF50';
|
809
|
+
case 'class': return '#2196F3';
|
810
|
+
case 'function': return '#FF9800';
|
811
|
+
case 'method': return '#9C27B0';
|
812
|
+
default: return '#999';
|
813
|
+
}
|
814
|
+
}
|
815
|
+
|
816
|
+
/**
|
817
|
+
* Handle node click
|
818
|
+
*/
|
819
|
+
handleNodeClick(event, d) {
|
820
|
+
event.stopPropagation();
|
821
|
+
|
822
|
+
// Toggle children
|
823
|
+
if (d.children) {
|
824
|
+
d._children = d.children;
|
825
|
+
d.children = null;
|
826
|
+
} else if (d._children) {
|
827
|
+
d.children = d._children;
|
828
|
+
d._children = null;
|
829
|
+
}
|
830
|
+
|
831
|
+
// Re-render tree
|
832
|
+
this.renderTree();
|
833
|
+
|
834
|
+
// Update selection
|
835
|
+
this.selectedNode = d;
|
836
|
+
|
837
|
+
// Update the data viewer in the left pane if it's a file
|
838
|
+
if (d.data.type === 'file' && this.fileActivity.has(d.data.path)) {
|
839
|
+
this.showFileDetails(d.data.path);
|
840
|
+
}
|
841
|
+
}
|
842
|
+
|
843
|
+
/**
|
844
|
+
* Show file details in the left viewer pane
|
845
|
+
*/
|
846
|
+
showFileDetails(filePath) {
|
847
|
+
const activity = this.fileActivity.get(filePath);
|
848
|
+
if (!activity) return;
|
849
|
+
|
850
|
+
const dataContent = document.getElementById('module-data-content');
|
851
|
+
if (!dataContent) return;
|
852
|
+
|
853
|
+
// Update header
|
854
|
+
const dataHeader = document.querySelector('.module-data-header h5');
|
855
|
+
if (dataHeader) {
|
856
|
+
dataHeader.innerHTML = `📄 ${filePath.split('/').pop()}`;
|
857
|
+
}
|
858
|
+
|
859
|
+
// Build operations display
|
860
|
+
let html = '<div style="padding: 10px; overflow-y: auto; height: 100%;">';
|
861
|
+
html += `<div style="margin-bottom: 15px;">`;
|
862
|
+
html += `<strong>File Path:</strong> ${filePath}<br>`;
|
863
|
+
html += `<strong>Operations:</strong> ${activity.operations.length}<br>`;
|
864
|
+
html += `<strong>Sessions:</strong> ${activity.sessions.size}`;
|
865
|
+
html += `</div>`;
|
866
|
+
|
867
|
+
// Show operations timeline
|
868
|
+
html += '<div style="margin-bottom: 15px;"><strong>Operations Timeline:</strong></div>';
|
869
|
+
activity.operations.forEach((op, index) => {
|
870
|
+
const time = new Date(op.timestamp).toLocaleTimeString();
|
871
|
+
html += `<div style="margin-bottom: 10px; padding: 8px; background: #f5f5f5; border-left: 3px solid ${this.getOperationColor(op.type)};">`;
|
872
|
+
html += `<div><strong>${op.type}</strong> at ${time}</div>`;
|
873
|
+
|
874
|
+
if (op.type === 'Edit' && op.parameters) {
|
875
|
+
html += `<div style="margin-top: 5px; font-size: 0.9em;">`;
|
876
|
+
html += `<div style="color: #d32f2f;">- ${this.escapeHtml(op.parameters.old_string || '').substring(0, 100)}</div>`;
|
877
|
+
html += `<div style="color: #388e3c;">+ ${this.escapeHtml(op.parameters.new_string || '').substring(0, 100)}</div>`;
|
878
|
+
html += `</div>`;
|
879
|
+
}
|
880
|
+
html += `</div>`;
|
881
|
+
});
|
882
|
+
|
883
|
+
// Show AST structure if available
|
884
|
+
if (activity.astPaths.length > 0) {
|
885
|
+
html += '<div style="margin-top: 15px;"><strong>AST Structure:</strong></div>';
|
886
|
+
html += '<ul style="list-style: none; padding-left: 10px;">';
|
887
|
+
activity.astPaths.forEach(ast => {
|
888
|
+
const icon = ast.type === 'class' ? '🔷' : ast.type === 'function' ? '🔶' : '🔸';
|
889
|
+
html += `<li>${icon} ${ast.name} (${ast.type})</li>`;
|
890
|
+
});
|
891
|
+
html += '</ul>';
|
892
|
+
}
|
893
|
+
|
894
|
+
html += '</div>';
|
895
|
+
dataContent.innerHTML = html;
|
896
|
+
}
|
897
|
+
|
898
|
+
/**
|
899
|
+
* Get operation color
|
900
|
+
*/
|
901
|
+
getOperationColor(type) {
|
902
|
+
switch (type) {
|
903
|
+
case 'Write': return '#4CAF50';
|
904
|
+
case 'Edit': return '#FF9800';
|
905
|
+
case 'Read': return '#2196F3';
|
906
|
+
default: return '#999';
|
907
|
+
}
|
908
|
+
}
|
909
|
+
|
910
|
+
/**
|
911
|
+
* Escape HTML
|
912
|
+
*/
|
913
|
+
escapeHtml(text) {
|
914
|
+
const div = document.createElement('div');
|
915
|
+
div.textContent = text;
|
916
|
+
return div.innerHTML;
|
917
|
+
}
|
918
|
+
|
919
|
+
/**
|
920
|
+
* Expand all nodes
|
921
|
+
*/
|
922
|
+
expandAllNodes() {
|
923
|
+
if (!this.d3Root) return;
|
924
|
+
|
925
|
+
this.d3Root.descendants().forEach(d => {
|
926
|
+
if (d._children) {
|
927
|
+
d.children = d._children;
|
928
|
+
d._children = null;
|
929
|
+
}
|
930
|
+
});
|
931
|
+
|
932
|
+
this.renderTree();
|
933
|
+
}
|
934
|
+
|
935
|
+
/**
|
936
|
+
* Collapse all nodes
|
937
|
+
*/
|
938
|
+
collapseAllNodes() {
|
939
|
+
if (!this.d3Root) return;
|
940
|
+
|
941
|
+
this.d3Root.descendants().forEach(d => {
|
942
|
+
if (d.children && d.depth > 0) {
|
943
|
+
d._children = d.children;
|
944
|
+
d.children = null;
|
945
|
+
}
|
946
|
+
});
|
947
|
+
|
948
|
+
this.renderTree();
|
949
|
+
}
|
950
|
+
|
951
|
+
/**
|
952
|
+
* Reset zoom
|
953
|
+
*/
|
954
|
+
resetZoom() {
|
955
|
+
if (!this.d3Tree) return;
|
956
|
+
|
957
|
+
this.d3Tree.svg.transition()
|
958
|
+
.duration(750)
|
959
|
+
.call(this.d3Tree.zoom.transform, d3.zoomIdentity);
|
960
|
+
}
|
961
|
+
|
962
|
+
/**
|
963
|
+
* Update session list in main selector
|
964
|
+
*/
|
965
|
+
updateSessionList() {
|
966
|
+
// Update the main session selector if it exists
|
967
|
+
const mainSelect = document.getElementById('session-select');
|
968
|
+
if (!mainSelect) return;
|
969
|
+
|
970
|
+
const currentValue = mainSelect.value;
|
971
|
+
|
972
|
+
// Clear existing options except "All Sessions"
|
973
|
+
while (mainSelect.options.length > 1) {
|
974
|
+
mainSelect.remove(1);
|
975
|
+
}
|
976
|
+
|
977
|
+
// Add session options from our tracked sessions
|
978
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
979
|
+
// Check if option already exists
|
980
|
+
let exists = false;
|
981
|
+
for (let i = 0; i < mainSelect.options.length; i++) {
|
982
|
+
if (mainSelect.options[i].value === sessionId) {
|
983
|
+
exists = true;
|
984
|
+
break;
|
985
|
+
}
|
986
|
+
}
|
987
|
+
|
988
|
+
if (!exists) {
|
989
|
+
const option = document.createElement('option');
|
990
|
+
option.value = sessionId;
|
991
|
+
option.textContent = `Session ${sessionId.substring(0, 8)}... (${session.files.size} files)`;
|
992
|
+
mainSelect.appendChild(option);
|
993
|
+
}
|
994
|
+
}
|
995
|
+
|
996
|
+
// Restore previous selection if it still exists
|
997
|
+
if (currentValue) {
|
998
|
+
mainSelect.value = currentValue;
|
999
|
+
}
|
1000
|
+
}
|
1001
|
+
|
1002
|
+
/**
|
1003
|
+
* Update statistics
|
1004
|
+
*/
|
1005
|
+
updateStats() {
|
1006
|
+
const stats = document.getElementById('claude-tree-stats');
|
1007
|
+
if (!stats) return;
|
1008
|
+
|
1009
|
+
const totalFiles = this.currentSession
|
1010
|
+
? Array.from(this.fileActivity.values()).filter(a => a.sessions.has(this.currentSession)).length
|
1011
|
+
: this.fileActivity.size;
|
1012
|
+
|
1013
|
+
const totalOps = this.currentSession
|
1014
|
+
? Array.from(this.fileActivity.values())
|
1015
|
+
.filter(a => a.sessions.has(this.currentSession))
|
1016
|
+
.reduce((sum, a) => sum + a.operations.length, 0)
|
1017
|
+
: Array.from(this.fileActivity.values())
|
1018
|
+
.reduce((sum, a) => sum + a.operations.length, 0);
|
1019
|
+
|
1020
|
+
stats.textContent = `Files: ${totalFiles} | Operations: ${totalOps} | Sessions: ${this.sessions.size}`;
|
1021
|
+
}
|
1022
|
+
}
|
1023
|
+
|
1024
|
+
// Create and export singleton instance
|
1025
|
+
window.CodeViewer = new CodeViewer();
|
1026
|
+
|
1027
|
+
// Auto-initialize when DOM is ready
|
1028
|
+
if (document.readyState === 'loading') {
|
1029
|
+
document.addEventListener('DOMContentLoaded', () => {
|
1030
|
+
window.CodeViewer.initialize();
|
1031
|
+
|
1032
|
+
// If Claude Tree tab is already active, show it
|
1033
|
+
const claudeTreeTab = document.getElementById('claude-tree-tab');
|
1034
|
+
if (claudeTreeTab && claudeTreeTab.classList.contains('active')) {
|
1035
|
+
console.log('[CodeViewer] Claude Tree tab is active on load, showing tree...');
|
1036
|
+
setTimeout(() => window.CodeViewer.show(), 100);
|
1037
|
+
}
|
1038
|
+
});
|
1039
|
+
} else {
|
1040
|
+
window.CodeViewer.initialize();
|
1041
|
+
|
1042
|
+
// If Claude Tree tab is already active, show it
|
1043
|
+
const claudeTreeTab = document.getElementById('claude-tree-tab');
|
1044
|
+
if (claudeTreeTab && claudeTreeTab.classList.contains('active')) {
|
1045
|
+
console.log('[CodeViewer] Claude Tree tab is active, showing tree...');
|
1046
|
+
setTimeout(() => window.CodeViewer.show(), 100);
|
1047
|
+
}
|
1048
|
+
}
|
1049
|
+
|
1050
|
+
// Also listen for tab changes to ensure we render when needed
|
1051
|
+
document.addEventListener('tabChanged', (event) => {
|
1052
|
+
if (event.detail && event.detail.newTab === 'claude-tree') {
|
1053
|
+
console.log('[CodeViewer] Tab changed to Claude Tree, forcing show...');
|
1054
|
+
setTimeout(() => window.CodeViewer.show(), 50);
|
1055
|
+
}
|
1056
|
+
});
|
1057
|
+
|
1058
|
+
// ADDITIONAL: Listen for clicks on the Claude Tree tab button directly
|
1059
|
+
document.addEventListener('click', (event) => {
|
1060
|
+
if (event.target && event.target.matches('[data-tab="claude-tree"]')) {
|
1061
|
+
console.log('[CodeViewer] Direct click on Claude Tree tab detected, forcing show...');
|
1062
|
+
setTimeout(() => window.CodeViewer.show(), 100);
|
1063
|
+
}
|
1064
|
+
});
|
1065
|
+
|
1066
|
+
// FALLBACK: Periodic check to ensure Claude Tree tab is properly rendered
|
1067
|
+
setInterval(() => {
|
1068
|
+
const claudeTreeTab = document.getElementById('claude-tree-tab');
|
1069
|
+
const claudeTreeContainer = document.getElementById('claude-tree-container');
|
1070
|
+
|
1071
|
+
if (claudeTreeTab && claudeTreeTab.classList.contains('active') &&
|
1072
|
+
claudeTreeContainer && !claudeTreeContainer.querySelector('.activity-tree-wrapper')) {
|
1073
|
+
console.log('[CodeViewer] Periodic check: Claude Tree tab is active but not properly rendered, fixing...');
|
1074
|
+
window.CodeViewer.show();
|
1075
|
+
}
|
1076
|
+
}, 5000);
|