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,17 +1,29 @@
|
|
1
1
|
/**
|
2
|
-
* Code Viewer Component
|
2
|
+
* Code Viewer Component - File Activity Tree Viewer
|
3
3
|
*
|
4
|
-
*
|
5
|
-
*
|
4
|
+
* Shows a D3.js tree visualization of files that have been 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 File Tree tab of the dashboard.
|
6
8
|
*/
|
7
9
|
|
8
10
|
class CodeViewer {
|
9
11
|
constructor() {
|
10
|
-
this.
|
11
|
-
this.
|
12
|
-
this.socket = null;
|
12
|
+
this.container = null;
|
13
|
+
this.svg = null;
|
13
14
|
this.initialized = false;
|
14
|
-
this.
|
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;
|
15
27
|
}
|
16
28
|
|
17
29
|
/**
|
@@ -19,462 +31,1041 @@ class CodeViewer {
|
|
19
31
|
*/
|
20
32
|
initialize() {
|
21
33
|
if (this.initialized) {
|
34
|
+
console.log('[CodeViewer] Already initialized, skipping');
|
22
35
|
return;
|
23
36
|
}
|
24
37
|
|
25
|
-
|
38
|
+
console.log('[CodeViewer] Initializing...');
|
39
|
+
this.setupContainer();
|
26
40
|
this.setupEventHandlers();
|
27
41
|
this.subscribeToEvents();
|
42
|
+
this.processExistingEvents();
|
28
43
|
|
29
44
|
this.initialized = true;
|
30
|
-
console.log('Code
|
45
|
+
console.log('[CodeViewer] Code Viewer (File Activity Tree) initialized successfully');
|
31
46
|
}
|
32
47
|
|
33
48
|
/**
|
34
|
-
*
|
49
|
+
* Setup the container in the File Tree tab
|
35
50
|
*/
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
51
|
+
setupContainer() {
|
52
|
+
// Find the File Tree tab container
|
53
|
+
const treeContainer = document.getElementById('claude-tree-container');
|
54
|
+
if (!treeContainer) {
|
55
|
+
console.error('File 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 File 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>
|
80
119
|
</div>
|
81
120
|
</div>
|
82
121
|
`;
|
83
122
|
|
84
|
-
//
|
85
|
-
document.
|
86
|
-
|
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
|
+
}
|
87
141
|
}
|
88
142
|
|
89
143
|
/**
|
90
|
-
*
|
144
|
+
* Render the content without switching tabs
|
145
|
+
* This is called by UIStateManager when the tab is already active
|
91
146
|
*/
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
147
|
+
renderContent() {
|
148
|
+
console.log('[CodeViewer] renderContent() called');
|
149
|
+
this._showInternal();
|
150
|
+
}
|
151
|
+
|
152
|
+
/**
|
153
|
+
* Show the activity tree (for backward compatibility)
|
154
|
+
* Note: Tab switching is now handled by UIStateManager
|
155
|
+
*/
|
156
|
+
show() {
|
157
|
+
console.log('[CodeViewer] show() called');
|
158
|
+
this._showInternal();
|
159
|
+
}
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Internal show implementation (without tab switching)
|
163
|
+
*/
|
164
|
+
_showInternal() {
|
165
|
+
|
166
|
+
// Get the file tree container
|
167
|
+
const claudeTreeContainer = document.getElementById('claude-tree-container');
|
168
|
+
if (!claudeTreeContainer) {
|
169
|
+
console.error('[CodeViewer] File Tree container not found!');
|
170
|
+
return;
|
171
|
+
}
|
97
172
|
|
98
|
-
//
|
99
|
-
|
100
|
-
|
101
|
-
|
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();
|
102
193
|
}
|
103
|
-
}
|
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
|
+
}
|
104
219
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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 File 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
|
+
}
|
109
317
|
}
|
110
318
|
});
|
111
|
-
|
112
|
-
//
|
113
|
-
|
114
|
-
|
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
|
115
324
|
});
|
325
|
+
|
326
|
+
console.log('[CodeViewer] Container protection enabled with aggressive filtering');
|
327
|
+
}
|
116
328
|
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
+
}
|
120
347
|
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
+
}
|
124
356
|
|
125
|
-
//
|
126
|
-
document.getElementById('
|
127
|
-
|
128
|
-
|
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
|
+
}
|
129
365
|
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
133
381
|
}
|
134
382
|
|
135
383
|
/**
|
136
|
-
* Subscribe to
|
384
|
+
* Subscribe to events from socket and event bus
|
137
385
|
*/
|
138
386
|
subscribeToEvents() {
|
387
|
+
// Listen for claude events from socket
|
139
388
|
if (window.socket) {
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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 File 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 File Tree tab is active
|
410
|
+
if (this.isTabActive()) {
|
411
|
+
this.buildTreeData();
|
412
|
+
this.renderTree();
|
413
|
+
this.updateStats();
|
414
|
+
}
|
415
|
+
}
|
145
416
|
});
|
146
417
|
}
|
147
418
|
}
|
148
419
|
|
149
420
|
/**
|
150
|
-
*
|
421
|
+
* Check if File Tree tab is active
|
151
422
|
*/
|
152
|
-
|
153
|
-
|
154
|
-
|
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);
|
155
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;
|
156
469
|
|
157
|
-
|
158
|
-
|
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 || '/';
|
159
478
|
|
160
|
-
|
161
|
-
this.updateHeader(nodeData);
|
479
|
+
const filePath = tool_parameters.file_path || tool_parameters.notebook_path;
|
162
480
|
|
163
|
-
|
164
|
-
this.loadCode(nodeData);
|
481
|
+
console.log('[CodeViewer] Processing file operation:', tool_name, filePath);
|
165
482
|
|
166
|
-
|
167
|
-
|
483
|
+
this.processFileOperation({
|
484
|
+
tool_name,
|
485
|
+
tool_parameters,
|
486
|
+
tool_output,
|
487
|
+
timestamp,
|
488
|
+
session_id,
|
489
|
+
working_directory,
|
490
|
+
filePath
|
491
|
+
});
|
168
492
|
}
|
169
493
|
|
170
494
|
/**
|
171
|
-
*
|
495
|
+
* Process a file operation event (legacy format)
|
172
496
|
*/
|
173
|
-
|
174
|
-
this.
|
175
|
-
|
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
|
+
});
|
176
512
|
}
|
177
513
|
|
178
514
|
/**
|
179
|
-
*
|
515
|
+
* Process a file operation
|
180
516
|
*/
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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);
|
185
544
|
|
186
|
-
//
|
187
|
-
|
188
|
-
|
189
|
-
|
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)
|
190
584
|
}
|
191
585
|
|
192
586
|
/**
|
193
|
-
*
|
587
|
+
* Extract AST paths from code content
|
194
588
|
*/
|
195
|
-
|
196
|
-
|
589
|
+
extractASTPaths(content, filePath) {
|
590
|
+
if (!content || typeof content !== 'string') return [];
|
197
591
|
|
198
|
-
|
199
|
-
const
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
+
}
|
203
630
|
}
|
631
|
+
|
632
|
+
return paths;
|
633
|
+
}
|
634
|
+
|
635
|
+
/**
|
636
|
+
* Build tree data from file activity
|
637
|
+
*/
|
638
|
+
buildTreeData() {
|
639
|
+
const root = {
|
640
|
+
name: 'File 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());
|
204
699
|
|
205
|
-
//
|
206
|
-
|
207
|
-
|
208
|
-
// Request code from server
|
209
|
-
if (this.socket) {
|
210
|
-
this.socket.emit('code:content:request', {
|
211
|
-
path: nodeData.path,
|
212
|
-
line: nodeData.line,
|
213
|
-
type: nodeData.type,
|
214
|
-
name: nodeData.name
|
215
|
-
});
|
216
|
-
} else {
|
217
|
-
// Fallback: show mock code for demo
|
218
|
-
this.displayMockCode(nodeData);
|
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;
|
219
703
|
}
|
704
|
+
|
705
|
+
this.treeData = root;
|
220
706
|
}
|
221
707
|
|
222
708
|
/**
|
223
|
-
*
|
709
|
+
* Render the D3 tree
|
224
710
|
*/
|
225
|
-
|
226
|
-
if (!
|
227
|
-
|
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');
|
228
724
|
return;
|
229
725
|
}
|
230
726
|
|
231
|
-
|
232
|
-
|
233
|
-
|
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);
|
234
755
|
|
235
|
-
//
|
236
|
-
this.
|
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 };
|
237
799
|
}
|
238
800
|
|
239
801
|
/**
|
240
|
-
*
|
802
|
+
* Get node color based on type
|
241
803
|
*/
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
codeContent.className = `language-${language}`;
|
252
|
-
|
253
|
-
// Apply Prism syntax highlighting
|
254
|
-
if (window.Prism) {
|
255
|
-
Prism.highlightElement(codeContent);
|
256
|
-
|
257
|
-
// Add line numbers if plugin is available
|
258
|
-
if (Prism.plugins && Prism.plugins.lineNumbers) {
|
259
|
-
Prism.plugins.lineNumbers.resize(codeElement);
|
260
|
-
}
|
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';
|
261
813
|
}
|
262
814
|
}
|
263
815
|
|
264
816
|
/**
|
265
|
-
*
|
817
|
+
* Handle node click
|
266
818
|
*/
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
def __init__(self):
|
278
|
-
"""Initialize the ${nodeData.name} class."""
|
279
|
-
self._data = {}
|
280
|
-
self._initialized = False
|
281
|
-
|
282
|
-
def process(self, input_data):
|
283
|
-
"""Process the input data."""
|
284
|
-
if not self._initialized:
|
285
|
-
self._initialize()
|
286
|
-
return self._transform(input_data)
|
287
|
-
|
288
|
-
def _initialize(self):
|
289
|
-
"""Initialize internal state."""
|
290
|
-
self._initialized = True
|
291
|
-
|
292
|
-
def _transform(self, data):
|
293
|
-
"""Transform the data."""
|
294
|
-
return data`;
|
295
|
-
break;
|
296
|
-
|
297
|
-
case 'function':
|
298
|
-
mockCode = `def ${nodeData.name}(${nodeData.params ? nodeData.params.join(', ') : ''}):
|
299
|
-
"""
|
300
|
-
${nodeData.docstring || 'A sample function implementation.'}
|
301
|
-
|
302
|
-
Args:
|
303
|
-
${nodeData.params ? nodeData.params.map(p => `${p}: Description of ${p}`).join('\n ') : 'None'}
|
304
|
-
|
305
|
-
Returns:
|
306
|
-
${nodeData.returns || 'None'}: Return value description
|
307
|
-
"""
|
308
|
-
# Implementation here
|
309
|
-
result = None
|
310
|
-
|
311
|
-
# Process logic
|
312
|
-
for item in range(10):
|
313
|
-
result = process_item(item)
|
314
|
-
|
315
|
-
return result`;
|
316
|
-
break;
|
317
|
-
|
318
|
-
case 'method':
|
319
|
-
mockCode = ` def ${nodeData.name}(self${nodeData.params ? ', ' + nodeData.params.join(', ') : ''}):
|
320
|
-
"""
|
321
|
-
${nodeData.docstring || 'A sample method implementation.'}
|
322
|
-
"""
|
323
|
-
# Method implementation
|
324
|
-
self._validate()
|
325
|
-
result = self._process()
|
326
|
-
return result`;
|
327
|
-
break;
|
328
|
-
|
329
|
-
default:
|
330
|
-
mockCode = `# ${nodeData.name}
|
331
|
-
# Type: ${nodeData.type}
|
332
|
-
# Path: ${nodeData.path || 'Unknown'}
|
333
|
-
# Line: ${nodeData.line || 'Unknown'}
|
334
|
-
|
335
|
-
# Code content would appear here
|
336
|
-
# This is a placeholder for demonstration purposes`;
|
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;
|
337
829
|
}
|
830
|
+
|
831
|
+
// Re-render tree
|
832
|
+
this.renderTree();
|
833
|
+
|
834
|
+
// Update selection
|
835
|
+
this.selectedNode = d;
|
338
836
|
|
339
|
-
|
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
|
+
}
|
340
841
|
}
|
341
842
|
|
342
843
|
/**
|
343
|
-
*
|
844
|
+
* Show file details in the left viewer pane
|
344
845
|
*/
|
345
|
-
|
346
|
-
const
|
347
|
-
|
348
|
-
|
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;
|
349
896
|
}
|
350
897
|
|
351
898
|
/**
|
352
|
-
*
|
899
|
+
* Get operation color
|
353
900
|
*/
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
'ts': 'typescript',
|
362
|
-
'jsx': 'jsx',
|
363
|
-
'tsx': 'tsx',
|
364
|
-
'css': 'css',
|
365
|
-
'html': 'html',
|
366
|
-
'json': 'json',
|
367
|
-
'yaml': 'yaml',
|
368
|
-
'yml': 'yaml',
|
369
|
-
'md': 'markdown',
|
370
|
-
'sh': 'bash',
|
371
|
-
'bash': 'bash',
|
372
|
-
'sql': 'sql',
|
373
|
-
'go': 'go',
|
374
|
-
'rs': 'rust',
|
375
|
-
'cpp': 'cpp',
|
376
|
-
'c': 'c',
|
377
|
-
'h': 'c',
|
378
|
-
'hpp': 'cpp',
|
379
|
-
'java': 'java',
|
380
|
-
'rb': 'ruby',
|
381
|
-
'php': 'php'
|
382
|
-
};
|
383
|
-
|
384
|
-
return languageMap[ext] || 'plaintext';
|
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
|
+
}
|
385
908
|
}
|
386
909
|
|
387
910
|
/**
|
388
|
-
*
|
911
|
+
* Escape HTML
|
389
912
|
*/
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
document.getElementById('code-nav-prev').disabled = true;
|
395
|
-
document.getElementById('code-nav-next').disabled = true;
|
396
|
-
document.getElementById('code-nav-position').textContent = '1 / 1';
|
913
|
+
escapeHtml(text) {
|
914
|
+
const div = document.createElement('div');
|
915
|
+
div.textContent = text;
|
916
|
+
return div.innerHTML;
|
397
917
|
}
|
398
918
|
|
399
919
|
/**
|
400
|
-
*
|
920
|
+
* Expand all nodes
|
401
921
|
*/
|
402
|
-
|
403
|
-
|
404
|
-
|
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();
|
405
933
|
}
|
406
934
|
|
407
935
|
/**
|
408
|
-
*
|
936
|
+
* Collapse all nodes
|
409
937
|
*/
|
410
|
-
|
411
|
-
|
412
|
-
|
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();
|
413
949
|
}
|
414
950
|
|
415
951
|
/**
|
416
|
-
*
|
952
|
+
* Reset zoom
|
417
953
|
*/
|
418
|
-
|
419
|
-
|
420
|
-
|
954
|
+
resetZoom() {
|
955
|
+
if (!this.d3Tree) return;
|
956
|
+
|
957
|
+
this.d3Tree.svg.transition()
|
958
|
+
.duration(750)
|
959
|
+
.call(this.d3Tree.zoom.transform, d3.zoomIdentity);
|
421
960
|
}
|
422
961
|
|
423
962
|
/**
|
424
|
-
*
|
963
|
+
* Update session list in main selector
|
425
964
|
*/
|
426
|
-
|
427
|
-
|
428
|
-
const
|
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;
|
429
971
|
|
430
|
-
|
431
|
-
|
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
|
+
}
|
432
987
|
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
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;
|
443
999
|
}
|
444
1000
|
}
|
445
1001
|
|
446
1002
|
/**
|
447
|
-
*
|
1003
|
+
* Update statistics
|
448
1004
|
*/
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
if (this.socket) {
|
457
|
-
this.socket.emit('file:open', {
|
458
|
-
path: this.currentNode.path,
|
459
|
-
line: this.currentNode.line
|
460
|
-
});
|
461
|
-
}
|
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;
|
462
1012
|
|
463
|
-
|
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}`;
|
464
1021
|
}
|
465
1022
|
}
|
466
1023
|
|
467
|
-
// Create singleton instance
|
468
|
-
|
1024
|
+
// Create and export singleton instance
|
1025
|
+
window.CodeViewer = new CodeViewer();
|
469
1026
|
|
470
|
-
//
|
471
|
-
if (
|
472
|
-
window.CodeViewer = codeViewer;
|
473
|
-
|
474
|
-
// Initialize when DOM is ready
|
1027
|
+
// Auto-initialize when DOM is ready
|
1028
|
+
if (document.readyState === 'loading') {
|
475
1029
|
document.addEventListener('DOMContentLoaded', () => {
|
476
|
-
|
1030
|
+
window.CodeViewer.initialize();
|
1031
|
+
|
1032
|
+
// If File 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] File Tree tab is active on load, showing tree...');
|
1036
|
+
setTimeout(() => window.CodeViewer.show(), 100);
|
1037
|
+
}
|
477
1038
|
});
|
1039
|
+
} else {
|
1040
|
+
window.CodeViewer.initialize();
|
1041
|
+
|
1042
|
+
// If File 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] File Tree tab is active, showing tree...');
|
1046
|
+
setTimeout(() => window.CodeViewer.show(), 100);
|
1047
|
+
}
|
478
1048
|
}
|
479
1049
|
|
480
|
-
|
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 File Tree, forcing show...');
|
1054
|
+
setTimeout(() => window.CodeViewer.show(), 50);
|
1055
|
+
}
|
1056
|
+
});
|
1057
|
+
|
1058
|
+
// Tab click handling is now done by UIStateManager
|
1059
|
+
// CodeViewer.renderContent() is called when the File Tree tab is activated
|
1060
|
+
|
1061
|
+
// FALLBACK: Periodic check to ensure File Tree tab is properly rendered
|
1062
|
+
setInterval(() => {
|
1063
|
+
const claudeTreeTab = document.getElementById('claude-tree-tab');
|
1064
|
+
const claudeTreeContainer = document.getElementById('claude-tree-container');
|
1065
|
+
|
1066
|
+
if (claudeTreeTab && claudeTreeTab.classList.contains('active') &&
|
1067
|
+
claudeTreeContainer && !claudeTreeContainer.querySelector('.activity-tree-wrapper')) {
|
1068
|
+
console.log('[CodeViewer] Periodic check: File Tree tab is active but not properly rendered, fixing...');
|
1069
|
+
window.CodeViewer.show();
|
1070
|
+
}
|
1071
|
+
}, 5000);
|