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