claude-mpm 4.1.10__py3-none-any.whl → 4.1.12__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.
Files changed (56) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +8 -0
  3. claude_mpm/cli/__init__.py +11 -0
  4. claude_mpm/cli/commands/analyze.py +2 -1
  5. claude_mpm/cli/commands/configure.py +9 -8
  6. claude_mpm/cli/commands/configure_tui.py +3 -1
  7. claude_mpm/cli/commands/dashboard.py +288 -0
  8. claude_mpm/cli/commands/debug.py +0 -1
  9. claude_mpm/cli/commands/mpm_init.py +442 -0
  10. claude_mpm/cli/commands/mpm_init_handler.py +84 -0
  11. claude_mpm/cli/parsers/base_parser.py +15 -0
  12. claude_mpm/cli/parsers/dashboard_parser.py +113 -0
  13. claude_mpm/cli/parsers/mpm_init_parser.py +128 -0
  14. claude_mpm/constants.py +10 -0
  15. claude_mpm/core/config.py +18 -0
  16. claude_mpm/core/instruction_reinforcement_hook.py +266 -0
  17. claude_mpm/core/pm_hook_interceptor.py +105 -8
  18. claude_mpm/dashboard/analysis_runner.py +52 -25
  19. claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
  20. claude_mpm/dashboard/static/built/components/code-tree.js +2 -0
  21. claude_mpm/dashboard/static/built/components/code-viewer.js +2 -0
  22. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  23. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  24. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  25. claude_mpm/dashboard/static/css/code-tree.css +330 -1
  26. claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
  27. claude_mpm/dashboard/static/dist/components/code-tree.js +2593 -2
  28. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  29. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  30. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  31. claude_mpm/dashboard/static/js/components/activity-tree.js +212 -13
  32. claude_mpm/dashboard/static/js/components/build-tracker.js +15 -13
  33. claude_mpm/dashboard/static/js/components/code-tree.js +2503 -917
  34. claude_mpm/dashboard/static/js/components/event-viewer.js +58 -19
  35. claude_mpm/dashboard/static/js/dashboard.js +46 -44
  36. claude_mpm/dashboard/static/js/socket-client.js +74 -32
  37. claude_mpm/dashboard/templates/index.html +25 -20
  38. claude_mpm/services/agents/deployment/agent_template_builder.py +11 -7
  39. claude_mpm/services/agents/memory/memory_format_service.py +3 -1
  40. claude_mpm/services/cli/agent_cleanup_service.py +1 -4
  41. claude_mpm/services/cli/socketio_manager.py +39 -8
  42. claude_mpm/services/cli/startup_checker.py +0 -1
  43. claude_mpm/services/core/cache_manager.py +0 -1
  44. claude_mpm/services/infrastructure/monitoring.py +1 -1
  45. claude_mpm/services/socketio/event_normalizer.py +64 -0
  46. claude_mpm/services/socketio/handlers/code_analysis.py +449 -0
  47. claude_mpm/services/socketio/server/connection_manager.py +3 -1
  48. claude_mpm/tools/code_tree_analyzer.py +930 -24
  49. claude_mpm/tools/code_tree_builder.py +0 -1
  50. claude_mpm/tools/code_tree_events.py +113 -15
  51. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/METADATA +2 -1
  52. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/RECORD +56 -48
  53. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/WHEEL +0 -0
  54. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/entry_points.txt +0 -0
  55. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/licenses/LICENSE +0 -0
  56. {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/top_level.txt +0 -0
@@ -22,9 +22,12 @@ class CodeTree {
22
22
  methods: 0,
23
23
  lines: 0
24
24
  };
25
- this.margin = {top: 20, right: 150, bottom: 20, left: 150};
25
+ // Radial layout settings
26
+ this.isRadialLayout = true; // Toggle for radial vs linear layout
27
+ this.margin = {top: 20, right: 20, bottom: 20, left: 20};
26
28
  this.width = 960 - this.margin.left - this.margin.right;
27
29
  this.height = 600 - this.margin.top - this.margin.bottom;
30
+ this.radius = Math.min(this.width, this.height) / 2;
28
31
  this.nodeId = 0;
29
32
  this.duration = 750;
30
33
  this.languageFilter = 'all';
@@ -34,16 +37,17 @@ class CodeTree {
34
37
  this.analyzing = false;
35
38
  this.selectedNode = null;
36
39
  this.socket = null;
40
+ this.autoDiscovered = false; // Track if auto-discovery has been done
41
+ this.zoom = null; // Store zoom behavior
42
+ this.activeNode = null; // Track currently active node
43
+ this.loadingNodes = new Set(); // Track nodes that are loading
37
44
  }
38
45
 
39
46
  /**
40
47
  * Initialize the code tree visualization
41
48
  */
42
49
  initialize() {
43
- console.log('CodeTree.initialize() called');
44
-
45
50
  if (this.initialized) {
46
- console.log('Code tree already initialized');
47
51
  return;
48
52
  }
49
53
 
@@ -53,8 +57,6 @@ class CodeTree {
53
57
  return;
54
58
  }
55
59
 
56
- console.log('Code tree container found:', this.container);
57
-
58
60
  // Check if tab is visible
59
61
  const tabPanel = document.getElementById('code-tab');
60
62
  if (!tabPanel) {
@@ -62,303 +64,415 @@ class CodeTree {
62
64
  return;
63
65
  }
64
66
 
65
- console.log('Code tab panel found, active:', tabPanel.classList.contains('active'));
67
+ // Check if working directory is set
68
+ const workingDir = this.getWorkingDirectory();
69
+ if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
70
+ this.showNoWorkingDirectoryMessage();
71
+ this.initialized = true;
72
+ return;
73
+ }
66
74
 
67
75
  // Initialize always
68
76
  this.setupControls();
69
77
  this.initializeTreeData();
70
78
  this.subscribeToEvents();
71
79
 
80
+ // Set initial status message
81
+ const breadcrumbContent = document.getElementById('breadcrumb-content');
82
+ if (breadcrumbContent && !this.analyzing) {
83
+ this.updateActivityTicker('Loading project structure...', 'info');
84
+ }
85
+
72
86
  // Only create visualization if tab is visible
73
87
  if (tabPanel.classList.contains('active')) {
74
- console.log('Tab is active, creating visualization');
75
88
  this.createVisualization();
76
89
  if (this.root && this.svg) {
77
90
  this.update(this.root);
78
91
  }
79
- } else {
80
- console.log('Tab is not active, deferring visualization');
92
+ // Auto-discover root level when tab is active
93
+ this.autoDiscoverRootLevel();
81
94
  }
82
95
 
83
96
  this.initialized = true;
84
- console.log('Code tree initialization complete');
85
97
  }
86
98
 
87
99
  /**
88
100
  * Render visualization when tab becomes visible
89
101
  */
90
102
  renderWhenVisible() {
91
- console.log('CodeTree.renderWhenVisible() called');
92
- console.log('Current state - initialized:', this.initialized, 'svg:', !!this.svg);
103
+ // Check if working directory is set
104
+ const workingDir = this.getWorkingDirectory();
105
+ if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
106
+ this.showNoWorkingDirectoryMessage();
107
+ return;
108
+ }
109
+
110
+ // If no directory message is shown, remove it
111
+ this.removeNoWorkingDirectoryMessage();
93
112
 
94
113
  if (!this.initialized) {
95
- console.log('Not initialized, calling initialize()');
96
114
  this.initialize();
97
115
  return;
98
116
  }
99
117
 
100
118
  if (!this.svg) {
101
- console.log('No SVG found, creating visualization');
102
119
  this.createVisualization();
103
120
  if (this.svg && this.treeGroup) {
104
- console.log('SVG created, updating tree');
105
121
  this.update(this.root);
106
- } else {
107
- console.log('Failed to create SVG or treeGroup');
108
122
  }
109
123
  } else {
110
- console.log('SVG exists, forcing update');
111
124
  // Force update with current data
112
125
  if (this.root && this.svg) {
113
126
  this.update(this.root);
114
127
  }
115
128
  }
129
+
130
+ // Auto-discover root level if not done yet
131
+ if (!this.autoDiscovered) {
132
+ this.autoDiscoverRootLevel();
133
+ }
116
134
  }
117
135
 
118
136
  /**
119
- * Setup control handlers
137
+ * Set up control event handlers
120
138
  */
121
139
  setupControls() {
122
- // Analyze button
123
- const analyzeBtn = document.getElementById('analyze-code');
124
- if (analyzeBtn) {
125
- analyzeBtn.addEventListener('click', () => this.startAnalysis());
126
- }
127
-
128
- // Cancel button
129
- const cancelBtn = document.getElementById('cancel-analysis');
130
- if (cancelBtn) {
131
- cancelBtn.addEventListener('click', () => this.cancelAnalysis());
132
- }
140
+ // Remove analyze and cancel button handlers since they're no longer in the UI
133
141
 
134
- // Expand all button
135
- const expandAllBtn = document.getElementById('code-expand-all');
136
- if (expandAllBtn) {
137
- expandAllBtn.addEventListener('click', () => this.expandAll());
142
+ const languageFilter = document.getElementById('language-filter');
143
+ if (languageFilter) {
144
+ languageFilter.addEventListener('change', (e) => {
145
+ this.languageFilter = e.target.value;
146
+ this.filterTree();
147
+ });
138
148
  }
139
149
 
140
- // Collapse all button
141
- const collapseAllBtn = document.getElementById('code-collapse-all');
142
- if (collapseAllBtn) {
143
- collapseAllBtn.addEventListener('click', () => this.collapseAll());
150
+ const searchBox = document.getElementById('code-search');
151
+ if (searchBox) {
152
+ searchBox.addEventListener('input', (e) => {
153
+ this.searchTerm = e.target.value.toLowerCase();
154
+ this.filterTree();
155
+ });
144
156
  }
145
157
 
146
- // Reset zoom button
158
+ const expandBtn = document.getElementById('code-expand-all');
159
+ if (expandBtn) {
160
+ expandBtn.addEventListener('click', () => this.expandAll());
161
+ }
162
+
163
+ const collapseBtn = document.getElementById('code-collapse-all');
164
+ if (collapseBtn) {
165
+ collapseBtn.addEventListener('click', () => this.collapseAll());
166
+ }
167
+
147
168
  const resetZoomBtn = document.getElementById('code-reset-zoom');
148
169
  if (resetZoomBtn) {
149
170
  resetZoomBtn.addEventListener('click', () => this.resetZoom());
150
171
  }
151
172
 
152
- // Toggle legend button
153
173
  const toggleLegendBtn = document.getElementById('code-toggle-legend');
154
174
  if (toggleLegendBtn) {
155
175
  toggleLegendBtn.addEventListener('click', () => this.toggleLegend());
156
176
  }
177
+
178
+ // Listen for working directory changes
179
+ document.addEventListener('workingDirectoryChanged', (e) => {
180
+ this.onWorkingDirectoryChanged(e.detail.directory);
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Handle working directory change
186
+ */
187
+ onWorkingDirectoryChanged(newDirectory) {
188
+ if (!newDirectory || newDirectory === 'Loading...' || newDirectory === 'Not selected') {
189
+ // Show no directory message
190
+ this.showNoWorkingDirectoryMessage();
191
+ // Reset tree state
192
+ this.autoDiscovered = false;
193
+ this.analyzing = false;
194
+ this.nodes.clear();
195
+ this.stats = {
196
+ files: 0,
197
+ classes: 0,
198
+ functions: 0,
199
+ methods: 0,
200
+ lines: 0
201
+ };
202
+ this.updateStats();
203
+ return;
204
+ }
205
+
206
+ // Remove any no directory message
207
+ this.removeNoWorkingDirectoryMessage();
208
+
209
+ // Reset discovery state for new directory
210
+ this.autoDiscovered = false;
211
+ this.analyzing = false;
212
+
213
+ // Clear existing data
214
+ this.nodes.clear();
215
+ this.stats = {
216
+ files: 0,
217
+ classes: 0,
218
+ functions: 0,
219
+ methods: 0,
220
+ lines: 0
221
+ };
222
+
223
+ // Re-initialize with new directory
224
+ this.initializeTreeData();
225
+ if (this.svg) {
226
+ this.update(this.root);
227
+ }
228
+
229
+ // Check if Code tab is currently active
230
+ const tabPanel = document.getElementById('code-tab');
231
+ if (tabPanel && tabPanel.classList.contains('active')) {
232
+ // Auto-discover in the new directory
233
+ this.autoDiscoverRootLevel();
234
+ }
235
+
236
+ this.updateStats();
237
+ }
157
238
 
158
- // Language filter
159
- const languageFilter = document.getElementById('language-filter');
160
- if (languageFilter) {
161
- languageFilter.addEventListener('change', (e) => {
162
- this.languageFilter = e.target.value;
163
- this.filterByLanguage();
164
- });
239
+ /**
240
+ * Show loading spinner
241
+ */
242
+ showLoading() {
243
+ let loadingDiv = document.getElementById('code-tree-loading');
244
+ if (!loadingDiv) {
245
+ // Create loading element if it doesn't exist
246
+ const container = document.getElementById('code-tree-container');
247
+ if (container) {
248
+ loadingDiv = document.createElement('div');
249
+ loadingDiv.id = 'code-tree-loading';
250
+ loadingDiv.innerHTML = `
251
+ <div class="code-tree-spinner"></div>
252
+ <div class="code-tree-loading-text">Analyzing code structure...</div>
253
+ `;
254
+ container.appendChild(loadingDiv);
255
+ }
165
256
  }
257
+ if (loadingDiv) {
258
+ loadingDiv.classList.remove('hidden');
259
+ }
260
+ }
166
261
 
167
- // Search input
168
- const searchInput = document.getElementById('code-search');
169
- if (searchInput) {
170
- searchInput.addEventListener('input', (e) => {
171
- this.searchTerm = e.target.value.toLowerCase();
172
- this.highlightSearchResults();
173
- });
262
+ /**
263
+ * Hide loading spinner
264
+ */
265
+ hideLoading() {
266
+ const loadingDiv = document.getElementById('code-tree-loading');
267
+ if (loadingDiv) {
268
+ loadingDiv.classList.add('hidden');
174
269
  }
175
270
  }
176
271
 
177
272
  /**
178
- * Create D3 visualization
273
+ * Create the D3.js visualization
179
274
  */
180
275
  createVisualization() {
181
- console.log('Creating code tree visualization');
182
-
183
- // Check if D3 is available
184
276
  if (typeof d3 === 'undefined') {
185
- console.error('D3.js is not loaded! Cannot create code tree visualization.');
277
+ console.error('D3.js is not loaded');
186
278
  return;
187
279
  }
188
-
189
- console.log('D3 is available:', typeof d3);
190
-
191
- const container = document.getElementById('code-tree');
192
- if (!container) {
193
- console.error('Code tree div not found');
280
+
281
+ const container = d3.select('#code-tree-container');
282
+ container.selectAll('*').remove();
283
+
284
+ if (!container || !container.node()) {
285
+ console.error('Code tree container not found');
194
286
  return;
195
287
  }
196
-
197
- console.log('Code tree div found:', container);
198
-
199
- // Create root if it doesn't exist
200
- if (!this.root && this.treeData) {
201
- console.log('Creating D3 hierarchy from tree data');
202
- this.root = d3.hierarchy(this.treeData);
203
- this.root.x0 = this.height / 2;
204
- this.root.y0 = 0;
205
- }
206
288
 
207
- // Clear any existing SVG
208
- d3.select(container).selectAll("*").remove();
289
+ // Calculate dimensions
290
+ const containerNode = container.node();
291
+ const containerWidth = containerNode.clientWidth || 960;
292
+ const containerHeight = containerNode.clientHeight || 600;
209
293
 
210
- // Get container dimensions
211
- const rect = container.getBoundingClientRect();
212
- this.width = rect.width - this.margin.left - this.margin.right;
213
- this.height = Math.max(500, rect.height) - this.margin.top - this.margin.bottom;
294
+ this.width = containerWidth - this.margin.left - this.margin.right;
295
+ this.height = containerHeight - this.margin.top - this.margin.bottom;
296
+ this.radius = Math.min(this.width, this.height) / 2;
214
297
 
215
298
  // Create SVG
216
- this.svg = d3.select(container)
217
- .append('svg')
218
- .attr('width', '100%')
219
- .attr('height', '100%')
220
- .attr('viewBox', `0 0 ${rect.width} ${rect.height}`)
221
- .call(d3.zoom()
222
- .scaleExtent([0.1, 3])
223
- .on('zoom', (event) => {
224
- this.treeGroup.attr('transform', event.transform);
225
- }));
299
+ this.svg = container.append('svg')
300
+ .attr('width', containerWidth)
301
+ .attr('height', containerHeight);
302
+
303
+ // Create tree group with appropriate centering
304
+ const centerX = containerWidth / 2;
305
+ const centerY = containerHeight / 2;
306
+
307
+ // Different initial positioning for different layouts
308
+ if (this.isRadialLayout) {
309
+ // Radial: center in the middle of the canvas
310
+ this.treeGroup = this.svg.append('g')
311
+ .attr('transform', `translate(${centerX},${centerY})`);
312
+ } else {
313
+ // Linear: start from left with some margin
314
+ this.treeGroup = this.svg.append('g')
315
+ .attr('transform', `translate(${this.margin.left + 100},${centerY})`);
316
+ }
317
+
318
+ // Create tree layout with improved spacing
319
+ if (this.isRadialLayout) {
320
+ // Use d3.cluster for better radial distribution
321
+ this.treeLayout = d3.cluster()
322
+ .size([2 * Math.PI, this.radius - 100])
323
+ .separation((a, b) => {
324
+ // Enhanced separation for radial layout
325
+ if (a.parent == b.parent) {
326
+ // Base separation on tree depth for better spacing
327
+ const depthFactor = Math.max(1, 4 - a.depth);
328
+ // Increase spacing for nodes with many siblings
329
+ const siblingCount = a.parent ? (a.parent.children?.length || 1) : 1;
330
+ const siblingFactor = siblingCount > 5 ? 2 : (siblingCount > 3 ? 1.5 : 1);
331
+ // More spacing at outer levels where circumference is larger
332
+ const radiusFactor = 1 + (a.depth * 0.2);
333
+ return (depthFactor * siblingFactor) / (a.depth || 1) * radiusFactor;
334
+ } else {
335
+ // Different parents - ensure enough space
336
+ return 4 / (a.depth || 1);
337
+ }
338
+ });
339
+ } else {
340
+ // Linear layout with dynamic sizing based on node count
341
+ // Use nodeSize for consistent spacing regardless of tree size
342
+ this.treeLayout = d3.tree()
343
+ .nodeSize([30, 200]) // Fixed spacing: 30px vertical, 200px horizontal
344
+ .separation((a, b) => {
345
+ // Consistent separation for linear layout
346
+ if (a.parent == b.parent) {
347
+ // Same parent - standard spacing
348
+ return 1;
349
+ } else {
350
+ // Different parents - slightly more space
351
+ return 1.5;
352
+ }
353
+ });
354
+ }
355
+
356
+ // Add zoom behavior with proper transform handling
357
+ this.zoom = d3.zoom()
358
+ .scaleExtent([0.1, 10])
359
+ .on('zoom', (event) => {
360
+ if (this.isRadialLayout) {
361
+ // Radial: maintain center point
362
+ this.treeGroup.attr('transform',
363
+ `translate(${centerX + event.transform.x},${centerY + event.transform.y}) scale(${event.transform.k})`);
364
+ } else {
365
+ // Linear: maintain left margin
366
+ this.treeGroup.attr('transform',
367
+ `translate(${this.margin.left + 100 + event.transform.x},${centerY + event.transform.y}) scale(${event.transform.k})`);
368
+ }
369
+ });
226
370
 
227
- this.treeGroup = this.svg.append('g')
228
- .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
371
+ this.svg.call(this.zoom);
229
372
 
230
- // Create tree layout
231
- this.treeLayout = d3.tree()
232
- .size([this.height, this.width]);
373
+ // Add controls overlay
374
+ this.addVisualizationControls();
233
375
 
234
376
  // Create tooltip
235
377
  this.tooltip = d3.select('body').append('div')
236
- .attr('class', 'code-tooltip')
237
- .style('opacity', 0);
378
+ .attr('class', 'code-tree-tooltip')
379
+ .style('opacity', 0)
380
+ .style('position', 'absolute')
381
+ .style('background', 'rgba(0, 0, 0, 0.8)')
382
+ .style('color', 'white')
383
+ .style('padding', '8px')
384
+ .style('border-radius', '4px')
385
+ .style('font-size', '12px')
386
+ .style('pointer-events', 'none');
238
387
  }
239
388
 
389
+ /**
390
+ * Clear all D3 visualization elements
391
+ */
392
+ clearD3Visualization() {
393
+ if (this.treeGroup) {
394
+ // Remove all existing nodes and links
395
+ this.treeGroup.selectAll('g.node').remove();
396
+ this.treeGroup.selectAll('path.link').remove();
397
+ }
398
+ // Reset node ID counter for proper tracking
399
+ this.nodeId = 0;
400
+ }
401
+
240
402
  /**
241
403
  * Initialize tree data structure
242
404
  */
243
405
  initializeTreeData() {
244
- console.log('Initializing tree data...');
406
+ const workingDir = this.getWorkingDirectory();
407
+ const dirName = workingDir ? workingDir.split('/').pop() || 'Project Root' : 'Project Root';
245
408
 
409
+ // Use '.' as the root path for consistency with relative path handling
410
+ // The actual working directory is retrieved via getWorkingDirectory() when needed
246
411
  this.treeData = {
247
- name: 'Project Root',
248
- type: 'module',
249
- path: '/',
250
- complexity: 0,
251
- children: []
412
+ name: dirName,
413
+ path: '.', // Always use '.' for root to simplify path handling
414
+ type: 'root',
415
+ children: [],
416
+ loaded: false,
417
+ expanded: true // Start expanded
252
418
  };
253
419
 
254
- // Only create D3 hierarchy if D3 is available
255
420
  if (typeof d3 !== 'undefined') {
256
421
  this.root = d3.hierarchy(this.treeData);
257
422
  this.root.x0 = this.height / 2;
258
423
  this.root.y0 = 0;
259
- console.log('Tree root created:', this.root);
260
- } else {
261
- console.warn('D3 not available yet, deferring hierarchy creation');
262
- this.root = null;
263
424
  }
264
425
  }
265
426
 
266
427
  /**
267
- * Subscribe to Socket.IO events
428
+ * Subscribe to code analysis events
268
429
  */
269
430
  subscribeToEvents() {
270
- // Try multiple socket sources
271
- this.getSocket();
272
-
273
- if (this.socket) {
274
- console.log('CodeTree: Socket available, subscribing to events');
275
-
276
- // Code analysis events - match the server-side event names
277
- this.socket.on('code:analysis:start', (data) => this.handleAnalysisStart(data));
278
- this.socket.on('code:analysis:accepted', (data) => this.handleAnalysisAccepted(data));
279
- this.socket.on('code:analysis:queued', (data) => this.handleAnalysisQueued(data));
280
- this.socket.on('code:analysis:cancelled', (data) => this.handleAnalysisCancelled(data));
281
- this.socket.on('code:analysis:progress', (data) => this.handleProgress(data));
282
- this.socket.on('code:analysis:complete', (data) => this.handleAnalysisComplete(data));
283
- this.socket.on('code:analysis:error', (data) => this.handleAnalysisError(data));
284
-
285
- // File and node events
286
- this.socket.on('code:file:start', (data) => this.handleFileStart(data));
287
- this.socket.on('code:file:complete', (data) => this.handleFileComplete(data));
288
- this.socket.on('code:node:found', (data) => this.handleNodeFound(data));
289
- } else {
290
- console.warn('CodeTree: Socket not available yet, will retry on analysis');
291
- }
292
- }
293
-
294
- /**
295
- * Get socket connection from available sources
296
- */
297
- getSocket() {
298
- // Try multiple sources for the socket
299
431
  if (!this.socket) {
300
- // Try window.socket first (most common)
301
432
  if (window.socket) {
302
433
  this.socket = window.socket;
303
- console.log('CodeTree: Using window.socket');
304
- }
305
- // Try from dashboard's socketClient
306
- else if (window.dashboard?.socketClient?.socket) {
434
+ this.setupEventHandlers();
435
+ } else if (window.dashboard?.socketClient?.socket) {
307
436
  this.socket = window.dashboard.socketClient.socket;
308
- console.log('CodeTree: Using dashboard.socketClient.socket');
309
- }
310
- // Try from socketClient directly
311
- else if (window.socketClient?.socket) {
437
+ this.setupEventHandlers();
438
+ } else if (window.socketClient?.socket) {
312
439
  this.socket = window.socketClient.socket;
313
- console.log('CodeTree: Using socketClient.socket');
440
+ this.setupEventHandlers();
314
441
  }
315
442
  }
316
- return this.socket;
317
443
  }
318
444
 
319
445
  /**
320
- * Start code analysis
446
+ * Automatically discover root-level objects when tab opens
321
447
  */
322
- startAnalysis() {
323
- if (this.analyzing) {
324
- console.log('Analysis already in progress');
448
+ autoDiscoverRootLevel() {
449
+ if (this.autoDiscovered || this.analyzing) {
325
450
  return;
326
451
  }
327
-
328
- console.log('Starting code analysis...');
329
452
 
330
- // Ensure socket is available
331
- this.getSocket();
332
- if (!this.socket) {
333
- console.error('Socket not available');
334
- this.showNotification('Cannot connect to server. Please check connection.', 'error');
453
+ // Update activity ticker
454
+ this.updateActivityTicker('🔍 Discovering project structure...', 'info');
455
+
456
+ // Get working directory
457
+ const workingDir = this.getWorkingDirectory();
458
+ if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
459
+ console.warn('Cannot auto-discover: no working directory set');
460
+ this.showNoWorkingDirectoryMessage();
335
461
  return;
336
462
  }
337
463
 
338
- // Re-subscribe to events if needed (in case socket reconnected)
339
- if (!this.socket._callbacks || !this.socket._callbacks['code:analysis:start']) {
340
- console.log('Re-subscribing to code analysis events');
341
- this.subscribeToEvents();
464
+ // Ensure we have an absolute path
465
+ if (!workingDir.startsWith('/') && !workingDir.match(/^[A-Z]:\\/)) {
466
+ console.error('Working directory is not absolute:', workingDir);
467
+ this.showNotification('Invalid working directory path', 'error');
468
+ return;
342
469
  }
343
470
 
344
- this.analyzing = true;
345
-
346
- // Update button state - but keep it responsive
347
- const analyzeBtn = document.getElementById('analyze-code');
348
- const cancelBtn = document.getElementById('cancel-analysis');
349
- if (analyzeBtn) {
350
- analyzeBtn.textContent = 'Analyzing...';
351
- analyzeBtn.classList.add('analyzing');
352
- }
353
- if (cancelBtn) {
354
- cancelBtn.style.display = 'inline-block';
355
- }
356
471
 
357
- // Show analysis status in footer
358
- this.showFooterAnalysisStatus('Starting analysis...');
472
+ this.autoDiscovered = true;
473
+ this.analyzing = true;
359
474
 
360
- // Reset tree but keep visualization
361
- this.initializeTreeData();
475
+ // Clear any existing nodes
362
476
  this.nodes.clear();
363
477
  this.stats = {
364
478
  files: 0,
@@ -367,825 +481,2039 @@ class CodeTree {
367
481
  methods: 0,
368
482
  lines: 0
369
483
  };
370
- this.updateStats();
371
484
 
372
- // Create visualization if not already created
373
- if (!this.svg) {
374
- this.createVisualization();
485
+ // Subscribe to events if not already done
486
+ if (this.socket && !this.socket.hasListeners('code:node:found')) {
487
+ this.setupEventHandlers();
375
488
  }
376
489
 
377
- // Initial tree update with empty root
378
- if (this.root && this.svg) {
379
- this.update(this.root);
490
+ // Update tree data with working directory as the root
491
+ const dirName = workingDir.split('/').pop() || 'Project Root';
492
+ this.treeData = {
493
+ name: dirName,
494
+ path: '.', // Use '.' for root to maintain consistency with relative path handling
495
+ type: 'root',
496
+ children: [],
497
+ loaded: false,
498
+ expanded: true // Start expanded to show discovered items
499
+ };
500
+
501
+ if (typeof d3 !== 'undefined') {
502
+ this.root = d3.hierarchy(this.treeData);
503
+ this.root.x0 = this.height / 2;
504
+ this.root.y0 = 0;
380
505
  }
381
506
 
382
- // Get analysis parameters from UI
383
- const pathInput = document.getElementById('analysis-path');
384
- let path = pathInput?.value?.trim();
507
+ // Update UI
508
+ this.showLoading();
509
+ this.updateBreadcrumb(`Discovering structure in ${dirName}...`, 'info');
385
510
 
386
- // Use working directory if no path specified
387
- if (!path || path === '') {
388
- // Try to get working directory from various sources
389
- path = window.workingDirectory ||
390
- window.dashboard?.workingDirectory ||
391
- window.socketClient?.sessions?.values()?.next()?.value?.working_directory ||
392
- '.';
393
-
394
- // Update the input field with the detected path
395
- if (pathInput && path !== '.') {
396
- pathInput.value = path;
397
- }
398
- }
511
+ // Get selected languages from checkboxes
512
+ const selectedLanguages = [];
513
+ document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
514
+ selectedLanguages.push(cb.value);
515
+ });
399
516
 
400
- const languages = this.getSelectedLanguages();
401
- const maxDepth = parseInt(document.getElementById('max-depth')?.value) || null;
402
- const ignorePatterns = this.getIgnorePatterns();
517
+ // Get ignore patterns
518
+ const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
403
519
 
404
- // Generate request ID
405
- const requestId = this.generateRequestId();
406
- this.currentRequestId = requestId;
520
+ // Enhanced debug logging
407
521
 
408
- // Build request payload
522
+ // Request top-level discovery with working directory
409
523
  const requestPayload = {
410
- request_id: requestId,
411
- path: path,
412
- languages: languages.length > 0 ? languages : null,
413
- max_depth: maxDepth,
414
- ignore_patterns: ignorePatterns.length > 0 ? ignorePatterns : null
524
+ path: workingDir, // Use working directory instead of '.'
525
+ depth: 'top_level',
526
+ languages: selectedLanguages,
527
+ ignore_patterns: ignorePatterns,
528
+ request_id: `discover_${Date.now()}` // Add request ID for tracking
415
529
  };
416
530
 
417
- console.log('Emitting code:analyze:request with payload:', requestPayload);
531
+ // Sending top-level discovery request
418
532
 
419
- // Request analysis from server
420
- this.socket.emit('code:analyze:request', requestPayload);
533
+ if (this.socket) {
534
+ this.socket.emit('code:discover:top_level', requestPayload);
535
+ }
421
536
 
422
- // Set a longer safety timeout (60 seconds) for truly stuck requests
423
- // This is just a safety net - normal cancellation flow should work
424
- this.requestTimeout = setTimeout(() => {
425
- if (this.analyzing && this.currentRequestId === requestId) {
426
- console.warn('Analysis appears stuck after 60 seconds');
427
- this.showNotification('Analysis is taking longer than expected. You can cancel if needed.', 'warning');
428
- // Don't auto-reset, let user decide to cancel
429
- }
430
- }, 60000); // 60 second safety timeout
537
+ // Update stats display
538
+ this.updateStats();
431
539
  }
432
540
 
433
541
  /**
434
- * Cancel current analysis
542
+ * Legacy analyzeCode method - redirects to auto-discovery
435
543
  */
436
- cancelAnalysis() {
437
- if (!this.analyzing) {
544
+ analyzeCode() {
545
+ if (this.analyzing) {
438
546
  return;
439
547
  }
440
-
441
- console.log('Cancelling analysis...');
442
-
443
- if (this.socket && this.currentRequestId) {
444
- this.socket.emit('code:analyze:cancel', {
445
- request_id: this.currentRequestId
446
- });
447
- }
448
-
449
- this.resetAnalysisState();
548
+
549
+ // Redirect to auto-discovery
550
+ this.autoDiscoverRootLevel();
450
551
  }
451
-
552
+
452
553
  /**
453
- * Reset analysis state
554
+ * Cancel ongoing analysis - removed since we no longer have a cancel button
454
555
  */
455
- resetAnalysisState() {
556
+ cancelAnalysis() {
456
557
  this.analyzing = false;
457
- this.currentRequestId = null;
458
-
459
- // Clear any timeouts
460
- if (this.requestTimeout) {
461
- clearTimeout(this.requestTimeout);
462
- this.requestTimeout = null;
463
- }
464
-
465
- // Update button state
466
- const analyzeBtn = document.getElementById('analyze-code');
467
- const cancelBtn = document.getElementById('cancel-analysis');
468
- if (analyzeBtn) {
469
- analyzeBtn.disabled = false;
470
- analyzeBtn.textContent = 'Analyze';
471
- analyzeBtn.classList.remove('analyzing');
472
- }
473
- if (cancelBtn) {
474
- cancelBtn.style.display = 'none';
558
+ this.hideLoading();
559
+
560
+ if (this.socket) {
561
+ this.socket.emit('code:analysis:cancel');
475
562
  }
476
-
477
- // Hide analysis status in footer
478
- this.hideFooterAnalysisStatus();
479
- }
480
-
481
- /**
482
- * Generate unique request ID
483
- */
484
- generateRequestId() {
485
- return `analysis-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
486
- }
487
-
488
- /**
489
- * Get selected languages from UI
490
- */
491
- getSelectedLanguages() {
492
- const languages = [];
493
- const checkboxes = document.querySelectorAll('.language-checkbox:checked');
494
- checkboxes.forEach(cb => {
495
- languages.push(cb.value);
496
- });
497
- return languages;
563
+
564
+ this.updateBreadcrumb('Analysis cancelled', 'warning');
565
+ this.showNotification('Analysis cancelled', 'warning');
566
+ this.addEventToDisplay('Analysis cancelled', 'warning');
498
567
  }
499
-
568
+
500
569
  /**
501
- * Get ignore patterns from UI
570
+ * Create the events display area
502
571
  */
503
- getIgnorePatterns() {
504
- const patterns = [];
505
- const input = document.getElementById('ignore-patterns');
506
- if (input && input.value) {
507
- patterns.push(...input.value.split(',').map(p => p.trim()).filter(p => p));
572
+ createEventsDisplay() {
573
+ let eventsContainer = document.getElementById('analysis-events');
574
+ if (!eventsContainer) {
575
+ const treeContainer = document.getElementById('code-tree-container');
576
+ if (treeContainer) {
577
+ eventsContainer = document.createElement('div');
578
+ eventsContainer.id = 'analysis-events';
579
+ eventsContainer.className = 'analysis-events';
580
+ eventsContainer.style.display = 'none';
581
+ treeContainer.appendChild(eventsContainer);
582
+ }
508
583
  }
509
- return patterns;
510
584
  }
511
585
 
512
586
  /**
513
- * Handle analysis start event
587
+ * Clear the events display
514
588
  */
515
- handleAnalysisStart(data) {
516
- console.log('Code analysis started:', data);
517
-
518
- // Only handle if this is for our current request
519
- if (data.request_id && data.request_id !== this.currentRequestId) {
520
- return;
589
+ clearEventsDisplay() {
590
+ const eventsContainer = document.getElementById('analysis-events');
591
+ if (eventsContainer) {
592
+ eventsContainer.innerHTML = '';
593
+ eventsContainer.style.display = 'block';
521
594
  }
522
-
523
- // Clear request timeout since we got a response
524
- if (this.requestTimeout) {
525
- clearTimeout(this.requestTimeout);
526
- this.requestTimeout = null;
527
- }
528
-
529
- const message = `Analyzing ${data.total_files || 0} files...`;
530
- this.updateProgress(0, message);
531
- this.updateTicker(message, 'progress');
532
- this.showNotification('Analysis started - building tree in real-time...', 'info');
533
595
  }
534
596
 
535
597
  /**
536
- * Handle file start event
598
+ * Add an event to the display
537
599
  */
538
- handleFileStart(data) {
539
- console.log('Analyzing file:', data.path);
540
- const message = `Analyzing: ${data.path}`;
541
- this.updateProgress(data.progress || 0, message);
542
- this.updateTicker(`📄 ${data.path}`, 'file');
543
-
544
- // Add file node to tree
545
- const fileNode = {
546
- name: data.name || data.path.split('/').pop(),
547
- type: 'file',
548
- path: data.path,
549
- language: data.language,
550
- children: []
551
- };
552
-
553
- this.addNodeToTree(fileNode, data.path);
554
- this.stats.files++;
555
- this.updateStats();
556
-
557
- // Incremental tree update - update visualization as files are discovered
558
- if (this.svg && this.root) {
559
- // Throttle updates to avoid performance issues
560
- if (!this.updateThrottleTimer) {
561
- this.updateThrottleTimer = setTimeout(() => {
562
- this.update(this.root);
563
- this.updateThrottleTimer = null;
564
- }, 100); // Update every 100ms max
565
- }
600
+ addEventToDisplay(message, type = 'info') {
601
+ const eventsContainer = document.getElementById('analysis-events');
602
+ if (eventsContainer) {
603
+ const eventEl = document.createElement('div');
604
+ eventEl.className = 'analysis-event';
605
+ eventEl.style.borderLeftColor = type === 'warning' ? '#f59e0b' :
606
+ type === 'error' ? '#ef4444' : '#3b82f6';
607
+
608
+ const timestamp = new Date().toLocaleTimeString();
609
+ eventEl.innerHTML = `<span style="color: #718096;">[${timestamp}]</span> ${message}`;
610
+
611
+ eventsContainer.appendChild(eventEl);
612
+ // Auto-scroll to bottom
613
+ eventsContainer.scrollTop = eventsContainer.scrollHeight;
566
614
  }
567
615
  }
568
616
 
569
617
  /**
570
- * Handle file complete event
618
+ * Setup Socket.IO event handlers
571
619
  */
572
- handleFileComplete(data) {
573
- console.log('File analysis complete:', data.path);
620
+ setupEventHandlers() {
621
+ if (!this.socket) return;
622
+
623
+ // Analysis lifecycle events
624
+ this.socket.on('code:analysis:accepted', (data) => this.onAnalysisAccepted(data));
625
+ this.socket.on('code:analysis:queued', (data) => this.onAnalysisQueued(data));
626
+ this.socket.on('code:analysis:start', (data) => this.onAnalysisStart(data));
627
+ this.socket.on('code:analysis:complete', (data) => this.onAnalysisComplete(data));
628
+ this.socket.on('code:analysis:cancelled', (data) => this.onAnalysisCancelled(data));
629
+ this.socket.on('code:analysis:error', (data) => this.onAnalysisError(data));
630
+
631
+ // Node discovery events
632
+ this.socket.on('code:top_level:discovered', (data) => this.onTopLevelDiscovered(data));
633
+ this.socket.on('code:directory:discovered', (data) => this.onDirectoryDiscovered(data));
634
+ this.socket.on('code:file:discovered', (data) => this.onFileDiscovered(data));
635
+ this.socket.on('code:file:analyzed', (data) => this.onFileAnalyzed(data));
636
+ this.socket.on('code:node:found', (data) => this.onNodeFound(data));
637
+
638
+ // Progress updates
639
+ this.socket.on('code:analysis:progress', (data) => this.onProgressUpdate(data));
640
+
641
+ // Lazy loading responses
642
+ this.socket.on('code:directory:contents', (data) => {
643
+ // Update the requested directory with its contents
644
+ if (data.path) {
645
+ // Convert absolute path back to relative path to match tree nodes
646
+ let searchPath = data.path;
647
+ const workingDir = this.getWorkingDirectory();
648
+ if (workingDir && searchPath.startsWith(workingDir)) {
649
+ // Remove working directory prefix to get relative path
650
+ searchPath = searchPath.substring(workingDir.length).replace(/^\//, '');
651
+ // If empty after removing prefix, it's the root
652
+ if (!searchPath) {
653
+ searchPath = '.';
654
+ }
655
+ }
656
+
657
+ const node = this.findNodeByPath(searchPath);
658
+ if (node && data.children) {
659
+ // Find D3 node and remove loading pulse (use searchPath, not data.path)
660
+ const d3Node = this.findD3NodeByPath(searchPath);
661
+ if (d3Node && this.loadingNodes.has(searchPath)) {
662
+ this.removeLoadingPulse(d3Node);
663
+ }
664
+ node.children = data.children.map(child => {
665
+ // Construct full path for child by combining parent path with child name
666
+ // The backend now returns just the item name, not the full path
667
+ let childPath;
668
+ if (searchPath === '.' || searchPath === '') {
669
+ // Root level - child path is just the name
670
+ childPath = child.name || child.path;
671
+ } else {
672
+ // Subdirectory - combine parent path with child name
673
+ // Use child.name (backend returns just the name) or fallback to child.path
674
+ const childName = child.name || child.path;
675
+ childPath = `${searchPath}/${childName}`;
676
+ }
677
+
678
+ return {
679
+ ...child,
680
+ path: childPath, // Override with constructed path
681
+ loaded: child.type === 'directory' ? false : undefined,
682
+ analyzed: child.type === 'file' ? false : undefined,
683
+ expanded: false,
684
+ children: []
685
+ };
686
+ });
687
+ node.loaded = true;
688
+ node.expanded = true; // Mark as expanded to show children
689
+
690
+ // Update D3 hierarchy and make sure the node is expanded
691
+ if (this.root && this.svg) {
692
+ // Store old root to preserve expansion state
693
+ const oldRoot = this.root;
694
+
695
+ // Recreate hierarchy with updated data
696
+ this.root = d3.hierarchy(this.treeData);
697
+ this.root.x0 = this.height / 2;
698
+ this.root.y0 = 0;
699
+
700
+ // Preserve expansion state from old tree
701
+ this.preserveExpansionState(oldRoot, this.root);
702
+
703
+ // Find the D3 node again after hierarchy recreation
704
+ const updatedD3Node = this.findD3NodeByPath(searchPath);
705
+ if (updatedD3Node) {
706
+ // Ensure children are visible (not collapsed)
707
+ updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
708
+ updatedD3Node._children = null;
709
+ updatedD3Node.data.expanded = true;
710
+ }
711
+
712
+ this.update(this.root);
713
+ }
714
+
715
+ // Update stats based on discovered contents
716
+ if (data.stats) {
717
+ this.stats.files += data.stats.files || 0;
718
+ this.stats.directories += data.stats.directories || 0;
719
+ this.updateStats();
720
+ }
721
+
722
+ this.updateBreadcrumb(`Loaded ${data.path}`, 'success');
723
+ this.hideLoading();
724
+ }
725
+ }
726
+ });
574
727
 
575
- // Update the file node if we have stats
576
- if (data.stats) {
577
- const fileNode = this.nodes.get(data.path);
578
- if (fileNode) {
579
- fileNode.stats = data.stats;
580
- if (data.stats.lines) {
581
- this.stats.lines += data.stats.lines;
728
+ // Top level discovery response
729
+ this.socket.on('code:top_level:discovered', (data) => {
730
+ if (data.items && Array.isArray(data.items)) {
731
+
732
+ // Add discovered items to the root node
733
+ this.treeData.children = data.items.map(item => ({
734
+ name: item.name,
735
+ path: item.path,
736
+ type: item.type,
737
+ language: item.type === 'file' ? this.detectLanguage(item.path) : undefined,
738
+ size: item.size,
739
+ lines: item.lines,
740
+ loaded: item.type === 'directory' ? false : undefined,
741
+ analyzed: item.type === 'file' ? false : undefined,
742
+ expanded: false,
743
+ children: []
744
+ }));
745
+
746
+ this.treeData.loaded = true;
747
+
748
+ // Update stats
749
+ if (data.stats) {
750
+ this.stats = { ...this.stats, ...data.stats };
582
751
  this.updateStats();
583
752
  }
753
+
754
+ // Update D3 hierarchy
755
+ if (typeof d3 !== 'undefined') {
756
+ // Clear any existing nodes before creating new ones
757
+ this.clearD3Visualization();
758
+
759
+ // Create new hierarchy
760
+ this.root = d3.hierarchy(this.treeData);
761
+ this.root.x0 = this.height / 2;
762
+ this.root.y0 = 0;
763
+
764
+ if (this.svg) {
765
+ this.update(this.root);
766
+ }
767
+ }
768
+
769
+ this.analyzing = false;
770
+ this.hideLoading();
771
+ this.updateBreadcrumb(`Discovered ${data.items.length} root items`, 'success');
772
+ this.showNotification(`Found ${data.items.length} items in project root`, 'success');
584
773
  }
585
- }
774
+ });
586
775
  }
587
776
 
588
777
  /**
589
- * Handle node found event
778
+ * Handle analysis start event
590
779
  */
591
- handleNodeFound(data) {
592
- console.log('Node found:', data);
593
-
594
- // Update ticker with node discovery
595
- const icons = {
596
- 'function': '⚡',
597
- 'class': '🏛️',
598
- 'method': '🔧',
599
- 'module': '📦'
600
- };
601
- const icon = icons[data.type] || '📌';
602
- const nodeName = data.name || 'unnamed';
603
- this.updateTicker(`${icon} ${nodeName}`, 'node');
604
-
605
- // Create node object
606
- const node = {
607
- name: data.name,
608
- type: data.type, // module, class, function, method
609
- path: data.path,
610
- line: data.line,
611
- complexity: data.complexity || 0,
612
- docstring: data.docstring,
613
- params: data.params,
614
- returns: data.returns,
615
- children: []
616
- };
780
+ onAnalysisStart(data) {
781
+ this.analyzing = true;
782
+ const message = data.message || 'Starting code analysis...';
617
783
 
618
- // Add to tree
619
- this.addNodeToTree(node, data.parent_path || data.path);
784
+ // Update activity ticker
785
+ this.updateActivityTicker('🚀 Starting analysis...', 'info');
620
786
 
621
- // Update stats
622
- switch (data.type) {
623
- case 'class':
624
- this.stats.classes++;
625
- break;
626
- case 'function':
627
- this.stats.functions++;
628
- break;
629
- case 'method':
630
- this.stats.methods++;
631
- break;
632
- }
787
+ this.updateBreadcrumb(message, 'info');
788
+ this.addEventToDisplay(`🚀 ${message}`, 'info');
633
789
 
634
- if (data.lines) {
635
- this.stats.lines += data.lines;
790
+ // Initialize or clear the tree
791
+ if (!this.treeData || this.treeData.children.length === 0) {
792
+ this.initializeTreeData();
636
793
  }
637
794
 
795
+ // Reset stats
796
+ this.stats = {
797
+ files: 0,
798
+ classes: 0,
799
+ functions: 0,
800
+ methods: 0,
801
+ lines: 0
802
+ };
638
803
  this.updateStats();
639
-
640
- // Incremental tree update - batch updates for performance
641
- if (this.svg && this.root) {
642
- // Clear existing throttle timer
643
- if (this.updateThrottleTimer) {
644
- clearTimeout(this.updateThrottleTimer);
645
- }
646
-
647
- // Set new throttle timer - batch multiple nodes together
648
- this.updateThrottleTimer = setTimeout(() => {
649
- this.update(this.root);
650
- this.updateThrottleTimer = null;
651
- }, 200); // Update every 200ms max for node additions
652
- }
653
- }
654
-
655
- /**
656
- * Handle progress update
657
- */
658
- handleProgress(data) {
659
- this.updateProgress(data.percentage, data.message);
660
804
  }
661
805
 
662
806
  /**
663
- * Handle analysis complete
807
+ * Handle top-level discovery event (initial root directory scan)
664
808
  */
665
- handleAnalysisComplete(data) {
666
- console.log('Code analysis complete:', data);
809
+ onTopLevelDiscovered(data) {
810
+ // Received top-level discovery response
667
811
 
668
- // Only handle if this is for our current request
669
- if (data.request_id && data.request_id !== this.currentRequestId) {
670
- return;
671
- }
812
+ // Update activity ticker
813
+ this.updateActivityTicker(`📁 Discovered ${(data.items || []).length} top-level items`, 'success');
672
814
 
673
- this.resetAnalysisState();
815
+ // Add to events display
816
+ this.addEventToDisplay(`📁 Found ${(data.items || []).length} top-level items in project root`, 'info');
674
817
 
675
- // Final tree update
676
- if (this.svg) {
677
- this.update(this.root);
678
- }
818
+ // The root node (with path '.') should receive the children
819
+ const rootNode = this.findNodeByPath('.');
679
820
 
680
- // Update final stats
681
- if (data.stats) {
682
- this.stats = {...this.stats, ...data.stats};
683
- this.updateStats();
684
- }
821
+ console.log('🔎 Looking for root node with path ".", found:', rootNode ? {
822
+ name: rootNode.name,
823
+ path: rootNode.path,
824
+ currentChildren: rootNode.children ? rootNode.children.length : 0
825
+ } : 'NOT FOUND');
685
826
 
686
- // Show completion message
687
- const completeMessage = `✅ Complete: ${this.stats.files} files, ${this.stats.functions} functions, ${this.stats.classes} classes`;
688
- this.updateTicker(completeMessage, 'progress');
689
- this.showNotification('Analysis complete', 'success');
690
- }
691
-
692
- /**
693
- * Handle analysis error
827
+ if (rootNode && data.items) {
828
+ console.log('🌳 Populating root node with children');
829
+
830
+ // Update the root node with discovered children
831
+ rootNode.children = data.items.map(child => {
832
+ // Items at root level get their name as the path
833
+ const childPath = child.name;
834
+
835
+ console.log(` Adding child: ${child.name} with path: ${childPath}`);
836
+
837
+ return {
838
+ name: child.name,
839
+ path: childPath, // Just the name for top-level items
840
+ type: child.type,
841
+ loaded: child.type === 'directory' ? false : undefined,
842
+ analyzed: child.type === 'file' ? false : undefined,
843
+ expanded: false,
844
+ children: child.type === 'directory' ? [] : undefined,
845
+ size: child.size,
846
+ has_code: child.has_code
847
+ };
848
+ });
849
+
850
+ rootNode.loaded = true;
851
+ rootNode.expanded = true;
852
+
853
+ // Update D3 hierarchy and render
854
+ if (this.root && this.svg) {
855
+ // Recreate hierarchy with new data
856
+ this.root = d3.hierarchy(this.treeData);
857
+ this.root.x0 = this.height / 2;
858
+ this.root.y0 = 0;
859
+
860
+ // Update the tree visualization
861
+ this.update(this.root);
862
+ }
863
+
864
+ // Hide loading and show success
865
+ this.hideLoading();
866
+ this.updateBreadcrumb(`Discovered ${data.items.length} items`, 'success');
867
+ this.showNotification(`Found ${data.items.length} top-level items`, 'success');
868
+ } else {
869
+ console.error('❌ Could not find root node to populate');
870
+ this.showNotification('Failed to populate root directory', 'error');
871
+ }
872
+
873
+ // Mark analysis as complete
874
+ this.analyzing = false;
875
+ }
876
+
877
+ /**
878
+ * Handle directory discovered event
694
879
  */
695
- handleAnalysisError(data) {
696
- console.error('Code analysis error:', data);
880
+ onDirectoryDiscovered(data) {
881
+ // Update activity ticker first
882
+ this.updateActivityTicker(`📁 Discovered: ${data.name || 'directory'}`);
697
883
 
698
- // Only handle if this is for our current request
699
- if (data.request_id && data.request_id !== this.currentRequestId) {
700
- return;
884
+ // Add to events display
885
+ this.addEventToDisplay(`📁 Found ${(data.children || []).length} items in: ${data.name || data.path}`, 'info');
886
+
887
+ console.log('📥 Received directory discovery:', {
888
+ path: data.path,
889
+ name: data.name,
890
+ childrenCount: (data.children || []).length,
891
+ children: (data.children || []).map(c => ({ name: c.name, type: c.type })),
892
+ workingDir: this.getWorkingDirectory()
893
+ });
894
+
895
+ // Convert absolute path back to relative path to match tree nodes
896
+ let searchPath = data.path;
897
+ const workingDir = this.getWorkingDirectory();
898
+ if (workingDir && searchPath.startsWith(workingDir)) {
899
+ // Remove working directory prefix to get relative path
900
+ searchPath = searchPath.substring(workingDir.length).replace(/^\//, '');
901
+ // If empty after removing prefix, it's the root
902
+ if (!searchPath) {
903
+ searchPath = '.';
904
+ }
701
905
  }
702
906
 
703
- this.resetAnalysisState();
907
+ console.log('🔎 Searching for node with path:', searchPath);
704
908
 
705
- // Show error message
706
- const errorMessage = data.message || 'Unknown error';
707
- this.updateTicker(`❌ ${errorMessage}`, 'error');
708
- this.showNotification(`Analysis failed: ${errorMessage}`, 'error');
909
+ // Find the node that was clicked to trigger this discovery
910
+ const node = this.findNodeByPath(searchPath);
911
+
912
+ // Located target node for expansion
913
+
914
+ if (node && data.children) {
915
+ // Update the node with discovered children
916
+ node.children = data.children.map(child => {
917
+ // Construct full path for child by combining parent path with child name
918
+ // The backend now returns just the item name, not the full path
919
+ let childPath;
920
+ if (searchPath === '.' || searchPath === '') {
921
+ // Root level - child path is just the name
922
+ childPath = child.name || child.path;
923
+ } else {
924
+ // Subdirectory - combine parent path with child name
925
+ // Use child.name (backend returns just the name) or fallback to child.path
926
+ const childName = child.name || child.path;
927
+ childPath = `${searchPath}/${childName}`;
928
+ }
929
+
930
+ return {
931
+ name: child.name,
932
+ path: childPath, // Use constructed path instead of child.path
933
+ type: child.type,
934
+ loaded: child.type === 'directory' ? false : undefined,
935
+ analyzed: child.type === 'file' ? false : undefined,
936
+ expanded: false,
937
+ children: child.type === 'directory' ? [] : undefined,
938
+ size: child.size,
939
+ has_code: child.has_code
940
+ };
941
+ });
942
+ node.loaded = true;
943
+ node.expanded = true;
944
+
945
+ // Find D3 node and remove loading pulse (use searchPath, not data.path)
946
+ const d3Node = this.findD3NodeByPath(searchPath);
947
+ if (d3Node) {
948
+ // Remove loading animation
949
+ if (this.loadingNodes.has(searchPath)) {
950
+ this.removeLoadingPulse(d3Node);
951
+ }
952
+ }
953
+
954
+ // Update D3 hierarchy and redraw with expanded node
955
+ if (this.root && this.svg) {
956
+ // Store old root to preserve expansion state
957
+ const oldRoot = this.root;
958
+
959
+ // Recreate hierarchy with updated data
960
+ this.root = d3.hierarchy(this.treeData);
961
+
962
+ // Restore positions for smooth animation
963
+ this.root.x0 = this.height / 2;
964
+ this.root.y0 = 0;
965
+
966
+ // Preserve expansion state from old tree
967
+ this.preserveExpansionState(oldRoot, this.root);
968
+
969
+ // Find the D3 node again after hierarchy recreation
970
+ const updatedD3Node = this.findD3NodeByPath(searchPath);
971
+ if (updatedD3Node && updatedD3Node.data.children && updatedD3Node.data.children.length > 0) {
972
+ // Ensure the node is expanded to show children
973
+ updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
974
+ updatedD3Node._children = null;
975
+ // Mark data as expanded
976
+ updatedD3Node.data.expanded = true;
977
+ }
978
+
979
+ this.update(this.root);
980
+ }
981
+
982
+ this.updateBreadcrumb(`Loaded ${node.children.length} items from ${node.name}`, 'success');
983
+ this.updateStats();
984
+ } else if (!node) {
985
+ this.logAllPaths(this.treeData);
986
+ } else if (node && !data.children) {
987
+ // This might be a top-level directory discovery
988
+ const pathParts = data.path ? data.path.split('/').filter(p => p) : [];
989
+ const isTopLevel = pathParts.length === 1;
990
+
991
+ if (isTopLevel || data.forceAdd) {
992
+ const dirNode = {
993
+ name: data.name || pathParts[pathParts.length - 1] || 'Unknown',
994
+ path: data.path,
995
+ type: 'directory',
996
+ children: [],
997
+ loaded: false,
998
+ expanded: false,
999
+ stats: data.stats || {}
1000
+ };
1001
+
1002
+ this.addNodeToTree(dirNode, data.parent || '');
1003
+ this.updateBreadcrumb(`Discovered: ${data.path}`, 'info');
1004
+ }
1005
+ }
709
1006
  }
710
-
1007
+
711
1008
  /**
712
- * Handle analysis accepted event
1009
+ * Handle file discovered event
713
1010
  */
714
- handleAnalysisAccepted(data) {
715
- console.log('Analysis request accepted:', data);
1011
+ onFileDiscovered(data) {
1012
+ // Update activity ticker
1013
+ const fileName = data.name || (data.path ? data.path.split('/').pop() : 'file');
1014
+ this.updateActivityTicker(`📄 Found: ${fileName}`);
716
1015
 
717
- if (data.request_id === this.currentRequestId) {
718
- // Clear timeout since server responded
719
- if (this.requestTimeout) {
720
- clearTimeout(this.requestTimeout);
721
- this.requestTimeout = null;
1016
+ // Add to events display
1017
+ this.addEventToDisplay(`📄 Discovered: ${data.path || 'Unknown file'}`, 'info');
1018
+
1019
+ const pathParts = data.path ? data.path.split('/').filter(p => p) : [];
1020
+ const parentPath = pathParts.slice(0, -1).join('/');
1021
+
1022
+ const fileNode = {
1023
+ name: data.name || pathParts[pathParts.length - 1] || 'Unknown',
1024
+ path: data.path,
1025
+ type: 'file',
1026
+ language: data.language || this.detectLanguage(data.path),
1027
+ size: data.size || 0,
1028
+ lines: data.lines || 0,
1029
+ children: [],
1030
+ analyzed: false
1031
+ };
1032
+
1033
+ this.addNodeToTree(fileNode, parentPath);
1034
+ this.stats.files++;
1035
+ this.updateStats();
1036
+ this.updateBreadcrumb(`Found: ${data.path}`, 'info');
1037
+ }
1038
+
1039
+ /**
1040
+ * Handle file analyzed event
1041
+ */
1042
+ onFileAnalyzed(data) {
1043
+ // Remove loading pulse if this file was being analyzed
1044
+ const d3Node = this.findD3NodeByPath(data.path);
1045
+ if (d3Node && this.loadingNodes.has(data.path)) {
1046
+ this.removeLoadingPulse(d3Node);
1047
+ }
1048
+ // Update activity ticker
1049
+ if (data.path) {
1050
+ const fileName = data.path.split('/').pop();
1051
+ this.updateActivityTicker(`🔍 Analyzed: ${fileName}`);
1052
+ }
1053
+
1054
+ const fileNode = this.findNodeByPath(data.path);
1055
+ if (fileNode) {
1056
+ fileNode.analyzed = true;
1057
+ fileNode.complexity = data.complexity || 0;
1058
+ fileNode.lines = data.lines || 0;
1059
+
1060
+ // Add code elements as children
1061
+ if (data.elements && Array.isArray(data.elements)) {
1062
+ fileNode.children = data.elements.map(elem => ({
1063
+ name: elem.name,
1064
+ type: elem.type.toLowerCase(),
1065
+ path: `${data.path}#${elem.name}`,
1066
+ line: elem.line,
1067
+ complexity: elem.complexity || 1,
1068
+ docstring: elem.docstring || '',
1069
+ children: elem.methods ? elem.methods.map(m => ({
1070
+ name: m.name,
1071
+ type: 'method',
1072
+ path: `${data.path}#${elem.name}.${m.name}`,
1073
+ line: m.line,
1074
+ complexity: m.complexity || 1,
1075
+ docstring: m.docstring || ''
1076
+ })) : []
1077
+ }));
1078
+ }
1079
+
1080
+ // Update stats
1081
+ if (data.stats) {
1082
+ this.stats.classes += data.stats.classes || 0;
1083
+ this.stats.functions += data.stats.functions || 0;
1084
+ this.stats.methods += data.stats.methods || 0;
1085
+ this.stats.lines += data.stats.lines || 0;
722
1086
  }
723
- this.showNotification('Analysis request accepted by server', 'info');
1087
+
1088
+ this.updateStats();
1089
+ if (this.root) {
1090
+ this.update(this.root);
1091
+ }
1092
+
1093
+ this.updateBreadcrumb(`Analyzed: ${data.path}`, 'success');
724
1094
  }
725
1095
  }
726
-
1096
+
727
1097
  /**
728
- * Handle analysis queued event
1098
+ * Handle node found event
729
1099
  */
730
- handleAnalysisQueued(data) {
731
- console.log('Analysis queued:', data);
732
-
733
- if (data.request_id === this.currentRequestId) {
734
- this.showNotification(`Analysis queued (position: ${data.queue_size || 1})`, 'info');
1100
+ onNodeFound(data) {
1101
+ // Add to events display with appropriate icon
1102
+ const typeIcon = data.type === 'class' ? '🏛️' :
1103
+ data.type === 'function' ? '⚡' :
1104
+ data.type === 'method' ? '🔧' : '📦';
1105
+ this.addEventToDisplay(`${typeIcon} Found ${data.type || 'node'}: ${data.name || 'Unknown'}`);
1106
+
1107
+ // Extract node info
1108
+ const nodeInfo = {
1109
+ name: data.name || 'Unknown',
1110
+ type: (data.type || 'unknown').toLowerCase(),
1111
+ path: data.path || '',
1112
+ line: data.line || 0,
1113
+ complexity: data.complexity || 1,
1114
+ docstring: data.docstring || ''
1115
+ };
1116
+
1117
+ // Map event types to our internal types
1118
+ const typeMapping = {
1119
+ 'class': 'class',
1120
+ 'function': 'function',
1121
+ 'method': 'method',
1122
+ 'module': 'module',
1123
+ 'file': 'file',
1124
+ 'directory': 'directory'
1125
+ };
1126
+
1127
+ nodeInfo.type = typeMapping[nodeInfo.type] || nodeInfo.type;
1128
+
1129
+ // Determine parent path
1130
+ let parentPath = '';
1131
+ if (data.parent_path) {
1132
+ parentPath = data.parent_path;
1133
+ } else if (data.file_path) {
1134
+ parentPath = data.file_path;
1135
+ } else if (nodeInfo.path.includes('/')) {
1136
+ const parts = nodeInfo.path.split('/');
1137
+ parts.pop();
1138
+ parentPath = parts.join('/');
735
1139
  }
1140
+
1141
+ // Update stats based on node type
1142
+ switch(nodeInfo.type) {
1143
+ case 'class':
1144
+ this.stats.classes++;
1145
+ break;
1146
+ case 'function':
1147
+ this.stats.functions++;
1148
+ break;
1149
+ case 'method':
1150
+ this.stats.methods++;
1151
+ break;
1152
+ case 'file':
1153
+ this.stats.files++;
1154
+ break;
1155
+ }
1156
+
1157
+ // Add node to tree
1158
+ this.addNodeToTree(nodeInfo, parentPath);
1159
+ this.updateStats();
1160
+
1161
+ // Show progress in breadcrumb
1162
+ const elementType = nodeInfo.type.charAt(0).toUpperCase() + nodeInfo.type.slice(1);
1163
+ this.updateBreadcrumb(`Found ${elementType}: ${nodeInfo.name}`, 'info');
736
1164
  }
737
-
1165
+
738
1166
  /**
739
- * Handle analysis cancelled event
1167
+ * Handle progress update
740
1168
  */
741
- handleAnalysisCancelled(data) {
742
- console.log('Analysis cancelled:', data);
1169
+ onProgressUpdate(data) {
1170
+ const progress = data.progress || 0;
1171
+ const message = data.message || `Processing... ${progress}%`;
743
1172
 
744
- if (!data.request_id || data.request_id === this.currentRequestId) {
745
- this.resetAnalysisState();
746
- this.showNotification('Analysis cancelled', 'warning');
1173
+ this.updateBreadcrumb(message, 'info');
1174
+
1175
+ // Update progress bar if it exists
1176
+ const progressBar = document.querySelector('.code-tree-progress');
1177
+ if (progressBar) {
1178
+ progressBar.style.width = `${progress}%`;
747
1179
  }
748
1180
  }
749
-
1181
+
750
1182
  /**
751
- * Show notification message
1183
+ * Handle analysis complete event
752
1184
  */
753
- showNotification(message, type = 'info') {
754
- console.log(`CodeTree notification: ${message} (${type})`);
1185
+ onAnalysisComplete(data) {
1186
+ this.analyzing = false;
1187
+ this.hideLoading();
755
1188
 
756
- // Try to find existing notification area in the Code tab
757
- let notification = document.querySelector('#code-tab .notification-area');
1189
+ // Update activity ticker
1190
+ this.updateActivityTicker(' Ready', 'success');
758
1191
 
759
- // If not found, create one in the Code tab
760
- if (!notification) {
761
- const codeTab = document.getElementById('code-tab');
762
- if (!codeTab) {
763
- console.error('Code tab not found for notification');
764
- return;
1192
+ // Add completion event
1193
+ this.addEventToDisplay('✅ Analysis complete!', 'success');
1194
+
1195
+ // Update tree visualization
1196
+ if (this.root && this.svg) {
1197
+ this.update(this.root);
1198
+ }
1199
+
1200
+ // Update stats from completion data
1201
+ if (data.stats) {
1202
+ this.stats = { ...this.stats, ...data.stats };
1203
+ this.updateStats();
1204
+ }
1205
+
1206
+ const message = data.message || `Analysis complete: ${this.stats.files} files, ${this.stats.classes} classes, ${this.stats.functions} functions`;
1207
+ this.updateBreadcrumb(message, 'success');
1208
+ this.showNotification(message, 'success');
1209
+ }
1210
+
1211
+ /**
1212
+ * Handle analysis error
1213
+ */
1214
+ onAnalysisError(data) {
1215
+ this.analyzing = false;
1216
+ this.hideLoading();
1217
+
1218
+ const message = data.message || data.error || 'Analysis failed';
1219
+ this.updateBreadcrumb(message, 'error');
1220
+ this.showNotification(message, 'error');
1221
+ }
1222
+
1223
+ /**
1224
+ * Handle analysis accepted
1225
+ */
1226
+ onAnalysisAccepted(data) {
1227
+ const message = data.message || 'Analysis request accepted';
1228
+ this.updateBreadcrumb(message, 'info');
1229
+ }
1230
+
1231
+ /**
1232
+ * Handle analysis queued
1233
+ */
1234
+ onAnalysisQueued(data) {
1235
+ const position = data.position || 0;
1236
+ const message = `Analysis queued (position ${position})`;
1237
+ this.updateBreadcrumb(message, 'warning');
1238
+ this.showNotification(message, 'info');
1239
+ }
1240
+
1241
+ /**
1242
+ * Handle INFO events for granular work tracking
1243
+ */
1244
+ onInfoEvent(data) {
1245
+ // Log to console for debugging
1246
+
1247
+ // Update breadcrumb for certain events
1248
+ if (data.type && data.type.startsWith('discovery.')) {
1249
+ // Discovery events
1250
+ if (data.type === 'discovery.start') {
1251
+ this.updateBreadcrumb(data.message, 'info');
1252
+ } else if (data.type === 'discovery.complete') {
1253
+ this.updateBreadcrumb(data.message, 'success');
1254
+ // Show stats if available
1255
+ if (data.stats) {
1256
+ }
1257
+ } else if (data.type === 'discovery.directory' || data.type === 'discovery.file') {
1258
+ // Quick flash of discovery events
1259
+ this.updateBreadcrumb(data.message, 'info');
1260
+ }
1261
+ } else if (data.type && data.type.startsWith('analysis.')) {
1262
+ // Analysis events
1263
+ if (data.type === 'analysis.start') {
1264
+ this.updateBreadcrumb(data.message, 'info');
1265
+ } else if (data.type === 'analysis.complete') {
1266
+ this.updateBreadcrumb(data.message, 'success');
1267
+ // Show stats if available
1268
+ if (data.stats) {
1269
+ const statsMsg = `Found: ${data.stats.classes || 0} classes, ${data.stats.functions || 0} functions, ${data.stats.methods || 0} methods`;
1270
+ }
1271
+ } else if (data.type === 'analysis.class' || data.type === 'analysis.function' || data.type === 'analysis.method') {
1272
+ // Show found elements briefly
1273
+ this.updateBreadcrumb(data.message, 'info');
1274
+ } else if (data.type === 'analysis.parse') {
1275
+ this.updateBreadcrumb(data.message, 'info');
1276
+ }
1277
+ } else if (data.type && data.type.startsWith('filter.')) {
1278
+ // Filter events - optionally show in debug mode
1279
+ if (window.debugMode || this.showFilterEvents) {
1280
+ console.debug('[FILTER]', data.type, data.path, data.reason);
1281
+ if (this.showFilterEvents) {
1282
+ this.updateBreadcrumb(data.message, 'warning');
1283
+ }
1284
+ }
1285
+ } else if (data.type && data.type.startsWith('cache.')) {
1286
+ // Cache events
1287
+ if (data.type === 'cache.hit') {
1288
+ console.debug('[CACHE HIT]', data.file);
1289
+ if (this.showCacheEvents) {
1290
+ this.updateBreadcrumb(data.message, 'info');
1291
+ }
1292
+ } else if (data.type === 'cache.miss') {
1293
+ console.debug('[CACHE MISS]', data.file);
765
1294
  }
766
-
767
- notification = document.createElement('div');
768
- notification.className = 'notification-area';
769
- notification.style.cssText = `
770
- position: absolute;
771
- top: 10px;
772
- right: 10px;
773
- max-width: 400px;
774
- z-index: 1000;
775
- padding: 12px 16px;
776
- border-radius: 4px;
777
- font-size: 14px;
778
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
779
- transition: opacity 0.3s ease;
780
- `;
781
- codeTab.insertBefore(notification, codeTab.firstChild);
782
1295
  }
783
1296
 
784
- // Set colors based on type
785
- const colors = {
786
- info: { bg: '#e3f2fd', text: '#1976d2', border: '#90caf9' },
787
- success: { bg: '#e8f5e9', text: '#388e3c', border: '#81c784' },
788
- warning: { bg: '#fff3e0', text: '#f57c00', border: '#ffb74d' },
789
- error: { bg: '#ffebee', text: '#d32f2f', border: '#ef5350' }
790
- };
1297
+ // Optionally add to an event log display if enabled
1298
+ if (this.eventLogEnabled && data.message) {
1299
+ this.addEventToDisplay(data);
1300
+ }
1301
+ }
1302
+
1303
+ /**
1304
+ * Add event to display log (if we have one)
1305
+ */
1306
+ addEventToDisplay(data) {
1307
+ // Could be implemented to show events in a dedicated log area
1308
+ // For now, just maintain a recent events list
1309
+ if (!this.recentEvents) {
1310
+ this.recentEvents = [];
1311
+ }
1312
+
1313
+ this.recentEvents.unshift({
1314
+ timestamp: data.timestamp || new Date().toISOString(),
1315
+ type: data.type,
1316
+ message: data.message,
1317
+ data: data
1318
+ });
791
1319
 
792
- const color = colors[type] || colors.info;
793
- notification.style.backgroundColor = color.bg;
794
- notification.style.color = color.text;
795
- notification.style.border = `1px solid ${color.border}`;
1320
+ // Keep only last 100 events
1321
+ if (this.recentEvents.length > 100) {
1322
+ this.recentEvents.pop();
1323
+ }
796
1324
 
797
- // Set message
1325
+ // Could update a UI element here if we had an event log display
1326
+ }
1327
+
1328
+ /**
1329
+ * Handle analysis cancelled
1330
+ */
1331
+ onAnalysisCancelled(data) {
1332
+ this.analyzing = false;
1333
+ this.hideLoading();
1334
+ const message = data.message || 'Analysis cancelled';
1335
+ this.updateBreadcrumb(message, 'warning');
1336
+ }
1337
+
1338
+ /**
1339
+ * Show notification toast
1340
+ */
1341
+ showNotification(message, type = 'info') {
1342
+ const notification = document.createElement('div');
1343
+ notification.className = `code-tree-notification ${type}`;
798
1344
  notification.textContent = message;
799
- notification.style.display = 'block';
800
- notification.style.opacity = '1';
801
1345
 
802
- // Clear existing timeout
803
- if (this.notificationTimeout) {
804
- clearTimeout(this.notificationTimeout);
805
- }
806
-
807
- // Auto-hide after 5 seconds
808
- this.notificationTimeout = setTimeout(() => {
809
- notification.style.opacity = '0';
1346
+ // Change from appending to container to positioning absolutely within it
1347
+ const container = document.getElementById('code-tree-container');
1348
+ if (container) {
1349
+ // Position relative to the container
1350
+ notification.style.position = 'absolute';
1351
+ notification.style.top = '10px';
1352
+ notification.style.right = '10px';
1353
+ notification.style.zIndex = '1000';
1354
+
1355
+ // Ensure container is positioned
1356
+ if (!container.style.position || container.style.position === 'static') {
1357
+ container.style.position = 'relative';
1358
+ }
1359
+
1360
+ container.appendChild(notification);
1361
+
1362
+ // Animate out after 3 seconds
810
1363
  setTimeout(() => {
811
- notification.style.display = 'none';
812
- }, 300);
813
- }, 5000);
1364
+ notification.style.animation = 'slideOutRight 0.3s ease';
1365
+ setTimeout(() => notification.remove(), 300);
1366
+ }, 3000);
1367
+ }
814
1368
  }
815
1369
 
816
1370
  /**
817
1371
  * Add node to tree structure
818
1372
  */
819
- addNodeToTree(node, parentPath) {
820
- // Find parent in tree
821
- let parent = this.findNodeByPath(parentPath);
822
- if (!parent) {
823
- parent = this.treeData;
1373
+ addNodeToTree(nodeInfo, parentPath = '') {
1374
+ // CRITICAL: Validate that nodeInfo.path doesn't contain absolute paths
1375
+ // The backend should only send relative paths now
1376
+ if (nodeInfo.path && nodeInfo.path.startsWith('/')) {
1377
+ console.error('Absolute path detected in node, skipping:', nodeInfo.path);
1378
+ return;
824
1379
  }
825
1380
 
826
- // Check if node already exists (avoid duplicates)
827
- if (this.nodes.has(node.path)) {
828
- console.log('Node already exists:', node.path);
1381
+ // Also validate parent path
1382
+ if (parentPath && parentPath.startsWith('/')) {
1383
+ console.error('Absolute path detected in parent, skipping:', parentPath);
829
1384
  return;
830
1385
  }
831
1386
 
832
- // Add node to parent's children
833
- if (!parent.children) {
834
- parent.children = [];
1387
+ // Find parent node
1388
+ let parentNode = this.treeData;
1389
+
1390
+ if (parentPath) {
1391
+ parentNode = this.findNodeByPath(parentPath);
1392
+ if (!parentNode) {
1393
+ // CRITICAL: Do NOT create parent structure if it doesn't exist
1394
+ // This prevents creating nodes above the working directory
1395
+ console.warn('Parent node not found, skipping node creation:', parentPath);
1396
+ console.warn('Attempted to add node:', nodeInfo);
1397
+ return;
1398
+ }
1399
+ }
1400
+
1401
+ // Check if node already exists
1402
+ const existingNode = parentNode.children?.find(c =>
1403
+ c.path === nodeInfo.path ||
1404
+ (c.name === nodeInfo.name && c.type === nodeInfo.type)
1405
+ );
1406
+
1407
+ if (existingNode) {
1408
+ // Update existing node
1409
+ Object.assign(existingNode, nodeInfo);
1410
+ return;
1411
+ }
1412
+
1413
+ // Add new node
1414
+ if (!parentNode.children) {
1415
+ parentNode.children = [];
835
1416
  }
836
- parent.children.push(node);
837
1417
 
838
- // Store node reference
839
- this.nodes.set(node.path, node);
1418
+ // Ensure the node has a children array
1419
+ if (!nodeInfo.children) {
1420
+ nodeInfo.children = [];
1421
+ }
840
1422
 
841
- // Update hierarchy only if D3 is available
842
- if (typeof d3 !== 'undefined') {
843
- // Preserve expanded/collapsed state
844
- const oldExpandedNodes = new Set();
845
- if (this.root) {
846
- this.root.descendants().forEach(d => {
847
- if (d.children) {
848
- oldExpandedNodes.add(d.data.path);
849
- }
850
- });
851
- }
852
-
853
- // Create new hierarchy
1423
+ parentNode.children.push(nodeInfo);
1424
+
1425
+ // Store node reference for quick access
1426
+ this.nodes.set(nodeInfo.path, nodeInfo);
1427
+
1428
+ // Update tree if initialized
1429
+ if (this.root && this.svg) {
1430
+ // Recreate hierarchy with new data
854
1431
  this.root = d3.hierarchy(this.treeData);
855
1432
  this.root.x0 = this.height / 2;
856
1433
  this.root.y0 = 0;
857
1434
 
858
- // Restore expanded state
859
- this.root.descendants().forEach(d => {
860
- if (oldExpandedNodes.has(d.data.path) && d._children) {
861
- d.children = d._children;
862
- d._children = null;
863
- }
864
- });
1435
+ // Update only if we have a reasonable number of nodes to avoid performance issues
1436
+ if (this.nodes.size < 1000) {
1437
+ this.update(this.root);
1438
+ } else if (this.nodes.size % 100 === 0) {
1439
+ // Update every 100 nodes for large trees
1440
+ this.update(this.root);
1441
+ }
865
1442
  }
866
1443
  }
867
1444
 
868
1445
  /**
869
- * Find node by path
1446
+ * Find node by path in tree
1447
+ */
1448
+ findNodeByPath(path, node = null) {
1449
+ if (!node) {
1450
+ node = this.treeData;
1451
+ // Searching for node by path
1452
+ // Root node identified
1453
+ }
1454
+
1455
+ if (node.path === path) {
1456
+ return node;
1457
+ }
1458
+
1459
+ if (node.children) {
1460
+ for (const child of node.children) {
1461
+ const found = this.findNodeByPath(path, child);
1462
+ if (found) {
1463
+ return found;
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ if (!node.parent) {
1469
+ // Node path not found in current tree structure
1470
+ }
1471
+ return null;
1472
+ }
1473
+
1474
+ /**
1475
+ * Helper to log all paths in tree for debugging
1476
+ */
1477
+ logAllPaths(node, indent = '') {
1478
+ console.log(`${indent}${node.path} (${node.name})`);
1479
+ if (node.children) {
1480
+ for (const child of node.children) {
1481
+ this.logAllPaths(child, indent + ' ');
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ /**
1487
+ * Find D3 hierarchy node by path
870
1488
  */
871
- findNodeByPath(path) {
872
- return this.nodes.get(path);
1489
+ findD3NodeByPath(path) {
1490
+ if (!this.root) return null;
1491
+ return this.root.descendants().find(d => d.data.path === path);
1492
+ }
1493
+
1494
+ /**
1495
+ * Preserve expansion state when recreating hierarchy
1496
+ */
1497
+ preserveExpansionState(oldRoot, newRoot) {
1498
+ if (!oldRoot || !newRoot) return;
1499
+
1500
+ // Create a map of expanded nodes from the old tree
1501
+ const expansionMap = new Map();
1502
+ oldRoot.descendants().forEach(node => {
1503
+ if (node.data.expanded || (node.children && !node._children)) {
1504
+ expansionMap.set(node.data.path, true);
1505
+ }
1506
+ });
1507
+
1508
+ // Apply expansion state to new tree
1509
+ newRoot.descendants().forEach(node => {
1510
+ if (expansionMap.has(node.data.path)) {
1511
+ node.children = node._children || node.children;
1512
+ node._children = null;
1513
+ node.data.expanded = true;
1514
+ }
1515
+ });
873
1516
  }
874
1517
 
875
1518
  /**
876
- * Update progress in footer
1519
+ * Update statistics display
877
1520
  */
878
- updateProgress(percentage, message) {
879
- const footerStatus = document.getElementById('footer-analysis-progress');
880
- if (footerStatus) {
881
- // Format the message with percentage if available
882
- let statusText = message || 'Analyzing...';
883
- if (percentage > 0) {
884
- statusText = `[${Math.round(percentage)}%] ${statusText}`;
1521
+ updateStats() {
1522
+ // Update stats display - use correct IDs from HTML
1523
+ const statsElements = {
1524
+ 'file-count': this.stats.files,
1525
+ 'class-count': this.stats.classes,
1526
+ 'function-count': this.stats.functions,
1527
+ 'line-count': this.stats.lines
1528
+ };
1529
+
1530
+ for (const [id, value] of Object.entries(statsElements)) {
1531
+ const elem = document.getElementById(id);
1532
+ if (elem) {
1533
+ elem.textContent = value.toLocaleString();
885
1534
  }
886
- footerStatus.textContent = statusText;
1535
+ }
1536
+
1537
+ // Update progress text
1538
+ const progressText = document.getElementById('code-progress-text');
1539
+ if (progressText) {
1540
+ const statusText = this.analyzing ?
1541
+ `Analyzing... ${this.stats.files} files processed` :
1542
+ `Ready - ${this.stats.files} files in tree`;
1543
+ progressText.textContent = statusText;
1544
+ }
1545
+ }
1546
+
1547
+ /**
1548
+ * Update breadcrumb trail
1549
+ */
1550
+ updateBreadcrumb(message, type = 'info') {
1551
+ const breadcrumbContent = document.getElementById('breadcrumb-content');
1552
+ if (breadcrumbContent) {
1553
+ breadcrumbContent.textContent = message;
1554
+ breadcrumbContent.className = `breadcrumb-${type}`;
887
1555
  }
888
1556
  }
1557
+
1558
+ /**
1559
+ * Detect language from file extension
1560
+ */
1561
+ detectLanguage(filePath) {
1562
+ const ext = filePath.split('.').pop().toLowerCase();
1563
+ const languageMap = {
1564
+ 'py': 'python',
1565
+ 'js': 'javascript',
1566
+ 'ts': 'typescript',
1567
+ 'jsx': 'javascript',
1568
+ 'tsx': 'typescript',
1569
+ 'java': 'java',
1570
+ 'cpp': 'cpp',
1571
+ 'c': 'c',
1572
+ 'cs': 'csharp',
1573
+ 'rb': 'ruby',
1574
+ 'go': 'go',
1575
+ 'rs': 'rust',
1576
+ 'php': 'php',
1577
+ 'swift': 'swift',
1578
+ 'kt': 'kotlin',
1579
+ 'scala': 'scala',
1580
+ 'r': 'r',
1581
+ 'sh': 'bash',
1582
+ 'ps1': 'powershell'
1583
+ };
1584
+ return languageMap[ext] || 'unknown';
1585
+ }
1586
+
1587
+ /**
1588
+ * Add visualization controls for layout toggle
1589
+ */
1590
+ addVisualizationControls() {
1591
+ const controls = this.svg.append('g')
1592
+ .attr('class', 'viz-controls')
1593
+ .attr('transform', 'translate(10, 10)');
1594
+
1595
+ // Add layout toggle button
1596
+ const toggleButton = controls.append('g')
1597
+ .attr('class', 'layout-toggle')
1598
+ .style('cursor', 'pointer')
1599
+ .on('click', () => this.toggleLayout());
1600
+
1601
+ toggleButton.append('rect')
1602
+ .attr('width', 120)
1603
+ .attr('height', 30)
1604
+ .attr('rx', 5)
1605
+ .attr('fill', '#3b82f6')
1606
+ .attr('opacity', 0.8);
1607
+
1608
+ toggleButton.append('text')
1609
+ .attr('x', 60)
1610
+ .attr('y', 20)
1611
+ .attr('text-anchor', 'middle')
1612
+ .attr('fill', 'white')
1613
+ .style('font-size', '12px')
1614
+ .text(this.isRadialLayout ? 'Switch to Linear' : 'Switch to Radial');
1615
+ }
889
1616
 
890
1617
  /**
891
- * Show analysis status in footer
1618
+ * Toggle between radial and linear layouts
892
1619
  */
893
- showFooterAnalysisStatus(message) {
894
- const container = document.getElementById('footer-analysis-container');
895
- const progress = document.getElementById('footer-analysis-progress');
1620
+ toggleLayout() {
1621
+ this.isRadialLayout = !this.isRadialLayout;
1622
+ this.createVisualization();
1623
+ if (this.root) {
1624
+ this.update(this.root);
1625
+ }
1626
+ this.showNotification(
1627
+ this.isRadialLayout ? 'Switched to radial layout' : 'Switched to linear layout',
1628
+ 'info'
1629
+ );
1630
+ }
1631
+
1632
+ /**
1633
+ * Convert radial coordinates to Cartesian
1634
+ */
1635
+ radialPoint(x, y) {
1636
+ return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
1637
+ }
1638
+
1639
+ /**
1640
+ * Update D3 tree visualization
1641
+ */
1642
+ update(source) {
1643
+ if (!this.treeLayout || !this.treeGroup || !source) {
1644
+ return;
1645
+ }
1646
+
1647
+ // Compute the new tree layout
1648
+ const treeData = this.treeLayout(this.root);
1649
+ const nodes = treeData.descendants();
1650
+ const links = treeData.descendants().slice(1);
1651
+
1652
+ if (this.isRadialLayout) {
1653
+ // Radial layout adjustments
1654
+ nodes.forEach(d => {
1655
+ // Store original x,y for transitions
1656
+ if (d.x0 === undefined) {
1657
+ d.x0 = d.x;
1658
+ d.y0 = d.y;
1659
+ }
1660
+ });
1661
+ } else {
1662
+ // Linear layout with nodeSize doesn't need manual normalization
1663
+ // The tree layout handles spacing automatically
1664
+ }
1665
+
1666
+ // Update nodes
1667
+ const node = this.treeGroup.selectAll('g.node')
1668
+ .data(nodes, d => d.id || (d.id = ++this.nodeId));
1669
+
1670
+ // Enter new nodes
1671
+ const nodeEnter = node.enter().append('g')
1672
+ .attr('class', 'node')
1673
+ .attr('transform', d => {
1674
+ if (this.isRadialLayout) {
1675
+ const [x, y] = this.radialPoint(source.x0 || 0, source.y0 || 0);
1676
+ return `translate(${x},${y})`;
1677
+ } else {
1678
+ return `translate(${source.y0},${source.x0})`;
1679
+ }
1680
+ })
1681
+ .on('click', (event, d) => this.onNodeClick(event, d));
1682
+
1683
+ // Add circles for nodes
1684
+ nodeEnter.append('circle')
1685
+ .attr('class', 'node-circle')
1686
+ .attr('r', 1e-6)
1687
+ .style('fill', d => this.getNodeColor(d))
1688
+ .style('stroke', d => this.getNodeStrokeColor(d))
1689
+ .style('stroke-width', 2)
1690
+ .on('mouseover', (event, d) => this.showTooltip(event, d))
1691
+ .on('mouseout', () => this.hideTooltip());
1692
+
1693
+ // Add labels for nodes with smart positioning
1694
+ nodeEnter.append('text')
1695
+ .attr('class', 'node-label')
1696
+ .attr('dy', '.35em')
1697
+ .attr('x', d => {
1698
+ if (this.isRadialLayout) {
1699
+ // For radial layout, initial position
1700
+ return 0;
1701
+ } else {
1702
+ // Linear layout: standard positioning
1703
+ return d.children || d._children ? -13 : 13;
1704
+ }
1705
+ })
1706
+ .attr('text-anchor', d => {
1707
+ if (this.isRadialLayout) {
1708
+ return 'start'; // Will be adjusted in update
1709
+ } else {
1710
+ // Linear layout: standard anchoring
1711
+ return d.children || d._children ? 'end' : 'start';
1712
+ }
1713
+ })
1714
+ .text(d => {
1715
+ // Truncate long names
1716
+ const maxLength = 20;
1717
+ const name = d.data.name || '';
1718
+ return name.length > maxLength ?
1719
+ name.substring(0, maxLength - 3) + '...' : name;
1720
+ })
1721
+ .style('fill-opacity', 1e-6)
1722
+ .style('font-size', '12px')
1723
+ .style('font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif')
1724
+ .style('text-shadow', '1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8)');
1725
+
1726
+ // Add icons for node types
1727
+ nodeEnter.append('text')
1728
+ .attr('class', 'node-icon')
1729
+ .attr('dy', '.35em')
1730
+ .attr('x', 0)
1731
+ .attr('text-anchor', 'middle')
1732
+ .text(d => this.getNodeIcon(d))
1733
+ .style('font-size', '10px')
1734
+ .style('fill', 'white');
1735
+
1736
+ // Transition to new positions
1737
+ const nodeUpdate = nodeEnter.merge(node);
1738
+
1739
+ nodeUpdate.transition()
1740
+ .duration(this.duration)
1741
+ .attr('transform', d => {
1742
+ if (this.isRadialLayout) {
1743
+ const [x, y] = this.radialPoint(d.x, d.y);
1744
+ return `translate(${x},${y})`;
1745
+ } else {
1746
+ return `translate(${d.y},${d.x})`;
1747
+ }
1748
+ });
1749
+
1750
+ nodeUpdate.select('circle.node-circle')
1751
+ .attr('r', 8)
1752
+ .style('fill', d => this.getNodeColor(d))
1753
+ .style('stroke', d => this.getNodeStrokeColor(d))
1754
+ .attr('cursor', 'pointer');
1755
+
1756
+ // Update text labels with proper rotation for radial layout
1757
+ const isRadial = this.isRadialLayout; // Capture the layout type
1758
+ nodeUpdate.select('text.node-label')
1759
+ .style('fill-opacity', 1)
1760
+ .style('fill', '#333')
1761
+ .each(function(d) {
1762
+ const selection = d3.select(this);
1763
+
1764
+ if (isRadial) {
1765
+ // For radial layout, apply rotation and positioning
1766
+ const angle = (d.x * 180 / Math.PI) - 90; // Convert to degrees
1767
+
1768
+ // Determine if text should be flipped (left side of circle)
1769
+ const shouldFlip = angle > 90 || angle < -90;
1770
+
1771
+ // Calculate text position and rotation
1772
+ if (shouldFlip) {
1773
+ // Text on left side - rotate 180 degrees to read properly
1774
+ selection
1775
+ .attr('transform', `rotate(${angle + 180})`)
1776
+ .attr('x', -15) // Negative offset for flipped text
1777
+ .attr('text-anchor', 'end')
1778
+ .attr('dy', '.35em');
1779
+ } else {
1780
+ // Text on right side - normal orientation
1781
+ selection
1782
+ .attr('transform', `rotate(${angle})`)
1783
+ .attr('x', 15) // Positive offset for normal text
1784
+ .attr('text-anchor', 'start')
1785
+ .attr('dy', '.35em');
1786
+ }
1787
+ } else {
1788
+ // Linear layout - no rotation needed
1789
+ selection
1790
+ .attr('transform', null)
1791
+ .attr('x', d.children || d._children ? -13 : 13)
1792
+ .attr('text-anchor', d.children || d._children ? 'end' : 'start')
1793
+ .attr('dy', '.35em');
1794
+ }
1795
+ });
1796
+
1797
+ // Remove exiting nodes
1798
+ const nodeExit = node.exit().transition()
1799
+ .duration(this.duration)
1800
+ .attr('transform', d => {
1801
+ if (this.isRadialLayout) {
1802
+ const [x, y] = this.radialPoint(source.x, source.y);
1803
+ return `translate(${x},${y})`;
1804
+ } else {
1805
+ return `translate(${source.y},${source.x})`;
1806
+ }
1807
+ })
1808
+ .remove();
1809
+
1810
+ nodeExit.select('circle')
1811
+ .attr('r', 1e-6);
1812
+
1813
+ nodeExit.select('text.node-label')
1814
+ .style('fill-opacity', 1e-6);
896
1815
 
897
- if (container) {
898
- container.style.display = 'flex';
1816
+ nodeExit.select('text.node-icon')
1817
+ .style('fill-opacity', 1e-6);
1818
+
1819
+ // Update links
1820
+ const link = this.treeGroup.selectAll('path.link')
1821
+ .data(links, d => d.id);
1822
+
1823
+ // Enter new links
1824
+ const linkEnter = link.enter().insert('path', 'g')
1825
+ .attr('class', 'link')
1826
+ .attr('d', d => {
1827
+ const o = {x: source.x0, y: source.y0};
1828
+ return this.isRadialLayout ?
1829
+ this.radialDiagonal(o, o) :
1830
+ this.diagonal(o, o);
1831
+ })
1832
+ .style('fill', 'none')
1833
+ .style('stroke', '#ccc')
1834
+ .style('stroke-width', 2);
1835
+
1836
+ // Transition to new positions
1837
+ const linkUpdate = linkEnter.merge(link);
1838
+
1839
+ linkUpdate.transition()
1840
+ .duration(this.duration)
1841
+ .attr('d', d => this.isRadialLayout ?
1842
+ this.radialDiagonal(d, d.parent) :
1843
+ this.diagonal(d, d.parent));
1844
+
1845
+ // Remove exiting links
1846
+ link.exit().transition()
1847
+ .duration(this.duration)
1848
+ .attr('d', d => {
1849
+ const o = {x: source.x, y: source.y};
1850
+ return this.isRadialLayout ?
1851
+ this.radialDiagonal(o, o) :
1852
+ this.diagonal(o, o);
1853
+ })
1854
+ .remove();
1855
+
1856
+ // Store old positions for transition
1857
+ nodes.forEach(d => {
1858
+ d.x0 = d.x;
1859
+ d.y0 = d.y;
1860
+ });
1861
+ }
1862
+
1863
+ /**
1864
+ * Center the view on a specific node (Linear layout)
1865
+ */
1866
+ centerOnNode(d) {
1867
+ if (!this.svg || !this.zoom) return;
1868
+
1869
+ // Get current transform or use default zoom level
1870
+ const currentTransform = d3.zoomTransform(this.svg.node());
1871
+ // Zoom in to 2x for better focus on clicked node
1872
+ const targetScale = currentTransform.k < 2 ? 2 : currentTransform.k;
1873
+
1874
+ // Account for the initial tree group offset
1875
+ const initialOffsetX = this.margin.left + 100;
1876
+ const initialOffsetY = this.height / 2;
1877
+
1878
+ // Calculate position to center the node accounting for the tree group's transform
1879
+ const x = initialOffsetX - d.y * targetScale + this.width / 2;
1880
+ const y = initialOffsetY - d.x * targetScale + this.height / 2;
1881
+
1882
+ this.svg.transition()
1883
+ .duration(750)
1884
+ .call(
1885
+ this.zoom.transform,
1886
+ d3.zoomIdentity
1887
+ .translate(x, y)
1888
+ .scale(targetScale)
1889
+ );
1890
+ }
1891
+
1892
+ /**
1893
+ * Center the view on a specific node (Radial layout)
1894
+ */
1895
+ centerOnNodeRadial(d) {
1896
+ if (!this.svg || !this.zoom) return;
1897
+
1898
+ // Use the same radialPoint function for consistency
1899
+ const [x, y] = this.radialPoint(d.x, d.y);
1900
+
1901
+ // Get current transform or use default zoom level
1902
+ const currentTransform = d3.zoomTransform(this.svg.node());
1903
+ // Zoom in to 2x for better focus on clicked node
1904
+ const targetScale = currentTransform.k < 2 ? 2 : currentTransform.k;
1905
+
1906
+ // Account for the initial tree group centering
1907
+ const centerX = this.width / 2;
1908
+ const centerY = this.height / 2;
1909
+
1910
+ // Calculate translation to center the node
1911
+ // The tree group is centered at centerX, centerY
1912
+ // We need to offset by the node's position scaled
1913
+ const targetX = centerX - x * targetScale;
1914
+ const targetY = centerY - y * targetScale;
1915
+
1916
+ // Apply smooth transition to center the node with zoom
1917
+ this.svg.transition()
1918
+ .duration(750)
1919
+ .call(
1920
+ this.zoom.transform,
1921
+ d3.zoomIdentity
1922
+ .translate(targetX, targetY)
1923
+ .scale(targetScale)
1924
+ );
1925
+ }
1926
+
1927
+ /**
1928
+ * Highlight the active node with larger icon
1929
+ */
1930
+ highlightActiveNode(d) {
1931
+ // Reset all nodes to normal size and clear parent context
1932
+ // First clear classes on the selection
1933
+ const allCircles = this.treeGroup.selectAll('circle.node-circle');
1934
+ allCircles
1935
+ .classed('active', false)
1936
+ .classed('parent-context', false);
1937
+
1938
+ // Then apply transition separately
1939
+ allCircles
1940
+ .transition()
1941
+ .duration(300)
1942
+ .attr('r', 8)
1943
+ .style('stroke', null)
1944
+ .style('stroke-width', null)
1945
+ .style('opacity', null);
1946
+
1947
+ // Reset all labels to normal
1948
+ this.treeGroup.selectAll('text.node-label')
1949
+ .style('font-weight', 'normal')
1950
+ .style('font-size', '12px');
1951
+
1952
+ // Find and increase size of clicked node - use data matching
1953
+ // Make the size increase MUCH more dramatic: 8 -> 20 (2.5x the size)
1954
+ const activeNodeCircle = this.treeGroup.selectAll('g.node')
1955
+ .filter(node => node === d)
1956
+ .select('circle.node-circle');
1957
+
1958
+ // First set the class (not part of transition)
1959
+ activeNodeCircle.classed('active', true);
1960
+
1961
+ // Then apply the transition with styles - MUCH LARGER
1962
+ activeNodeCircle
1963
+ .transition()
1964
+ .duration(300)
1965
+ .attr('r', 20) // Much larger radius (2.5x)
1966
+ .style('stroke', '#3b82f6')
1967
+ .style('stroke-width', 5) // Thicker border
1968
+ .style('filter', 'drop-shadow(0 0 15px rgba(59, 130, 246, 0.6))'); // Stronger glow effect
1969
+
1970
+ // Also make the label bold
1971
+ this.treeGroup.selectAll('g.node')
1972
+ .filter(node => node === d)
1973
+ .select('text.node-label')
1974
+ .style('font-weight', 'bold')
1975
+ .style('font-size', '14px'); // Slightly larger text
1976
+
1977
+ // Store active node
1978
+ this.activeNode = d;
1979
+ }
1980
+
1981
+ /**
1982
+ * Add pulsing animation for loading state
1983
+ */
1984
+ addLoadingPulse(d) {
1985
+ // Use consistent selection pattern
1986
+ const node = this.treeGroup.selectAll('g.node')
1987
+ .filter(node => node === d)
1988
+ .select('circle.node-circle');
1989
+
1990
+ // Add to loading set
1991
+ this.loadingNodes.add(d.data.path);
1992
+
1993
+ // Add pulsing class and orange color - separate operations
1994
+ node.classed('loading-pulse', true);
1995
+ node.style('fill', '#fb923c'); // Orange color for loading
1996
+
1997
+ // Create pulse animation
1998
+ const pulseAnimation = () => {
1999
+ if (!this.loadingNodes.has(d.data.path)) return;
2000
+
2001
+ node.transition()
2002
+ .duration(600)
2003
+ .attr('r', 14)
2004
+ .style('opacity', 0.6)
2005
+ .transition()
2006
+ .duration(600)
2007
+ .attr('r', 10)
2008
+ .style('opacity', 1)
2009
+ .on('end', () => {
2010
+ if (this.loadingNodes.has(d.data.path)) {
2011
+ pulseAnimation(); // Continue pulsing
2012
+ }
2013
+ });
2014
+ };
2015
+
2016
+ pulseAnimation();
2017
+ }
2018
+
2019
+ /**
2020
+ * Remove pulsing animation when loading complete
2021
+ */
2022
+ removeLoadingPulse(d) {
2023
+ // Remove from loading set
2024
+ this.loadingNodes.delete(d.data.path);
2025
+
2026
+ // Use consistent selection pattern
2027
+ const node = this.treeGroup.selectAll('g.node')
2028
+ .filter(node => node === d)
2029
+ .select('circle.node-circle');
2030
+
2031
+ // Clear class first
2032
+ node.classed('loading-pulse', false);
2033
+
2034
+ // Then interrupt and transition
2035
+ node.interrupt() // Stop animation
2036
+ .transition()
2037
+ .duration(300)
2038
+ .attr('r', this.activeNode === d ? 20 : 8) // Use 20 for active node
2039
+ .style('opacity', 1)
2040
+ .style('fill', d => this.getNodeColor(d)); // Restore original color
2041
+ }
2042
+
2043
+ /**
2044
+ * Show parent node alongside for context
2045
+ */
2046
+ showWithParent(d) {
2047
+ if (!d.parent) return;
2048
+
2049
+ // Make parent more visible
2050
+ const parentNode = this.treeGroup.selectAll('g.node')
2051
+ .filter(node => node === d.parent);
2052
+
2053
+ // Highlight parent with different style - separate class from styles
2054
+ const parentCircle = parentNode.select('circle.node-circle');
2055
+ parentCircle.classed('parent-context', true);
2056
+ parentCircle
2057
+ .style('stroke', '#10b981')
2058
+ .style('stroke-width', 3)
2059
+ .style('opacity', 0.8);
2060
+
2061
+ // For radial, adjust zoom to show both parent and clicked node
2062
+ if (this.isRadialLayout && d.parent) {
2063
+ // Calculate bounding box including parent and immediate children
2064
+ const nodes = [d, d.parent];
2065
+ if (d.children) nodes.push(...d.children);
2066
+ else if (d._children) nodes.push(...d._children);
2067
+
2068
+ const angles = nodes.map(n => n.x);
2069
+ const radii = nodes.map(n => n.y);
2070
+
2071
+ const minAngle = Math.min(...angles);
2072
+ const maxAngle = Math.max(...angles);
2073
+ const maxRadius = Math.max(...radii);
2074
+
2075
+ // Zoom to fit parent and children
2076
+ const angleSpan = maxAngle - minAngle;
2077
+ const scale = Math.min(
2078
+ angleSpan > 0 ? (Math.PI * 2) / (angleSpan * 2) : 2.5, // Fit angle span
2079
+ this.width / (2 * maxRadius), // Fit radius
2080
+ 2.5 // Max zoom
2081
+ );
2082
+
2083
+ // Calculate center angle and radius
2084
+ const centerAngle = (minAngle + maxAngle) / 2;
2085
+ const centerRadius = maxRadius / 2;
2086
+ const centerX = centerRadius * Math.cos(centerAngle - Math.PI / 2);
2087
+ const centerY = centerRadius * Math.sin(centerAngle - Math.PI / 2);
2088
+
2089
+ this.svg.transition()
2090
+ .duration(750)
2091
+ .call(
2092
+ this.zoom.transform,
2093
+ d3.zoomIdentity
2094
+ .translate(this.width / 2 - centerX * scale, this.height / 2 - centerY * scale)
2095
+ .scale(scale)
2096
+ );
2097
+ }
2098
+ }
2099
+
2100
+ /**
2101
+ * Handle node click - implement lazy loading with enhanced visual feedback
2102
+ */
2103
+ onNodeClick(event, d) {
2104
+ // Handle node click interaction
2105
+
2106
+ // Check event parameter
2107
+ if (event) {
2108
+ try {
2109
+ if (typeof event.stopPropagation === 'function') {
2110
+ event.stopPropagation();
2111
+ } else {
2112
+ }
2113
+ } catch (error) {
2114
+ console.error('[CodeTree] ERROR calling stopPropagation:', error);
2115
+ }
2116
+ } else {
899
2117
  }
900
2118
 
901
- if (progress) {
902
- progress.textContent = message || 'Analyzing...';
903
- // Add pulsing animation
904
- progress.style.animation = 'pulse 1.5s ease-in-out infinite';
2119
+ // Check d parameter structure
2120
+ if (!d) {
2121
+ console.error('[CodeTree] ERROR: d is null/undefined, cannot continue');
2122
+ return;
2123
+ }
2124
+
2125
+ if (!d.data) {
2126
+ console.error('[CodeTree] ERROR: d.data is null/undefined, cannot continue');
2127
+ return;
905
2128
  }
2129
+
2130
+ hasName: d.data.hasOwnProperty('name'),
2131
+ hasPath: d.data.hasOwnProperty('path'),
2132
+ hasType: d.data.hasOwnProperty('type'),
2133
+ hasLoaded: d.data.hasOwnProperty('loaded'),
2134
+ name: d.data.name,
2135
+ path: d.data.path,
2136
+ type: d.data.type,
2137
+ loaded: d.data.loaded
2138
+ });
2139
+
2140
+ // Node interaction detected
2141
+
2142
+ // === PHASE 1: Immediate Visual Effects (Synchronous) ===
2143
+ // These execute immediately before any async operations
2144
+
2145
+
2146
+ // Center on clicked node (immediate visual effect)
2147
+ try {
2148
+ if (this.isRadialLayout) {
2149
+ if (typeof this.centerOnNodeRadial === 'function') {
2150
+ this.centerOnNodeRadial(d);
2151
+ } else {
2152
+ console.error('[CodeTree] centerOnNodeRadial is not a function!');
2153
+ }
2154
+ } else {
2155
+ if (typeof this.centerOnNode === 'function') {
2156
+ this.centerOnNode(d);
2157
+ } else {
2158
+ console.error('[CodeTree] centerOnNode is not a function!');
2159
+ }
2160
+ }
2161
+ } catch (error) {
2162
+ console.error('[CodeTree] ERROR during centering:', error, error.stack);
2163
+ }
2164
+
2165
+
2166
+ // Highlight with larger icon (immediate visual effect)
2167
+ try {
2168
+ if (typeof this.highlightActiveNode === 'function') {
2169
+ this.highlightActiveNode(d);
2170
+ } else {
2171
+ console.error('[CodeTree] highlightActiveNode is not a function!');
2172
+ }
2173
+ } catch (error) {
2174
+ console.error('[CodeTree] ERROR during highlightActiveNode:', error, error.stack);
2175
+ }
2176
+
2177
+
2178
+ // Show parent context (immediate visual effect)
2179
+ try {
2180
+ if (typeof this.showWithParent === 'function') {
2181
+ this.showWithParent(d);
2182
+ } else {
2183
+ console.error('[CodeTree] showWithParent is not a function!');
2184
+ }
2185
+ } catch (error) {
2186
+ console.error('[CodeTree] ERROR during showWithParent:', error, error.stack);
2187
+ }
2188
+
2189
+
2190
+ // Add pulsing animation immediately for directories
2191
+
2192
+ if (d.data.type === 'directory' && !d.data.loaded) {
2193
+ try {
2194
+ if (typeof this.addLoadingPulse === 'function') {
2195
+ this.addLoadingPulse(d);
2196
+ } else {
2197
+ console.error('[CodeTree] addLoadingPulse is not a function!');
2198
+ }
2199
+ } catch (error) {
2200
+ console.error('[CodeTree] ERROR during addLoadingPulse:', error, error.stack);
2201
+ }
2202
+ } else {
2203
+ }
2204
+
2205
+
2206
+ // === PHASE 2: Prepare Data (Synchronous) ===
2207
+
2208
+
2209
+ // Get selected languages from checkboxes
2210
+ const selectedLanguages = [];
2211
+ const checkboxes = document.querySelectorAll('.language-checkbox:checked');
2212
+ checkboxes.forEach(cb => {
2213
+ selectedLanguages.push(cb.value);
2214
+ });
2215
+
2216
+ // Get ignore patterns
2217
+ const ignorePatternsElement = document.getElementById('ignore-patterns');
2218
+ const ignorePatterns = ignorePatternsElement?.value || '';
2219
+
2220
+
2221
+ // === PHASE 3: Async Operations (Delayed) ===
2222
+ // Add a small delay to ensure visual effects are rendered first
2223
+
2224
+ type: d.data.type,
2225
+ loaded: d.data.loaded,
2226
+ typeIsDirectory: d.data.type === 'directory',
2227
+ loadedIsFalsy: !d.data.loaded,
2228
+ willRequestDiscovery: (d.data.type === 'directory' && !d.data.loaded)
2229
+ });
2230
+
2231
+ // For directories that haven't been loaded yet, request discovery
2232
+ if (d.data.type === 'directory' && !d.data.loaded) {
2233
+ // Mark as loading immediately to prevent duplicate requests
2234
+ d.data.loaded = 'loading';
2235
+
2236
+ // Ensure path is absolute or relative to working directory
2237
+ const fullPath = this.ensureFullPath(d.data.path);
2238
+
2239
+ // Sending discovery request for child content
2240
+
2241
+ // Store reference to the D3 node for later expansion
2242
+ const clickedD3Node = d;
2243
+
2244
+ // Delay the socket request to ensure visual effects are rendered
2245
+ setTimeout(() => {
2246
+
2247
+ // Request directory contents via Socket.IO
2248
+ if (this.socket) {
2249
+ this.socket.emit('code:discover:directory', {
2250
+ path: fullPath,
2251
+ depth: 1, // Only get immediate children
2252
+ languages: selectedLanguages,
2253
+ ignore_patterns: ignorePatterns
2254
+ });
2255
+
2256
+ this.updateBreadcrumb(`Loading ${d.data.name}...`, 'info');
2257
+ this.showNotification(`Loading directory: ${d.data.name}`, 'info');
2258
+ } else {
2259
+ }
2260
+ }, 100); // 100ms delay to ensure visual effects render first
2261
+ }
2262
+ // For files that haven't been analyzed, request analysis
2263
+ else if (d.data.type === 'file' && !d.data.analyzed) {
2264
+ // Only analyze files of selected languages
2265
+ const fileLanguage = this.detectLanguage(d.data.path);
2266
+ if (!selectedLanguages.includes(fileLanguage) && fileLanguage !== 'unknown') {
2267
+ this.showNotification(`Skipping ${d.data.name} - ${fileLanguage} not selected`, 'warning');
2268
+ return;
2269
+ }
2270
+
2271
+ // Add pulsing animation immediately
2272
+ this.addLoadingPulse(d);
2273
+
2274
+ // Mark as loading immediately
2275
+ d.data.analyzed = 'loading';
2276
+
2277
+ // Ensure path is absolute or relative to working directory
2278
+ const fullPath = this.ensureFullPath(d.data.path);
2279
+
2280
+ // Delay the socket request to ensure visual effects are rendered
2281
+ setTimeout(() => {
2282
+
2283
+ if (this.socket) {
2284
+ this.socket.emit('code:analyze:file', {
2285
+ path: fullPath
2286
+ });
2287
+
2288
+ this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
2289
+ this.showNotification(`Analyzing: ${d.data.name}`, 'info');
2290
+ }
2291
+ }, 100); // 100ms delay to ensure visual effects render first
2292
+ }
2293
+ // Toggle children visibility for already loaded nodes
2294
+ else if (d.data.type === 'directory' && d.data.loaded === true) {
2295
+ // Directory is loaded, toggle expansion
2296
+ if (d.children) {
2297
+ // Collapse - hide children
2298
+ d._children = d.children;
2299
+ d.children = null;
2300
+ d.data.expanded = false;
2301
+ } else if (d._children) {
2302
+ // Expand - show children
2303
+ d.children = d._children;
2304
+ d._children = null;
2305
+ d.data.expanded = true;
2306
+ } else if (d.data.children && d.data.children.length > 0) {
2307
+ // Children exist in data but not in D3 node, recreate hierarchy
2308
+ this.root = d3.hierarchy(this.treeData);
2309
+ const updatedD3Node = this.findD3NodeByPath(d.data.path);
2310
+ if (updatedD3Node) {
2311
+ updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
2312
+ updatedD3Node._children = null;
2313
+ updatedD3Node.data.expanded = true;
2314
+ }
2315
+ }
2316
+ this.update(this.root);
2317
+ }
2318
+ // Also handle other nodes that might have children
2319
+ else if (d.children || d._children) {
2320
+ if (d.children) {
2321
+ d._children = d.children;
2322
+ d.children = null;
2323
+ d.data.expanded = false;
2324
+ } else {
2325
+ d.children = d._children;
2326
+ d._children = null;
2327
+ d.data.expanded = true;
2328
+ }
2329
+ this.update(d);
2330
+ } else {
2331
+ }
2332
+
2333
+ // Update selection
2334
+ this.selectedNode = d;
2335
+ try {
2336
+ this.highlightNode(d);
2337
+ } catch (error) {
2338
+ console.error('[CodeTree] ERROR during highlightNode:', error);
2339
+ }
2340
+
906
2341
  }
907
2342
 
908
2343
  /**
909
- * Hide analysis status in footer
2344
+ * Ensure path is absolute or relative to working directory
910
2345
  */
911
- hideFooterAnalysisStatus() {
912
- const container = document.getElementById('footer-analysis-container');
913
- const progress = document.getElementById('footer-analysis-progress');
2346
+ ensureFullPath(path) {
2347
+ console.log('🔗 ensureFullPath called with:', path);
2348
+
2349
+ if (!path) return path;
2350
+
2351
+ // If already absolute, return as is
2352
+ if (path.startsWith('/')) {
2353
+ console.log(' → Already absolute, returning:', path);
2354
+ return path;
2355
+ }
2356
+
2357
+ // Get working directory
2358
+ const workingDir = this.getWorkingDirectory();
2359
+ console.log(' → Working directory:', workingDir);
2360
+
2361
+ if (!workingDir) {
2362
+ console.log(' → No working directory, returning original:', path);
2363
+ return path;
2364
+ }
914
2365
 
915
- if (container) {
916
- // Fade out after a brief delay
917
- setTimeout(() => {
918
- container.style.display = 'none';
919
- }, 2000);
2366
+ // Special handling for root path
2367
+ if (path === '.') {
2368
+ console.log(' → Root path detected, returning working dir:', workingDir);
2369
+ return workingDir;
920
2370
  }
921
2371
 
922
- if (progress) {
923
- progress.style.animation = 'none';
2372
+ // If path equals working directory, return as is
2373
+ if (path === workingDir) {
2374
+ console.log(' → Path equals working directory, returning:', workingDir);
2375
+ return workingDir;
924
2376
  }
925
- }
926
-
927
- /**
928
- * Update statistics display
929
- */
930
- updateStats() {
931
- const fileCount = document.getElementById('file-count');
932
- const classCount = document.getElementById('class-count');
933
- const functionCount = document.getElementById('function-count');
934
- const lineCount = document.getElementById('line-count');
935
2377
 
936
- if (fileCount) fileCount.textContent = this.stats.files;
937
- if (classCount) classCount.textContent = this.stats.classes;
938
- if (functionCount) functionCount.textContent = this.stats.functions;
939
- if (lineCount) lineCount.textContent = this.stats.lines;
2378
+ // Combine working directory with relative path
2379
+ const result = `${workingDir}/${path}`.replace(/\/+/g, '/');
2380
+ console.log(' → Combining with working dir, result:', result);
2381
+ return result;
940
2382
  }
941
2383
 
942
2384
  /**
943
- * Update tree visualization
2385
+ * Highlight selected node
944
2386
  */
945
- update(source) {
946
- if (!this.svg || !this.treeGroup) {
947
- return;
948
- }
949
-
950
- // Compute new tree layout
951
- const treeData = this.treeLayout(this.root);
952
- const nodes = treeData.descendants();
953
- const links = treeData.links();
954
-
955
- // Normalize for fixed-depth
956
- nodes.forEach(d => {
957
- d.y = d.depth * 180;
958
- });
959
-
960
- // Update nodes
961
- const node = this.treeGroup.selectAll('g.code-node')
962
- .data(nodes, d => d.id || (d.id = ++this.nodeId));
963
-
964
- // Enter new nodes
965
- const nodeEnter = node.enter().append('g')
966
- .attr('class', d => `code-node ${d.data.type} complexity-${this.getComplexityLevel(d.data.complexity)}`)
967
- .attr('transform', d => `translate(${source.y0},${source.x0})`)
968
- .on('click', (event, d) => this.toggleNode(event, d))
969
- .on('mouseover', (event, d) => this.showTooltip(event, d))
970
- .on('mouseout', () => this.hideTooltip());
971
-
972
- // Add circles
973
- nodeEnter.append('circle')
974
- .attr('r', 1e-6)
975
- .style('fill', d => d._children ? '#e2e8f0' : this.getNodeColor(d.data.type));
976
-
977
- // Add text labels
978
- nodeEnter.append('text')
979
- .attr('dy', '.35em')
980
- .attr('x', d => d.children || d._children ? -13 : 13)
981
- .attr('text-anchor', d => d.children || d._children ? 'end' : 'start')
982
- .text(d => d.data.name)
983
- .style('fill-opacity', 1e-6);
984
-
985
- // Add icons
986
- nodeEnter.append('text')
987
- .attr('class', 'node-icon')
988
- .attr('dy', '.35em')
989
- .attr('x', 0)
990
- .attr('text-anchor', 'middle')
991
- .text(d => this.getNodeIcon(d.data.type))
992
- .style('font-size', '16px');
993
-
994
- // Transition nodes to their new position
995
- const nodeUpdate = nodeEnter.merge(node);
996
-
997
- nodeUpdate.transition()
998
- .duration(this.duration)
999
- .attr('transform', d => `translate(${d.y},${d.x})`);
1000
-
1001
- nodeUpdate.select('circle')
1002
- .attr('r', 8)
1003
- .style('fill', d => d._children ? '#e2e8f0' : this.getNodeColor(d.data.type));
1004
-
1005
- nodeUpdate.select('text')
1006
- .style('fill-opacity', 1);
1007
-
1008
- // Remove exiting nodes
1009
- const nodeExit = node.exit().transition()
1010
- .duration(this.duration)
1011
- .attr('transform', d => `translate(${source.y},${source.x})`)
1012
- .remove();
1013
-
1014
- nodeExit.select('circle')
1015
- .attr('r', 1e-6);
1016
-
1017
- nodeExit.select('text')
1018
- .style('fill-opacity', 1e-6);
1019
-
1020
- // Update links
1021
- const link = this.treeGroup.selectAll('path.code-link')
1022
- .data(links, d => d.target.id);
1023
-
1024
- // Enter new links
1025
- const linkEnter = link.enter().insert('path', 'g')
1026
- .attr('class', 'code-link')
1027
- .attr('d', d => {
1028
- const o = {x: source.x0, y: source.y0};
1029
- return this.diagonal(o, o);
1030
- });
1031
-
1032
- // Transition links to their new position
1033
- const linkUpdate = linkEnter.merge(link);
1034
-
1035
- linkUpdate.transition()
1036
- .duration(this.duration)
1037
- .attr('d', d => this.diagonal(d.source, d.target));
1038
-
1039
- // Remove exiting links
1040
- link.exit().transition()
1041
- .duration(this.duration)
1042
- .attr('d', d => {
1043
- const o = {x: source.x, y: source.y};
1044
- return this.diagonal(o, o);
1045
- })
1046
- .remove();
1047
-
1048
- // Store old positions for transition
1049
- nodes.forEach(d => {
1050
- d.x0 = d.x;
1051
- d.y0 = d.y;
1052
- });
2387
+ highlightNode(node) {
2388
+ // Remove previous highlights
2389
+ this.treeGroup.selectAll('circle.node-circle')
2390
+ .style('stroke-width', 2)
2391
+ .classed('selected', false);
2392
+
2393
+ // Highlight selected node
2394
+ this.treeGroup.selectAll('circle.node-circle')
2395
+ .filter(d => d === node)
2396
+ .style('stroke-width', 4)
2397
+ .classed('selected', true);
1053
2398
  }
1054
2399
 
1055
2400
  /**
1056
2401
  * Create diagonal path for links
1057
2402
  */
1058
- diagonal(source, target) {
1059
- return `M ${source.y} ${source.x}
1060
- C ${(source.y + target.y) / 2} ${source.x},
1061
- ${(source.y + target.y) / 2} ${target.x},
1062
- ${target.y} ${target.x}`;
2403
+ diagonal(s, d) {
2404
+ return `M ${s.y} ${s.x}
2405
+ C ${(s.y + d.y) / 2} ${s.x},
2406
+ ${(s.y + d.y) / 2} ${d.x},
2407
+ ${d.y} ${d.x}`;
1063
2408
  }
1064
-
2409
+
1065
2410
  /**
1066
- * Toggle node expansion/collapse
2411
+ * Create radial diagonal path for links
1067
2412
  */
1068
- toggleNode(event, d) {
1069
- if (d.children) {
1070
- d._children = d.children;
1071
- d.children = null;
1072
- } else {
1073
- d.children = d._children;
1074
- d._children = null;
1075
- }
1076
-
1077
- this.update(d);
1078
-
1079
- // Update breadcrumb
1080
- this.updateBreadcrumb(d);
1081
-
1082
- // Mark as selected
1083
- this.selectNode(d);
1084
-
1085
- // Show code viewer if it's a code node
1086
- if (d.data.type !== 'module' && d.data.type !== 'file') {
1087
- this.showCodeViewer(d.data);
1088
- }
2413
+ radialDiagonal(s, d) {
2414
+ const path = d3.linkRadial()
2415
+ .angle(d => d.x)
2416
+ .radius(d => d.y);
2417
+ return path({source: s, target: d});
1089
2418
  }
1090
2419
 
1091
2420
  /**
1092
- * Select a node
2421
+ * Get node color based on type and complexity
1093
2422
  */
1094
- selectNode(node) {
1095
- // Remove previous selection
1096
- if (this.selectedNode) {
1097
- d3.select(this.selectedNode).classed('selected', false);
2423
+ getNodeColor(d) {
2424
+ const type = d.data.type;
2425
+ const complexity = d.data.complexity || 1;
2426
+
2427
+ // Base colors by type
2428
+ const baseColors = {
2429
+ 'root': '#6B7280',
2430
+ 'directory': '#3B82F6',
2431
+ 'file': '#10B981',
2432
+ 'module': '#8B5CF6',
2433
+ 'class': '#F59E0B',
2434
+ 'function': '#EF4444',
2435
+ 'method': '#EC4899'
2436
+ };
2437
+
2438
+ const baseColor = baseColors[type] || '#6B7280';
2439
+
2440
+ // Adjust brightness based on complexity (higher complexity = darker)
2441
+ if (complexity > 10) {
2442
+ return d3.color(baseColor).darker(0.5);
2443
+ } else if (complexity > 5) {
2444
+ return d3.color(baseColor).darker(0.25);
1098
2445
  }
1099
2446
 
1100
- // Add selection to new node
1101
- this.selectedNode = node;
1102
- if (node) {
1103
- d3.select(node).classed('selected', true);
1104
- }
2447
+ return baseColor;
1105
2448
  }
1106
2449
 
1107
2450
  /**
1108
- * Update breadcrumb navigation
2451
+ * Get node stroke color
1109
2452
  */
1110
- updateBreadcrumb(node) {
1111
- const path = [];
1112
- let current = node;
1113
-
1114
- while (current) {
1115
- path.unshift(current.data.name);
1116
- current = current.parent;
2453
+ getNodeStrokeColor(d) {
2454
+ if (d.data.loaded === 'loading' || d.data.analyzed === 'loading') {
2455
+ return '#FCD34D'; // Yellow for loading
1117
2456
  }
1118
-
1119
- const breadcrumbContent = document.getElementById('breadcrumb-content');
1120
- if (breadcrumbContent) {
1121
- breadcrumbContent.textContent = path.join(' > ');
1122
- breadcrumbContent.className = 'ticker-file';
2457
+ if (d.data.type === 'directory' && !d.data.loaded) {
2458
+ return '#94A3B8'; // Gray for unloaded
1123
2459
  }
1124
- }
1125
-
1126
- /**
1127
- * Update ticker with event
1128
- */
1129
- updateTicker(message, type = 'info') {
1130
- const breadcrumbContent = document.getElementById('breadcrumb-content');
1131
- if (breadcrumbContent) {
1132
- // Add class based on type
1133
- let className = '';
1134
- switch(type) {
1135
- case 'file': className = 'ticker-file'; break;
1136
- case 'node': className = 'ticker-node'; break;
1137
- case 'progress': className = 'ticker-progress'; break;
1138
- case 'error': className = 'ticker-error'; break;
1139
- default: className = '';
1140
- }
1141
-
1142
- breadcrumbContent.textContent = message;
1143
- breadcrumbContent.className = className + ' ticker-event';
1144
-
1145
- // Trigger animation
1146
- breadcrumbContent.style.animation = 'none';
1147
- setTimeout(() => {
1148
- breadcrumbContent.style.animation = '';
1149
- }, 10);
2460
+ if (d.data.type === 'file' && !d.data.analyzed) {
2461
+ return '#CBD5E1'; // Light gray for unanalyzed
1150
2462
  }
2463
+ return this.getNodeColor(d);
1151
2464
  }
1152
2465
 
1153
2466
  /**
1154
- * Show code viewer for a node
2467
+ * Get icon for node type
1155
2468
  */
1156
- showCodeViewer(nodeData) {
1157
- // Emit event to open code viewer
1158
- if (window.CodeViewer) {
1159
- window.CodeViewer.show(nodeData);
1160
- }
2469
+ getNodeIcon(d) {
2470
+ const icons = {
2471
+ 'root': '📦',
2472
+ 'directory': '📁',
2473
+ 'file': '📄',
2474
+ 'module': '📦',
2475
+ 'class': 'C',
2476
+ 'function': 'ƒ',
2477
+ 'method': 'm'
2478
+ };
2479
+ return icons[d.data.type] || '•';
1161
2480
  }
1162
2481
 
1163
2482
  /**
1164
- * Show tooltip
2483
+ * Show tooltip on hover
1165
2484
  */
1166
2485
  showTooltip(event, d) {
1167
2486
  if (!this.tooltip) return;
2487
+
2488
+ const info = [];
2489
+ info.push(`<strong>${d.data.name}</strong>`);
2490
+ info.push(`Type: ${d.data.type}`);
1168
2491
 
1169
- let content = `<strong>${d.data.name}</strong><br/>`;
1170
- content += `Type: ${d.data.type}<br/>`;
1171
-
2492
+ if (d.data.language) {
2493
+ info.push(`Language: ${d.data.language}`);
2494
+ }
1172
2495
  if (d.data.complexity) {
1173
- content += `Complexity: ${d.data.complexity}<br/>`;
2496
+ info.push(`Complexity: ${d.data.complexity}`);
1174
2497
  }
1175
-
1176
- if (d.data.line) {
1177
- content += `Line: ${d.data.line}<br/>`;
2498
+ if (d.data.lines) {
2499
+ info.push(`Lines: ${d.data.lines}`);
1178
2500
  }
1179
-
1180
- if (d.data.docstring) {
1181
- content += `<em>${d.data.docstring.substring(0, 100)}...</em>`;
2501
+ if (d.data.path) {
2502
+ info.push(`Path: ${d.data.path}`);
1182
2503
  }
1183
2504
 
2505
+ // Special messages for lazy-loaded nodes
2506
+ if (d.data.type === 'directory' && !d.data.loaded) {
2507
+ info.push('<em>Click to explore contents</em>');
2508
+ } else if (d.data.type === 'file' && !d.data.analyzed) {
2509
+ info.push('<em>Click to analyze file</em>');
2510
+ }
2511
+
1184
2512
  this.tooltip.transition()
1185
2513
  .duration(200)
1186
2514
  .style('opacity', .9);
1187
-
1188
- this.tooltip.html(content)
2515
+
2516
+ this.tooltip.html(info.join('<br>'))
1189
2517
  .style('left', (event.pageX + 10) + 'px')
1190
2518
  .style('top', (event.pageY - 28) + 'px');
1191
2519
  }
@@ -1194,101 +2522,214 @@ class CodeTree {
1194
2522
  * Hide tooltip
1195
2523
  */
1196
2524
  hideTooltip() {
1197
- if (this.tooltip) {
1198
- this.tooltip.transition()
1199
- .duration(500)
1200
- .style('opacity', 0);
1201
- }
2525
+ if (!this.tooltip) return;
2526
+
2527
+ this.tooltip.transition()
2528
+ .duration(500)
2529
+ .style('opacity', 0);
1202
2530
  }
1203
2531
 
1204
2532
  /**
1205
- * Get node color based on type
2533
+ * Filter tree based on language and search
1206
2534
  */
1207
- getNodeColor(type) {
1208
- const colors = {
1209
- module: '#8b5cf6',
1210
- file: '#6366f1',
1211
- class: '#3b82f6',
1212
- function: '#f59e0b',
1213
- method: '#10b981'
1214
- };
1215
- return colors[type] || '#718096';
1216
- }
2535
+ filterTree() {
2536
+ if (!this.root) return;
1217
2537
 
1218
- /**
1219
- * Get node icon based on type
1220
- */
1221
- getNodeIcon(type) {
1222
- const icons = {
1223
- module: '📦',
1224
- file: '📄',
1225
- class: '🏛️',
1226
- function: '⚡',
1227
- method: '🔧'
1228
- };
1229
- return icons[type] || '📌';
1230
- }
2538
+ // Apply filters
2539
+ this.root.descendants().forEach(d => {
2540
+ d.data._hidden = false;
1231
2541
 
1232
- /**
1233
- * Get complexity level
1234
- */
1235
- getComplexityLevel(complexity) {
1236
- if (complexity <= 5) return 'low';
1237
- if (complexity <= 10) return 'medium';
1238
- return 'high';
1239
- }
2542
+ // Language filter
2543
+ if (this.languageFilter !== 'all') {
2544
+ if (d.data.type === 'file' && d.data.language !== this.languageFilter) {
2545
+ d.data._hidden = true;
2546
+ }
2547
+ }
1240
2548
 
1241
- /**
1242
- * Expand all nodes
1243
- */
1244
- expandAll() {
1245
- this.expand(this.root);
2549
+ // Search filter
2550
+ if (this.searchTerm) {
2551
+ if (!d.data.name.toLowerCase().includes(this.searchTerm)) {
2552
+ d.data._hidden = true;
2553
+ }
2554
+ }
2555
+ });
2556
+
2557
+ // Update display
1246
2558
  this.update(this.root);
1247
2559
  }
1248
2560
 
1249
2561
  /**
1250
- * Expand node recursively
2562
+ * Expand all nodes in the tree
1251
2563
  */
1252
- expand(node) {
1253
- if (node._children) {
1254
- node.children = node._children;
1255
- node._children = null;
1256
- }
1257
- if (node.children) {
1258
- node.children.forEach(child => this.expand(child));
1259
- }
2564
+ expandAll() {
2565
+ if (!this.root) return;
2566
+
2567
+ // Recursively expand all nodes
2568
+ const expandRecursive = (node) => {
2569
+ if (node._children) {
2570
+ node.children = node._children;
2571
+ node._children = null;
2572
+ }
2573
+ if (node.children) {
2574
+ node.children.forEach(expandRecursive);
2575
+ }
2576
+ };
2577
+
2578
+ expandRecursive(this.root);
2579
+ this.update(this.root);
2580
+ this.showNotification('All nodes expanded', 'info');
1260
2581
  }
1261
2582
 
1262
2583
  /**
1263
- * Collapse all nodes
2584
+ * Collapse all nodes in the tree
1264
2585
  */
1265
2586
  collapseAll() {
1266
- this.collapse(this.root);
2587
+ if (!this.root) return;
2588
+
2589
+ // Recursively collapse all nodes except root
2590
+ const collapseRecursive = (node) => {
2591
+ if (node.children) {
2592
+ node._children = node.children;
2593
+ node.children = null;
2594
+ }
2595
+ if (node._children) {
2596
+ node._children.forEach(collapseRecursive);
2597
+ }
2598
+ };
2599
+
2600
+ this.root.children?.forEach(collapseRecursive);
1267
2601
  this.update(this.root);
2602
+ this.showNotification('All nodes collapsed', 'info');
1268
2603
  }
1269
2604
 
1270
2605
  /**
1271
- * Collapse node recursively
2606
+ * Reset zoom to fit the tree
1272
2607
  */
1273
- collapse(node) {
1274
- if (node.children) {
1275
- node._children = node.children;
1276
- node.children.forEach(child => this.collapse(child));
1277
- node.children = null;
1278
- }
2608
+ resetZoom() {
2609
+ if (!this.svg || !this.zoom) return;
2610
+
2611
+ // Reset to identity transform for radial layout (centered)
2612
+ this.svg.transition()
2613
+ .duration(750)
2614
+ .call(
2615
+ this.zoom.transform,
2616
+ d3.zoomIdentity
2617
+ );
2618
+
2619
+ this.showNotification('Zoom reset', 'info');
1279
2620
  }
1280
2621
 
1281
2622
  /**
1282
- * Reset zoom
2623
+ * Focus on a specific node and its subtree
1283
2624
  */
1284
- resetZoom() {
1285
- if (this.svg) {
2625
+ focusOnNode(node) {
2626
+ if (!this.svg || !this.zoom || !node) return;
2627
+
2628
+ // Get all descendants of this node
2629
+ const descendants = node.descendants ? node.descendants() : [node];
2630
+
2631
+ if (this.isRadialLayout) {
2632
+ // For radial layout, calculate the bounding box in polar coordinates
2633
+ const angles = descendants.map(d => d.x);
2634
+ const radii = descendants.map(d => d.y);
2635
+
2636
+ const minAngle = Math.min(...angles);
2637
+ const maxAngle = Math.max(...angles);
2638
+ const minRadius = Math.min(...radii);
2639
+ const maxRadius = Math.max(...radii);
2640
+
2641
+ // Convert polar bounds to Cartesian for centering
2642
+ const centerAngle = (minAngle + maxAngle) / 2;
2643
+ const centerRadius = (minRadius + maxRadius) / 2;
2644
+
2645
+ // Convert to Cartesian coordinates
2646
+ const centerX = centerRadius * Math.cos(centerAngle - Math.PI / 2);
2647
+ const centerY = centerRadius * Math.sin(centerAngle - Math.PI / 2);
2648
+
2649
+ // Calculate the span for zoom scale
2650
+ const angleSpan = maxAngle - minAngle;
2651
+ const radiusSpan = maxRadius - minRadius;
2652
+
2653
+ // Calculate scale to fit the subtree
2654
+ // Use angle span to determine scale (radial layout specific)
2655
+ let scale = 1;
2656
+ if (angleSpan > 0 && radiusSpan > 0) {
2657
+ // Scale based on the larger dimension
2658
+ const angleFactor = Math.PI * 2 / angleSpan; // Full circle / angle span
2659
+ const radiusFactor = this.radius / radiusSpan;
2660
+ scale = Math.min(angleFactor, radiusFactor, 3); // Max zoom of 3x
2661
+ scale = Math.max(scale, 1); // Min zoom of 1x
2662
+ }
2663
+
2664
+ // Animate the zoom and center
2665
+ this.svg.transition()
2666
+ .duration(750)
2667
+ .call(
2668
+ this.zoom.transform,
2669
+ d3.zoomIdentity
2670
+ .translate(this.width/2 - centerX * scale, this.height/2 - centerY * scale)
2671
+ .scale(scale)
2672
+ );
2673
+
2674
+ } else {
2675
+ // For linear/tree layout
2676
+ const xValues = descendants.map(d => d.x);
2677
+ const yValues = descendants.map(d => d.y);
2678
+
2679
+ const minX = Math.min(...xValues);
2680
+ const maxX = Math.max(...xValues);
2681
+ const minY = Math.min(...yValues);
2682
+ const maxY = Math.max(...yValues);
2683
+
2684
+ // Calculate center
2685
+ const centerX = (minX + maxX) / 2;
2686
+ const centerY = (minY + maxY) / 2;
2687
+
2688
+ // Calculate bounds
2689
+ const width = maxX - minX;
2690
+ const height = maxY - minY;
2691
+
2692
+ // Calculate scale to fit
2693
+ const padding = 100;
2694
+ let scale = 1;
2695
+ if (width > 0 && height > 0) {
2696
+ const scaleX = (this.width - padding) / width;
2697
+ const scaleY = (this.height - padding) / height;
2698
+ scale = Math.min(scaleX, scaleY, 2.5); // Max zoom of 2.5x
2699
+ scale = Math.max(scale, 0.5); // Min zoom of 0.5x
2700
+ }
2701
+
2702
+ // Animate zoom to focus
1286
2703
  this.svg.transition()
1287
2704
  .duration(750)
1288
- .call(d3.zoom().transform, d3.zoomIdentity);
2705
+ .call(
2706
+ this.zoom.transform,
2707
+ d3.zoomIdentity
2708
+ .translate(this.width/2 - centerX * scale, this.height/2 - centerY * scale)
2709
+ .scale(scale)
2710
+ );
1289
2711
  }
2712
+
2713
+ // Update breadcrumb with focused path
2714
+ const path = this.getNodePath(node);
2715
+ this.updateBreadcrumb(`Focused: ${path}`, 'info');
1290
2716
  }
1291
2717
 
2718
+ /**
2719
+ * Get the full path of a node
2720
+ */
2721
+ getNodePath(node) {
2722
+ const path = [];
2723
+ let current = node;
2724
+ while (current) {
2725
+ if (current.data && current.data.name) {
2726
+ path.unshift(current.data.name);
2727
+ }
2728
+ current = current.parent;
2729
+ }
2730
+ return path.join(' / ');
2731
+ }
2732
+
1292
2733
  /**
1293
2734
  * Toggle legend visibility
1294
2735
  */
@@ -1304,54 +2745,199 @@ class CodeTree {
1304
2745
  }
1305
2746
 
1306
2747
  /**
1307
- * Filter nodes by language
2748
+ * Get the current working directory
1308
2749
  */
1309
- filterByLanguage() {
1310
- // Implementation for language filtering
1311
- console.log('Filtering by language:', this.languageFilter);
1312
- // This would filter the tree data and update visualization
1313
- this.update(this.root);
2750
+ getWorkingDirectory() {
2751
+ // Try to get from dashboard's working directory manager
2752
+ if (window.dashboard && window.dashboard.workingDirectoryManager) {
2753
+ return window.dashboard.workingDirectoryManager.getCurrentWorkingDir();
2754
+ }
2755
+
2756
+ // Fallback to checking the DOM element
2757
+ const workingDirPath = document.getElementById('working-dir-path');
2758
+ if (workingDirPath) {
2759
+ const pathText = workingDirPath.textContent.trim();
2760
+ if (pathText && pathText !== 'Loading...' && pathText !== 'Not selected') {
2761
+ return pathText;
2762
+ }
2763
+ }
2764
+
2765
+ return null;
1314
2766
  }
1315
-
2767
+
1316
2768
  /**
1317
- * Highlight search results
2769
+ * Show a message when no working directory is selected
1318
2770
  */
1319
- highlightSearchResults() {
1320
- if (!this.treeGroup) return;
1321
-
1322
- // Clear previous highlights
1323
- this.treeGroup.selectAll('.code-node').classed('highlighted', false);
2771
+ showNoWorkingDirectoryMessage() {
2772
+ const container = document.getElementById('code-tree-container');
2773
+ if (!container) return;
2774
+
2775
+ // Remove any existing message
2776
+ this.removeNoWorkingDirectoryMessage();
2777
+
2778
+ // Hide loading if shown
2779
+ this.hideLoading();
2780
+
2781
+ // Create message element
2782
+ const messageDiv = document.createElement('div');
2783
+ messageDiv.id = 'no-working-dir-message';
2784
+ messageDiv.className = 'no-working-dir-message';
2785
+ messageDiv.innerHTML = `
2786
+ <div class="message-icon">📁</div>
2787
+ <h3>No Working Directory Selected</h3>
2788
+ <p>Please select a working directory from the top menu to analyze code.</p>
2789
+ <button id="select-working-dir-btn" class="btn btn-primary">
2790
+ Select Working Directory
2791
+ </button>
2792
+ `;
2793
+ messageDiv.style.cssText = `
2794
+ text-align: center;
2795
+ padding: 40px;
2796
+ color: #666;
2797
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2798
+ `;
2799
+
2800
+ // Style the message elements
2801
+ const messageIcon = messageDiv.querySelector('.message-icon');
2802
+ if (messageIcon) {
2803
+ messageIcon.style.cssText = 'font-size: 48px; margin-bottom: 16px; opacity: 0.5;';
2804
+ }
2805
+
2806
+ const h3 = messageDiv.querySelector('h3');
2807
+ if (h3) {
2808
+ h3.style.cssText = 'margin: 16px 0; color: #333; font-size: 20px;';
2809
+ }
2810
+
2811
+ const p = messageDiv.querySelector('p');
2812
+ if (p) {
2813
+ p.style.cssText = 'margin: 16px 0; color: #666; font-size: 14px;';
2814
+ }
2815
+
2816
+ const button = messageDiv.querySelector('button');
2817
+ if (button) {
2818
+ button.style.cssText = `
2819
+ margin-top: 20px;
2820
+ padding: 10px 20px;
2821
+ background: #3b82f6;
2822
+ color: white;
2823
+ border: none;
2824
+ border-radius: 6px;
2825
+ cursor: pointer;
2826
+ font-size: 14px;
2827
+ transition: background 0.2s;
2828
+ `;
2829
+ button.addEventListener('mouseenter', () => {
2830
+ button.style.background = '#2563eb';
2831
+ });
2832
+ button.addEventListener('mouseleave', () => {
2833
+ button.style.background = '#3b82f6';
2834
+ });
2835
+ button.addEventListener('click', () => {
2836
+ // Trigger working directory selection
2837
+ const changeDirBtn = document.getElementById('change-dir-btn');
2838
+ if (changeDirBtn) {
2839
+ changeDirBtn.click();
2840
+ } else if (window.dashboard && window.dashboard.workingDirectoryManager) {
2841
+ window.dashboard.workingDirectoryManager.showChangeDirDialog();
2842
+ }
2843
+ });
2844
+ }
1324
2845
 
1325
- if (!this.searchTerm) return;
2846
+ container.appendChild(messageDiv);
1326
2847
 
1327
- // Highlight matching nodes
1328
- this.treeGroup.selectAll('.code-node').each((d, i, nodes) => {
1329
- if (d.data.name.toLowerCase().includes(this.searchTerm)) {
1330
- d3.select(nodes[i]).classed('highlighted', true);
1331
- }
1332
- });
2848
+ // Update breadcrumb
2849
+ this.updateBreadcrumb('Please select a working directory', 'warning');
2850
+ }
2851
+
2852
+ /**
2853
+ * Remove the no working directory message
2854
+ */
2855
+ removeNoWorkingDirectoryMessage() {
2856
+ const message = document.getElementById('no-working-dir-message');
2857
+ if (message) {
2858
+ message.remove();
2859
+ }
2860
+ }
2861
+
2862
+ /**
2863
+ * Export tree data
2864
+ */
2865
+ exportTree() {
2866
+ const exportData = {
2867
+ timestamp: new Date().toISOString(),
2868
+ workingDirectory: this.getWorkingDirectory(),
2869
+ stats: this.stats,
2870
+ tree: this.treeData
2871
+ };
2872
+
2873
+ const blob = new Blob([JSON.stringify(exportData, null, 2)],
2874
+ {type: 'application/json'});
2875
+ const url = URL.createObjectURL(blob);
2876
+ const link = document.createElement('a');
2877
+ link.href = url;
2878
+ link.download = `code-tree-${Date.now()}.json`;
2879
+ link.click();
2880
+ URL.revokeObjectURL(url);
2881
+
2882
+ this.showNotification('Tree exported successfully', 'success');
1333
2883
  }
1334
- }
1335
2884
 
1336
- // Export for use in dashboard
1337
- if (typeof window !== 'undefined') {
1338
- window.CodeTree = CodeTree;
2885
+ /**
2886
+ * Update activity ticker with real-time messages
2887
+ */
2888
+ updateActivityTicker(message, type = 'info') {
2889
+ const breadcrumb = document.getElementById('breadcrumb-content');
2890
+ if (breadcrumb) {
2891
+ // Add spinning icon for loading states
2892
+ const icon = type === 'info' && message.includes('...') ? '⟳ ' : '';
2893
+ breadcrumb.innerHTML = `${icon}${message}`;
2894
+ breadcrumb.className = `breadcrumb-${type}`;
2895
+ }
2896
+ }
1339
2897
 
1340
- // Initialize when DOM is ready
1341
- document.addEventListener('DOMContentLoaded', () => {
1342
- const codeTree = new CodeTree();
1343
- window.codeTree = codeTree;
1344
-
1345
- // Listen for tab switches to initialize when Code tab is activated
1346
- document.querySelectorAll('.tab-button').forEach(button => {
1347
- button.addEventListener('click', () => {
1348
- if (button.getAttribute('data-tab') === 'code') {
1349
- console.log('Code tab activated, initializing tree...');
1350
- codeTree.renderWhenVisible();
1351
- }
1352
- });
1353
- });
1354
- });
2898
+ /**
2899
+ * Update ticker message
2900
+ */
2901
+ updateTicker(message, type = 'info') {
2902
+ const ticker = document.getElementById('code-tree-ticker');
2903
+ if (ticker) {
2904
+ ticker.textContent = message;
2905
+ ticker.className = `ticker ticker-${type}`;
2906
+
2907
+ // Auto-hide after 5 seconds for non-error messages
2908
+ if (type !== 'error') {
2909
+ setTimeout(() => {
2910
+ ticker.style.opacity = '0';
2911
+ setTimeout(() => {
2912
+ ticker.style.opacity = '1';
2913
+ ticker.textContent = '';
2914
+ }, 300);
2915
+ }, 5000);
2916
+ }
2917
+ }
2918
+ }
1355
2919
  }
1356
2920
 
1357
- export default CodeTree;
2921
+ // Export for use in other modules
2922
+ window.CodeTree = CodeTree;
2923
+
2924
+ // Auto-initialize when DOM is ready
2925
+ document.addEventListener('DOMContentLoaded', () => {
2926
+ // Check if we're on a page with code tree container
2927
+ if (document.getElementById('code-tree-container')) {
2928
+ window.codeTree = new CodeTree();
2929
+
2930
+ // Listen for tab changes to initialize when code tab is selected
2931
+ document.addEventListener('click', (e) => {
2932
+ if (e.target.matches('[data-tab="code"]')) {
2933
+ setTimeout(() => {
2934
+ if (window.codeTree && !window.codeTree.initialized) {
2935
+ window.codeTree.initialize();
2936
+ } else if (window.codeTree) {
2937
+ window.codeTree.renderWhenVisible();
2938
+ }
2939
+ }, 100);
2940
+ }
2941
+ });
2942
+ }
2943
+ });/* Cache buster: 1756393851 */