claude-mpm 4.2.2__py3-none-any.whl → 4.2.4__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.
@@ -32,7 +32,7 @@ class CodeTree {
32
32
  lines: 0
33
33
  };
34
34
  // Radial layout settings
35
- this.isRadialLayout = true; // Toggle for radial vs linear layout
35
+ this.isRadialLayout = false; // Toggle for radial vs linear layout - defaulting to linear for better readability
36
36
  this.margin = {top: 20, right: 20, bottom: 20, left: 20};
37
37
  this.width = 960 - this.margin.left - this.margin.right;
38
38
  this.height = 600 - this.margin.top - this.margin.bottom;
@@ -48,10 +48,17 @@ class CodeTree {
48
48
  this.socket = null;
49
49
  this.autoDiscovered = false; // Track if auto-discovery has been done
50
50
  this.zoom = null; // Store zoom behavior
51
+
52
+ // Structured data properties
53
+ this.structuredDataContent = null;
54
+ this.selectedASTItem = null;
51
55
  this.activeNode = null; // Track currently active node
52
56
  this.loadingNodes = new Set(); // Track nodes that are loading
53
57
  this.bulkLoadMode = false; // Track bulk loading preference
54
58
  this.expandedPaths = new Set(); // Track which paths are expanded
59
+ this.focusedNode = null; // Track the currently focused directory
60
+ this.horizontalNodes = new Set(); // Track nodes that should have horizontal text
61
+ this.centralSpine = new Set(); // Track the main path through the tree
55
62
  }
56
63
 
57
64
  /**
@@ -87,6 +94,7 @@ class CodeTree {
87
94
  this.setupControls();
88
95
  this.initializeTreeData();
89
96
  this.subscribeToEvents();
97
+ this.initializeStructuredData();
90
98
 
91
99
  // Set initial status message
92
100
  const breadcrumbContent = document.getElementById('breadcrumb-content');
@@ -166,20 +174,8 @@ class CodeTree {
166
174
  });
167
175
  }
168
176
 
169
- const expandBtn = document.getElementById('code-expand-all');
170
- if (expandBtn) {
171
- expandBtn.addEventListener('click', () => this.expandAll());
172
- }
173
-
174
- const collapseBtn = document.getElementById('code-collapse-all');
175
- if (collapseBtn) {
176
- collapseBtn.addEventListener('click', () => this.collapseAll());
177
- }
178
-
179
- const resetZoomBtn = document.getElementById('code-reset-zoom');
180
- if (resetZoomBtn) {
181
- resetZoomBtn.addEventListener('click', () => this.resetZoom());
182
- }
177
+ // Note: Expand/collapse/reset buttons are now handled by the tree controls toolbar
178
+ // which is created dynamically in addTreeControls()
183
179
 
184
180
  const toggleLegendBtn = document.getElementById('code-toggle-legend');
185
181
  if (toggleLegendBtn) {
@@ -372,14 +368,27 @@ class CodeTree {
372
368
  });
373
369
  }
374
370
 
375
- // DISABLED: All zoom behavior has been completely disabled to prevent tree movement
376
- // The tree should remain completely stationary - no zooming, panning, or centering allowed
377
- this.zoom = null; // Completely disable zoom behavior
378
-
379
- // Do NOT apply zoom behavior to SVG - this prevents all zoom/pan interactions
380
- // this.svg.call(this.zoom); // DISABLED
381
-
382
- console.log('[CodeTree] All zoom and pan behavior disabled - tree is now completely stationary');
371
+ // Enable zoom and pan functionality for better navigation
372
+ this.zoom = d3.zoom()
373
+ .scaleExtent([0.1, 3]) // Allow zoom from 10% to 300%
374
+ .on('zoom', (event) => {
375
+ // Apply zoom transform to the tree group
376
+ this.treeGroup.attr('transform', event.transform);
377
+
378
+ // Keep text size constant by applying inverse scaling
379
+ this.adjustTextSizeForZoom(event.transform.k);
380
+
381
+ // Update zoom level display
382
+ this.updateZoomLevel(event.transform.k);
383
+ });
384
+
385
+ // Apply zoom behavior to SVG
386
+ this.svg.call(this.zoom);
387
+
388
+ // Add keyboard shortcuts for zoom
389
+ this.addZoomKeyboardShortcuts();
390
+
391
+ console.log('[CodeTree] Zoom and pan functionality enabled');
383
392
 
384
393
  // Add controls overlay
385
394
  this.addVisualizationControls();
@@ -416,12 +425,11 @@ class CodeTree {
416
425
  initializeTreeData() {
417
426
  const workingDir = this.getWorkingDirectory();
418
427
  const dirName = workingDir ? workingDir.split('/').pop() || 'Project Root' : 'Project Root';
419
-
420
- // Use '.' as the root path for consistency with relative path handling
421
- // The actual working directory is retrieved via getWorkingDirectory() when needed
428
+
429
+ // Use absolute path for consistency with API expectations
422
430
  this.treeData = {
423
431
  name: dirName,
424
- path: '.', // Always use '.' for root to simplify path handling
432
+ path: workingDir || '.', // Use working directory or fallback to '.'
425
433
  type: 'root',
426
434
  children: [],
427
435
  loaded: false,
@@ -531,7 +539,7 @@ class CodeTree {
531
539
  const dirName = workingDir.split('/').pop() || 'Project Root';
532
540
  this.treeData = {
533
541
  name: dirName,
534
- path: '.', // Use '.' for root to maintain consistency with relative path handling
542
+ path: workingDir, // Use absolute path for consistency with API expectations
535
543
  type: 'root',
536
544
  children: [],
537
545
  loaded: false,
@@ -549,10 +557,7 @@ class CodeTree {
549
557
  this.updateBreadcrumb(`Discovering structure in ${dirName}...`, 'info');
550
558
 
551
559
  // Get selected languages from checkboxes
552
- const selectedLanguages = [];
553
- document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
554
- selectedLanguages.push(cb.value);
555
- });
560
+ const selectedLanguages = this.getSelectedLanguages();
556
561
 
557
562
  // Get ignore patterns
558
563
  const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
@@ -643,7 +648,37 @@ class CodeTree {
643
648
  .attr('title', 'Toggle between radial and linear layouts')
644
649
  .text('◎')
645
650
  .on('click', () => this.toggleLayout());
646
-
651
+
652
+ // Zoom In
653
+ toolbar.append('button')
654
+ .attr('class', 'tree-control-btn')
655
+ .attr('title', 'Zoom in')
656
+ .text('🔍+')
657
+ .on('click', () => this.zoomIn());
658
+
659
+ // Zoom Out
660
+ toolbar.append('button')
661
+ .attr('class', 'tree-control-btn')
662
+ .attr('title', 'Zoom out')
663
+ .text('🔍-')
664
+ .on('click', () => this.zoomOut());
665
+
666
+ // Reset Zoom
667
+ toolbar.append('button')
668
+ .attr('class', 'tree-control-btn')
669
+ .attr('title', 'Reset zoom to fit tree')
670
+ .text('⌂')
671
+ .on('click', () => this.resetZoom());
672
+
673
+ // Zoom Level Display
674
+ toolbar.append('span')
675
+ .attr('class', 'zoom-level-display')
676
+ .attr('id', 'zoom-level-display')
677
+ .text('100%')
678
+ .style('margin-left', '8px')
679
+ .style('font-size', '11px')
680
+ .style('color', '#718096');
681
+
647
682
  // Path Search
648
683
  const searchInput = toolbar.append('input')
649
684
  .attr('class', 'tree-control-btn')
@@ -948,11 +983,44 @@ class CodeTree {
948
983
  this.socket.on('code:top_level:discovered', (data) => this.onTopLevelDiscovered(data));
949
984
  this.socket.on('code:directory:discovered', (data) => this.onDirectoryDiscovered(data));
950
985
  this.socket.on('code:file:discovered', (data) => this.onFileDiscovered(data));
951
- this.socket.on('code:file:analyzed', (data) => this.onFileAnalyzed(data));
986
+ this.socket.on('code:file:analyzed', (data) => {
987
+ console.log('📨 [SOCKET] Received code:file:analyzed event');
988
+ this.onFileAnalyzed(data);
989
+ });
952
990
  this.socket.on('code:node:found', (data) => this.onNodeFound(data));
953
991
 
954
992
  // Progress updates
955
993
  this.socket.on('code:analysis:progress', (data) => this.onProgressUpdate(data));
994
+
995
+ // Error handling
996
+ this.socket.on('code:analysis:error', (data) => {
997
+ console.error('❌ [FILE ANALYSIS] Analysis error:', data);
998
+ this.showNotification(`Analysis error: ${data.error || 'Unknown error'}`, 'error');
999
+ });
1000
+
1001
+ // Generic error handling
1002
+ this.socket.on('error', (error) => {
1003
+ console.error('❌ [SOCKET] Socket error:', error);
1004
+ });
1005
+
1006
+ // Socket connection status
1007
+ this.socket.on('connect', () => {
1008
+ console.log('✅ [SOCKET] Connected to server, analysis service should be available');
1009
+ this.connectionStable = true;
1010
+ });
1011
+
1012
+ this.socket.on('disconnect', () => {
1013
+ console.log('❌ [SOCKET] Disconnected from server - disabling AST analysis');
1014
+ this.connectionStable = false;
1015
+ // Clear any pending analysis timeouts
1016
+ if (this.analysisTimeouts) {
1017
+ this.analysisTimeouts.forEach((timeout, path) => {
1018
+ clearTimeout(timeout);
1019
+ this.loadingNodes.delete(path);
1020
+ });
1021
+ this.analysisTimeouts.clear();
1022
+ }
1023
+ });
956
1024
 
957
1025
  // Lazy loading responses
958
1026
  this.socket.on('code:directory:contents', (data) => {
@@ -1136,10 +1204,11 @@ class CodeTree {
1136
1204
  // Add to events display
1137
1205
  this.addEventToDisplay(`📁 Found ${(data.items || []).length} top-level items in project root`, 'info');
1138
1206
 
1139
- // The root node (with path '.') should receive the children
1140
- const rootNode = this.findNodeByPath('.');
1141
-
1142
- console.log('🔎 Looking for root node with path ".", found:', rootNode ? {
1207
+ // The root node should receive the children
1208
+ const workingDir = this.getWorkingDirectory();
1209
+ const rootNode = this.findNodeByPath(workingDir);
1210
+
1211
+ console.log(`🔎 Looking for root node with path "${workingDir}", found:`, rootNode ? {
1143
1212
  name: rootNode.name,
1144
1213
  path: rootNode.path,
1145
1214
  currentChildren: rootNode.children ? rootNode.children.length : 0
@@ -1150,14 +1219,16 @@ class CodeTree {
1150
1219
 
1151
1220
  // Update the root node with discovered children
1152
1221
  rootNode.children = data.items.map(child => {
1153
- // Items at root level get their name as the path
1154
- const childPath = child.name;
1155
-
1222
+ // CRITICAL FIX: Use consistent path format that matches API expectations
1223
+ // The API expects absolute paths, so construct them properly
1224
+ const workingDir = this.getWorkingDirectory();
1225
+ const childPath = workingDir ? `${workingDir}/${child.name}`.replace(/\/+/g, '/') : child.name;
1226
+
1156
1227
  console.log(` Adding child: ${child.name} with path: ${childPath}`);
1157
-
1228
+
1158
1229
  return {
1159
1230
  name: child.name,
1160
- path: childPath, // Just the name for top-level items
1231
+ path: childPath, // Use absolute path for consistency
1161
1232
  type: child.type,
1162
1233
  loaded: child.type === 'directory' ? false : undefined, // Explicitly false for directories
1163
1234
  analyzed: child.type === 'file' ? false : undefined,
@@ -1470,6 +1541,45 @@ class CodeTree {
1470
1541
  * Handle file analyzed event
1471
1542
  */
1472
1543
  onFileAnalyzed(data) {
1544
+ console.log('✅ [FILE ANALYSIS] Received analysis result:', {
1545
+ path: data.path,
1546
+ elements: data.elements ? data.elements.length : 0,
1547
+ complexity: data.complexity,
1548
+ lines: data.lines,
1549
+ stats: data.stats,
1550
+ elementsDetail: data.elements,
1551
+ fullData: data
1552
+ });
1553
+
1554
+ // Debug: Show elements in detail
1555
+ if (data.elements && data.elements.length > 0) {
1556
+ console.log('🔍 [AST ELEMENTS] Found elements:', data.elements.map(elem => ({
1557
+ name: elem.name,
1558
+ type: elem.type,
1559
+ line: elem.line,
1560
+ methods: elem.methods ? elem.methods.length : 0
1561
+ })));
1562
+ } else {
1563
+ const fileName = data.path.split('/').pop();
1564
+ console.log('⚠️ [AST ELEMENTS] No elements found in analysis result');
1565
+
1566
+ // Show user-friendly message for files with no AST elements
1567
+ if (fileName.endsWith('__init__.py')) {
1568
+ this.showNotification(`${fileName} is empty or contains only imports`, 'info');
1569
+ this.updateBreadcrumb(`${fileName} - no code elements to display`, 'info');
1570
+ } else {
1571
+ this.showNotification(`${fileName} contains no classes or functions`, 'info');
1572
+ this.updateBreadcrumb(`${fileName} - no AST elements found`, 'info');
1573
+ }
1574
+ }
1575
+
1576
+ // Clear analysis timeout
1577
+ if (this.analysisTimeouts && this.analysisTimeouts.has(data.path)) {
1578
+ clearTimeout(this.analysisTimeouts.get(data.path));
1579
+ this.analysisTimeouts.delete(data.path);
1580
+ console.log('⏰ [FILE ANALYSIS] Cleared timeout for:', data.path);
1581
+ }
1582
+
1473
1583
  // Remove loading pulse if this file was being analyzed
1474
1584
  const d3Node = this.findD3NodeByPath(data.path);
1475
1585
  if (d3Node && this.loadingNodes.has(data.path)) {
@@ -1484,13 +1594,14 @@ class CodeTree {
1484
1594
 
1485
1595
  const fileNode = this.findNodeByPath(data.path);
1486
1596
  if (fileNode) {
1597
+ console.log('🔍 [FILE NODE] Found file node for:', data.path);
1487
1598
  fileNode.analyzed = true;
1488
1599
  fileNode.complexity = data.complexity || 0;
1489
1600
  fileNode.lines = data.lines || 0;
1490
-
1601
+
1491
1602
  // Add code elements as children
1492
1603
  if (data.elements && Array.isArray(data.elements)) {
1493
- fileNode.children = data.elements.map(elem => ({
1604
+ const children = data.elements.map(elem => ({
1494
1605
  name: elem.name,
1495
1606
  type: elem.type.toLowerCase(),
1496
1607
  path: `${data.path}#${elem.name}`,
@@ -1506,8 +1617,17 @@ class CodeTree {
1506
1617
  docstring: m.docstring || ''
1507
1618
  })) : []
1508
1619
  }));
1620
+
1621
+ fileNode.children = children;
1622
+ console.log('✅ [FILE NODE] Added children to file node:', {
1623
+ filePath: data.path,
1624
+ childrenCount: children.length,
1625
+ children: children.map(c => ({ name: c.name, type: c.type }))
1626
+ });
1627
+ } else {
1628
+ console.log('⚠️ [FILE NODE] No elements to add as children');
1509
1629
  }
1510
-
1630
+
1511
1631
  // Update stats
1512
1632
  if (data.stats) {
1513
1633
  this.stats.classes += data.stats.classes || 0;
@@ -1515,13 +1635,48 @@ class CodeTree {
1515
1635
  this.stats.methods += data.stats.methods || 0;
1516
1636
  this.stats.lines += data.stats.lines || 0;
1517
1637
  }
1518
-
1638
+
1519
1639
  this.updateStats();
1520
- if (this.root) {
1640
+
1641
+ // CRITICAL FIX: Recreate D3 hierarchy to include new children
1642
+ if (this.root && fileNode.children && fileNode.children.length > 0) {
1643
+ console.log('🔄 [FILE NODE] Recreating D3 hierarchy to include AST children');
1644
+
1645
+ // Store the old root for expansion state preservation
1646
+ const oldRoot = this.root;
1647
+
1648
+ // Recreate the D3 hierarchy with updated data
1649
+ this.root = d3.hierarchy(this.treeData);
1650
+ this.root.x0 = this.height / 2;
1651
+ this.root.y0 = 0;
1652
+
1653
+ // Preserve expansion state from old tree
1654
+ this.preserveExpansionState(oldRoot, this.root);
1655
+
1656
+ // Find the updated file node in the new hierarchy
1657
+ const updatedFileNode = this.findD3NodeByPath(data.path);
1658
+ if (updatedFileNode) {
1659
+ // Ensure the file node is expanded to show its AST children
1660
+ if (updatedFileNode.children && updatedFileNode.children.length > 0) {
1661
+ updatedFileNode._children = null;
1662
+ updatedFileNode.data.expanded = true;
1663
+ console.log('✅ [FILE NODE] File node expanded to show AST children:', {
1664
+ path: data.path,
1665
+ childrenCount: updatedFileNode.children.length,
1666
+ childNames: updatedFileNode.children.map(c => c.data.name)
1667
+ });
1668
+ }
1669
+ }
1670
+
1671
+ // Update the visualization with the new hierarchy
1672
+ this.update(this.root);
1673
+ } else if (this.root) {
1521
1674
  this.update(this.root);
1522
1675
  }
1523
-
1676
+
1524
1677
  this.updateBreadcrumb(`Analyzed: ${data.path}`, 'success');
1678
+ } else {
1679
+ console.error('❌ [FILE NODE] Could not find file node for path:', data.path);
1525
1680
  }
1526
1681
  }
1527
1682
 
@@ -1965,12 +2120,12 @@ class CodeTree {
1965
2120
  * Update statistics display
1966
2121
  */
1967
2122
  updateStats() {
1968
- // Update stats display - use correct IDs from HTML
2123
+ // Update stats display - use correct IDs from corner controls
1969
2124
  const statsElements = {
1970
- 'file-count': this.stats.files,
1971
- 'class-count': this.stats.classes,
1972
- 'function-count': this.stats.functions,
1973
- 'line-count': this.stats.lines
2125
+ 'stats-files': this.stats.files,
2126
+ 'stats-classes': this.stats.classes,
2127
+ 'stats-functions': this.stats.functions,
2128
+ 'stats-methods': this.stats.methods
1974
2129
  };
1975
2130
 
1976
2131
  for (const [id, value] of Object.entries(statsElements)) {
@@ -2001,6 +2156,145 @@ class CodeTree {
2001
2156
  }
2002
2157
  }
2003
2158
 
2159
+ /**
2160
+ * Analyze file using HTTP fallback when SocketIO fails
2161
+ */
2162
+ async analyzeFileHTTP(filePath, fileName, d3Node) {
2163
+ console.log('🌐 [HTTP FALLBACK] Analyzing file via HTTP:', filePath);
2164
+ console.log('🌐 [HTTP FALLBACK] File name:', fileName);
2165
+
2166
+ try {
2167
+ // For now, create mock AST data since we don't have an HTTP endpoint yet
2168
+ // This demonstrates the structure and can be replaced with real HTTP call
2169
+ const mockAnalysisResult = this.createMockAnalysisData(filePath, fileName);
2170
+ console.log('🌐 [HTTP FALLBACK] Created mock data:', mockAnalysisResult);
2171
+
2172
+ // Simulate network delay
2173
+ setTimeout(() => {
2174
+ console.log('✅ [HTTP FALLBACK] Mock analysis complete for:', fileName);
2175
+ console.log('✅ [HTTP FALLBACK] Calling onFileAnalyzed with:', mockAnalysisResult);
2176
+ this.onFileAnalyzed(mockAnalysisResult);
2177
+ }, 1000);
2178
+
2179
+ } catch (error) {
2180
+ console.error('❌ [HTTP FALLBACK] Analysis failed:', error);
2181
+ this.showNotification(`Analysis failed: ${error.message}`, 'error');
2182
+ this.loadingNodes.delete(filePath);
2183
+ this.removeLoadingPulse(d3Node);
2184
+ }
2185
+ }
2186
+
2187
+ /**
2188
+ * Create mock analysis data for demonstration
2189
+ */
2190
+ createMockAnalysisData(filePath, fileName) {
2191
+ const ext = fileName.split('.').pop()?.toLowerCase();
2192
+ console.log('🔍 [MOCK DATA] Creating mock data for file:', fileName, 'extension:', ext);
2193
+
2194
+ // Create realistic mock data based on file type
2195
+ let elements = [];
2196
+
2197
+ if (ext === 'py') {
2198
+ elements = [
2199
+ {
2200
+ name: 'ExampleClass',
2201
+ type: 'class',
2202
+ line: 10,
2203
+ complexity: 3,
2204
+ docstring: 'Example class for demonstration',
2205
+ methods: [
2206
+ { name: '__init__', type: 'method', line: 12, complexity: 1 },
2207
+ { name: 'example_method', type: 'method', line: 18, complexity: 2 }
2208
+ ]
2209
+ },
2210
+ {
2211
+ name: 'example_function',
2212
+ type: 'function',
2213
+ line: 25,
2214
+ complexity: 2,
2215
+ docstring: 'Example function'
2216
+ }
2217
+ ];
2218
+ } else if (ext === 'js' || ext === 'ts') {
2219
+ elements = [
2220
+ {
2221
+ name: 'ExampleClass',
2222
+ type: 'class',
2223
+ line: 5,
2224
+ complexity: 2,
2225
+ methods: [
2226
+ { name: 'constructor', type: 'method', line: 6, complexity: 1 },
2227
+ { name: 'exampleMethod', type: 'method', line: 10, complexity: 2 }
2228
+ ]
2229
+ },
2230
+ {
2231
+ name: 'exampleFunction',
2232
+ type: 'function',
2233
+ line: 20,
2234
+ complexity: 1
2235
+ }
2236
+ ];
2237
+ } else {
2238
+ // For other file types, create at least one element to show it's working
2239
+ elements = [
2240
+ {
2241
+ name: 'mock_element',
2242
+ type: 'function',
2243
+ line: 1,
2244
+ complexity: 1,
2245
+ docstring: `Mock element for ${fileName}`
2246
+ }
2247
+ ];
2248
+ }
2249
+
2250
+ console.log('🔍 [MOCK DATA] Created elements:', elements);
2251
+
2252
+ return {
2253
+ path: filePath,
2254
+ elements: elements,
2255
+ complexity: elements.reduce((sum, elem) => sum + (elem.complexity || 1), 0),
2256
+ lines: 50,
2257
+ stats: {
2258
+ classes: elements.filter(e => e.type === 'class').length,
2259
+ functions: elements.filter(e => e.type === 'function').length,
2260
+ methods: elements.reduce((sum, e) => sum + (e.methods ? e.methods.length : 0), 0),
2261
+ lines: 50
2262
+ }
2263
+ };
2264
+ }
2265
+
2266
+ /**
2267
+ * Get selected languages from checkboxes with fallback
2268
+ */
2269
+ getSelectedLanguages() {
2270
+ const selectedLanguages = [];
2271
+ const checkboxes = document.querySelectorAll('.language-checkbox:checked');
2272
+
2273
+ console.log('🔍 [LANGUAGE] Found checkboxes:', checkboxes.length);
2274
+ console.log('🔍 [LANGUAGE] All language checkboxes:', document.querySelectorAll('.language-checkbox').length);
2275
+
2276
+ checkboxes.forEach(cb => {
2277
+ console.log('🔍 [LANGUAGE] Checked language:', cb.value);
2278
+ selectedLanguages.push(cb.value);
2279
+ });
2280
+
2281
+ // Fallback: if no languages are selected, default to common ones
2282
+ if (selectedLanguages.length === 0) {
2283
+ console.warn('⚠️ [LANGUAGE] No languages selected, using defaults');
2284
+ selectedLanguages.push('python', 'javascript', 'typescript');
2285
+
2286
+ // Also check the checkboxes programmatically
2287
+ document.querySelectorAll('.language-checkbox').forEach(cb => {
2288
+ if (['python', 'javascript', 'typescript'].includes(cb.value)) {
2289
+ cb.checked = true;
2290
+ console.log('✅ [LANGUAGE] Auto-checked:', cb.value);
2291
+ }
2292
+ });
2293
+ }
2294
+
2295
+ return selectedLanguages;
2296
+ }
2297
+
2004
2298
  /**
2005
2299
  * Detect language from file extension
2006
2300
  */
@@ -2082,6 +2376,172 @@ class CodeTree {
2082
2376
  return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
2083
2377
  }
2084
2378
 
2379
+ /**
2380
+ * Apply horizontal text to the central spine of the tree
2381
+ */
2382
+ applySingletonHorizontalLayout(nodes) {
2383
+ if (this.isRadialLayout) return; // Only apply to linear layout
2384
+
2385
+ // Clear previous horizontal nodes tracking
2386
+ this.horizontalNodes.clear();
2387
+ this.centralSpine.clear();
2388
+
2389
+ // Find the central spine - the main path through the tree
2390
+ this.identifyCentralSpine(nodes);
2391
+
2392
+ // Mark all central spine nodes for horizontal text
2393
+ this.centralSpine.forEach(path => {
2394
+ this.horizontalNodes.add(path);
2395
+ });
2396
+
2397
+ console.log(`🎯 [SPINE] Central spine nodes:`, Array.from(this.centralSpine));
2398
+ console.log(`📝 [TEXT] Horizontal text nodes:`, Array.from(this.horizontalNodes));
2399
+ }
2400
+
2401
+ /**
2402
+ * Identify the central spine of the tree (main path from root to deepest/most important nodes)
2403
+ */
2404
+ identifyCentralSpine(nodes) {
2405
+ if (!nodes || nodes.length === 0) return;
2406
+
2407
+ // Start with the root node
2408
+ const rootNode = nodes.find(node => node.depth === 0);
2409
+ if (!rootNode) {
2410
+ console.warn('🎯 [SPINE] No root node found!');
2411
+ return;
2412
+ }
2413
+
2414
+ this.centralSpine.add(rootNode.data.path);
2415
+ console.log(`🎯 [SPINE] Starting spine with root: ${rootNode.data.name} (${rootNode.data.path})`);
2416
+
2417
+ // Follow the main path through the tree
2418
+ let currentNode = rootNode;
2419
+ while (currentNode && currentNode.children && currentNode.children.length > 0) {
2420
+ // Choose the "main" child - prioritize directories, then by name
2421
+ const mainChild = this.selectMainChild(currentNode.children);
2422
+ if (mainChild) {
2423
+ this.centralSpine.add(mainChild.data.path);
2424
+ console.log(`🎯 [SPINE] Adding to spine: ${mainChild.data.name}`);
2425
+ currentNode = mainChild;
2426
+ } else {
2427
+ break;
2428
+ }
2429
+ }
2430
+ }
2431
+
2432
+ /**
2433
+ * Select the main child to continue the central spine
2434
+ */
2435
+ selectMainChild(children) {
2436
+ if (!children || children.length === 0) return null;
2437
+
2438
+ // If only one child, it's the main path
2439
+ if (children.length === 1) return children[0];
2440
+
2441
+ // Prioritize directories over files
2442
+ const directories = children.filter(child => child.data.type === 'directory');
2443
+ if (directories.length === 1) return directories[0];
2444
+
2445
+ // If multiple directories, choose the first one (could be enhanced with better logic)
2446
+ if (directories.length > 0) return directories[0];
2447
+
2448
+ // Fallback to first child
2449
+ return children[0];
2450
+ }
2451
+
2452
+ /**
2453
+ * Find chains of singleton nodes (nodes with only one child)
2454
+ */
2455
+ findSingletonChains(nodes) {
2456
+ const chains = [];
2457
+ const processed = new Set();
2458
+
2459
+ nodes.forEach(node => {
2460
+ if (processed.has(node)) return;
2461
+
2462
+ // Start a new chain if this node has exactly one child
2463
+ if (node.children && node.children.length === 1) {
2464
+ const chain = [node];
2465
+ let current = node.children[0];
2466
+
2467
+ console.log(`🔍 [CHAIN] Starting singleton chain with: ${node.data.name} (depth: ${node.depth})`);
2468
+
2469
+ // Follow the chain of singletons
2470
+ while (current && current.children && current.children.length === 1) {
2471
+ chain.push(current);
2472
+ processed.add(current);
2473
+ console.log(`🔍 [CHAIN] Adding to chain: ${current.data.name} (depth: ${current.depth})`);
2474
+ current = current.children[0];
2475
+ }
2476
+
2477
+ // Add the final node if it exists (even if it has multiple children or no children)
2478
+ if (current) {
2479
+ chain.push(current);
2480
+ processed.add(current);
2481
+ console.log(`🔍 [CHAIN] Final node in chain: ${current.data.name} (depth: ${current.depth})`);
2482
+ }
2483
+
2484
+ // Only create horizontal layout for chains of 2 or more nodes
2485
+ if (chain.length >= 2) {
2486
+ console.log(`✅ [CHAIN] Created horizontal chain:`, chain.map(n => n.data.name));
2487
+ chains.push(chain);
2488
+ processed.add(node);
2489
+ } else {
2490
+ console.log(`❌ [CHAIN] Chain too short (${chain.length}), skipping`);
2491
+ }
2492
+ }
2493
+ });
2494
+
2495
+ return chains;
2496
+ }
2497
+
2498
+ /**
2499
+ * Layout a chain of nodes horizontally with parent in center
2500
+ */
2501
+ layoutChainHorizontally(chain) {
2502
+ if (chain.length < 2) return;
2503
+
2504
+ const horizontalSpacing = 150; // Spacing between nodes in horizontal chain
2505
+ const parentNode = chain[0];
2506
+ const originalX = parentNode.x;
2507
+ const originalY = parentNode.y;
2508
+
2509
+ // CRITICAL: In D3 tree layout for linear mode:
2510
+ // - d.x controls VERTICAL position (up-down)
2511
+ // - d.y controls HORIZONTAL position (left-right)
2512
+ // To make singleton chains horizontal, we need to adjust d.x (vertical) to be the same
2513
+ // and spread out d.y (horizontal) positions
2514
+
2515
+ if (chain.length === 2) {
2516
+ // Simple case: parent and one child side by side
2517
+ const centerY = originalY;
2518
+ parentNode.y = centerY - horizontalSpacing / 2; // Parent to the left
2519
+ chain[1].y = centerY + horizontalSpacing / 2; // Child to the right
2520
+ chain[1].x = originalX; // Same vertical level as parent
2521
+ } else {
2522
+ // Multiple nodes: center the parent in the horizontal chain
2523
+ const totalWidth = (chain.length - 1) * horizontalSpacing;
2524
+ const startY = originalY - (totalWidth / 2);
2525
+
2526
+ chain.forEach((node, index) => {
2527
+ node.y = startY + (index * horizontalSpacing); // Spread horizontally
2528
+ node.x = originalX; // All at same vertical level
2529
+ });
2530
+ }
2531
+
2532
+ // Mark all nodes in this chain as needing horizontal text
2533
+ chain.forEach(node => {
2534
+ this.horizontalNodes.add(node.data.path);
2535
+ console.log(`📝 [TEXT] Marking node for horizontal text: ${node.data.name} (${node.data.path})`);
2536
+ });
2537
+
2538
+ console.log(`🔄 [LAYOUT] Horizontal chain of ${chain.length} nodes:`,
2539
+ chain.map(n => ({ name: n.data.name, vertical: n.x, horizontal: n.y })));
2540
+ console.log(`📝 [TEXT] Total horizontal nodes:`, Array.from(this.horizontalNodes));
2541
+ }
2542
+
2543
+
2544
+
2085
2545
  /**
2086
2546
  * Update D3 tree visualization
2087
2547
  */
@@ -2095,6 +2555,9 @@ class CodeTree {
2095
2555
  const nodes = treeData.descendants();
2096
2556
  const links = treeData.descendants().slice(1);
2097
2557
 
2558
+ // Apply horizontal layout for singleton chains
2559
+ this.applySingletonHorizontalLayout(nodes);
2560
+
2098
2561
  if (this.isRadialLayout) {
2099
2562
  // Radial layout adjustments
2100
2563
  nodes.forEach(d => {
@@ -2173,20 +2636,42 @@ class CodeTree {
2173
2636
 
2174
2637
  // Add labels for nodes with smart positioning
2175
2638
  nodeEnter.append('text')
2176
- .attr('class', 'node-label')
2639
+ .attr('class', d => {
2640
+ // Add horizontal-text class for root node
2641
+ const baseClass = 'node-label';
2642
+ if (d.depth === 0) {
2643
+ console.log(`📝 [TEXT] ✅ Adding horizontal-text class to root: ${d.data.name}`);
2644
+ return `${baseClass} horizontal-text`;
2645
+ }
2646
+ return baseClass;
2647
+ })
2177
2648
  .attr('dy', '.35em')
2178
2649
  .attr('x', d => {
2179
2650
  if (this.isRadialLayout) {
2180
2651
  // For radial layout, initial position
2181
2652
  return 0;
2653
+ } else if (d.depth === 0 || this.horizontalNodes.has(d.data.path)) {
2654
+ // Root node or horizontal nodes: center text above the node
2655
+ console.log(`📝 [TEXT] ✅ HORIZONTAL positioning for: ${d.data.name} (depth: ${d.depth}, path: ${d.data.path})`);
2656
+ console.log(`📝 [TEXT] ✅ Root check: depth === 0 = ${d.depth === 0}`);
2657
+ console.log(`📝 [TEXT] ✅ Horizontal set check: ${this.horizontalNodes.has(d.data.path)}`);
2658
+ return 0;
2182
2659
  } else {
2183
2660
  // Linear layout: standard positioning
2661
+ console.log(`📝 [TEXT] Positioning vertical text for: ${d.data.name} (depth: ${d.depth}, path: ${d.data.path})`);
2184
2662
  return d.children || d._children ? -13 : 13;
2185
2663
  }
2186
2664
  })
2665
+ .attr('y', d => {
2666
+ // For root node or horizontal nodes, position text above the node
2667
+ return (d.depth === 0 || this.horizontalNodes.has(d.data.path)) ? -20 : 0;
2668
+ })
2187
2669
  .attr('text-anchor', d => {
2188
2670
  if (this.isRadialLayout) {
2189
2671
  return 'start'; // Will be adjusted in update
2672
+ } else if (d.depth === 0 || this.horizontalNodes.has(d.data.path)) {
2673
+ // Root node or horizontal nodes: center the text
2674
+ return 'middle';
2190
2675
  } else {
2191
2676
  // Linear layout: standard anchoring
2192
2677
  return d.children || d._children ? 'end' : 'start';
@@ -2203,6 +2688,22 @@ class CodeTree {
2203
2688
  .style('font-size', '12px')
2204
2689
  .style('font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif')
2205
2690
  .style('text-shadow', '1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8)')
2691
+ .style('writing-mode', d => {
2692
+ // Force horizontal writing mode for root node
2693
+ if (d.depth === 0) {
2694
+ console.log(`📝 [TEXT] ✅ Setting horizontal writing-mode for root: ${d.data.name}`);
2695
+ return 'horizontal-tb';
2696
+ }
2697
+ return null;
2698
+ })
2699
+ .style('text-orientation', d => {
2700
+ // Force mixed text orientation for root node
2701
+ if (d.depth === 0) {
2702
+ console.log(`📝 [TEXT] ✅ Setting mixed text-orientation for root: ${d.data.name}`);
2703
+ return 'mixed';
2704
+ }
2705
+ return null;
2706
+ })
2206
2707
  .on('click', (event, d) => this.onNodeClick(event, d)) // CRITICAL FIX: Add click handler to labels
2207
2708
  .style('cursor', 'pointer');
2208
2709
 
@@ -2301,6 +2802,7 @@ class CodeTree {
2301
2802
 
2302
2803
  // Update text labels with proper rotation for radial layout
2303
2804
  const isRadial = this.isRadialLayout; // Capture the layout type
2805
+ const horizontalNodes = this.horizontalNodes; // Capture horizontal nodes set
2304
2806
  nodeUpdate.select('text.node-label')
2305
2807
  .style('fill-opacity', 1)
2306
2808
  .style('fill', '#333')
@@ -2331,12 +2833,26 @@ class CodeTree {
2331
2833
  .attr('dy', '.35em');
2332
2834
  }
2333
2835
  } else {
2334
- // Linear layout - no rotation needed
2335
- selection
2336
- .attr('transform', null)
2337
- .attr('x', d.children || d._children ? -13 : 13)
2338
- .attr('text-anchor', d.children || d._children ? 'end' : 'start')
2339
- .attr('dy', '.35em');
2836
+ // Linear layout - handle root node and horizontal nodes differently
2837
+ const isHorizontal = d.depth === 0 || horizontalNodes.has(d.data.path);
2838
+
2839
+ if (isHorizontal) {
2840
+ // Root node or horizontal nodes: text above the node, centered
2841
+ selection
2842
+ .attr('transform', null)
2843
+ .attr('x', 0)
2844
+ .attr('y', -20)
2845
+ .attr('text-anchor', 'middle')
2846
+ .attr('dy', '.35em');
2847
+ } else {
2848
+ // Regular linear layout - no rotation needed
2849
+ selection
2850
+ .attr('transform', null)
2851
+ .attr('x', d.children || d._children ? -13 : 13)
2852
+ .attr('y', 0)
2853
+ .attr('text-anchor', d.children || d._children ? 'end' : 'start')
2854
+ .attr('dy', '.35em');
2855
+ }
2340
2856
  }
2341
2857
  });
2342
2858
 
@@ -2404,6 +2920,14 @@ class CodeTree {
2404
2920
  d.x0 = d.x;
2405
2921
  d.y0 = d.y;
2406
2922
  });
2923
+
2924
+ // Apply current zoom level to maintain consistent text size
2925
+ if (this.zoom) {
2926
+ const currentTransform = d3.zoomTransform(this.svg.node());
2927
+ if (currentTransform.k !== 1) {
2928
+ this.adjustTextSizeForZoom(currentTransform.k);
2929
+ }
2930
+ }
2407
2931
  }
2408
2932
 
2409
2933
  /**
@@ -2578,16 +3102,21 @@ class CodeTree {
2578
3102
  * Handle node click - implement lazy loading with enhanced visual feedback
2579
3103
  */
2580
3104
  onNodeClick(event, d) {
3105
+ const clickId = Date.now() + Math.random();
2581
3106
  // DEBUG: Log all clicks to verify handler is working
2582
- console.log('🖱️ [NODE CLICK] Clicked on node:', {
3107
+ console.log(`🖱️ [NODE CLICK] Clicked on node (ID: ${clickId}):`, {
2583
3108
  name: d?.data?.name,
2584
3109
  path: d?.data?.path,
2585
3110
  type: d?.data?.type,
2586
3111
  loaded: d?.data?.loaded,
2587
3112
  hasChildren: !!(d?.children || d?._children),
2588
- dataChildren: d?.data?.children?.length || 0
3113
+ dataChildren: d?.data?.children?.length || 0,
3114
+ loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined'
2589
3115
  });
2590
-
3116
+
3117
+ // Update structured data with clicked node
3118
+ this.updateStructuredData(d);
3119
+
2591
3120
  // Handle node click interaction
2592
3121
 
2593
3122
  // Check event parameter
@@ -2686,11 +3215,8 @@ class CodeTree {
2686
3215
 
2687
3216
 
2688
3217
  // Get selected languages from checkboxes
2689
- const selectedLanguages = [];
2690
- const checkboxes = document.querySelectorAll('.language-checkbox:checked');
2691
- checkboxes.forEach(cb => {
2692
- selectedLanguages.push(cb.value);
2693
- });
3218
+ const selectedLanguages = this.getSelectedLanguages();
3219
+ console.log('🔍 [LANGUAGE] Selected languages:', selectedLanguages);
2694
3220
 
2695
3221
  // Get ignore patterns
2696
3222
  const ignorePatternsElement = document.getElementById('ignore-patterns');
@@ -2710,12 +3236,45 @@ class CodeTree {
2710
3236
  shouldLoad: d.data.type === 'directory' && !d.data.loaded
2711
3237
  });
2712
3238
  if (d.data.type === 'directory' && !d.data.loaded) {
2713
- // Prevent duplicate requests
2714
- if (this.loadingNodes.has(d.data.path)) {
2715
- this.showNotification(`Already loading: ${d.data.name}`, 'warning');
2716
- return;
3239
+ console.log('✅ [SUBDIRECTORY LOADING] Load check passed, proceeding with loading logic');
3240
+ console.log('🔍 [SUBDIRECTORY LOADING] Initial loading state:', {
3241
+ loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined',
3242
+ loadingNodesContent: Array.from(this.loadingNodes || [])
3243
+ });
3244
+
3245
+ try {
3246
+ // Debug the path and loadingNodes state
3247
+ console.log('🔍 [SUBDIRECTORY LOADING] Checking for duplicates:', {
3248
+ path: d.data.path,
3249
+ pathType: typeof d.data.path,
3250
+ loadingNodesType: typeof this.loadingNodes,
3251
+ loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined',
3252
+ hasMethod: this.loadingNodes && typeof this.loadingNodes.has === 'function'
3253
+ });
3254
+
3255
+ // Prevent duplicate requests
3256
+ const isDuplicate = this.loadingNodes && this.loadingNodes.has(d.data.path);
3257
+ console.log('🔍 [SUBDIRECTORY LOADING] Duplicate check result:', {
3258
+ isDuplicate: isDuplicate,
3259
+ loadingNodesContent: Array.from(this.loadingNodes || []),
3260
+ pathBeingChecked: d.data.path
3261
+ });
3262
+
3263
+ if (isDuplicate) {
3264
+ console.warn('⚠️ [SUBDIRECTORY LOADING] Duplicate request detected, but proceeding anyway:', {
3265
+ path: d.data.path,
3266
+ name: d.data.name,
3267
+ loadingNodesSize: this.loadingNodes.size,
3268
+ loadingNodesContent: Array.from(this.loadingNodes),
3269
+ pathInSet: this.loadingNodes.has(d.data.path)
3270
+ });
3271
+ // Remove the existing entry and proceed
3272
+ this.loadingNodes.delete(d.data.path);
3273
+ console.log('🧹 [SUBDIRECTORY LOADING] Removed duplicate entry, proceeding with fresh request');
2717
3274
  }
2718
-
3275
+
3276
+ console.log('✅ [SUBDIRECTORY LOADING] No duplicate request, proceeding to mark as loading');
3277
+
2719
3278
  // Mark as loading immediately to prevent duplicate requests
2720
3279
  d.data.loaded = 'loading';
2721
3280
  this.loadingNodes.add(d.data.path);
@@ -2788,10 +3347,13 @@ class CodeTree {
2788
3347
  if (data.exists && data.is_directory && data.contents) {
2789
3348
  const node = this.findNodeByPath(d.data.path);
2790
3349
  if (node) {
3350
+ console.log('🔧 [SUBDIRECTORY LOADING] Creating children with paths:',
3351
+ data.contents.map(item => ({ name: item.name, path: item.path })));
3352
+
2791
3353
  // Add children to the node
2792
3354
  node.children = data.contents.map(item => ({
2793
3355
  name: item.name,
2794
- path: `${d.data.path}/${item.name}`,
3356
+ path: item.path, // Use the full path from API response
2795
3357
  type: item.is_directory ? 'directory' : 'file',
2796
3358
  loaded: item.is_directory ? false : undefined,
2797
3359
  analyzed: !item.is_directory ? false : undefined,
@@ -2817,8 +3379,15 @@ class CodeTree {
2817
3379
  }
2818
3380
 
2819
3381
  this.update(updatedD3Node || this.root);
3382
+
3383
+ // Focus on the newly loaded directory for better UX
3384
+ if (updatedD3Node && data.contents.length > 0) {
3385
+ setTimeout(() => {
3386
+ this.focusOnDirectory(updatedD3Node);
3387
+ }, 500); // Small delay to let the update animation complete
3388
+ }
2820
3389
  }
2821
-
3390
+
2822
3391
  this.updateBreadcrumb(`Loaded ${data.contents.length} items`, 'success');
2823
3392
  this.showNotification(`Loaded ${data.contents.length} items from ${d.data.name}`, 'success');
2824
3393
  }
@@ -2870,12 +3439,37 @@ class CodeTree {
2870
3439
  d.data.loaded = false;
2871
3440
  }
2872
3441
  }, 100); // 100ms delay to ensure visual effects render first
2873
- }
3442
+
3443
+ } catch (error) {
3444
+ console.error('❌ [SUBDIRECTORY LOADING] Error in directory loading logic:', {
3445
+ error: error.message,
3446
+ stack: error.stack,
3447
+ path: d.data.path,
3448
+ nodeData: d.data
3449
+ });
3450
+ this.showNotification(`Error loading directory: ${error.message}`, 'error');
3451
+ }
3452
+ }
2874
3453
  // For files that haven't been analyzed, request analysis
2875
3454
  else if (d.data.type === 'file' && !d.data.analyzed) {
2876
3455
  // Only analyze files of selected languages
2877
3456
  const fileLanguage = this.detectLanguage(d.data.path);
3457
+ console.log('🔍 [FILE ANALYSIS] Language check:', {
3458
+ fileName: d.data.name,
3459
+ filePath: d.data.path,
3460
+ detectedLanguage: fileLanguage,
3461
+ selectedLanguages: selectedLanguages,
3462
+ isLanguageSelected: selectedLanguages.includes(fileLanguage),
3463
+ shouldAnalyze: selectedLanguages.includes(fileLanguage) || fileLanguage === 'unknown'
3464
+ });
3465
+
2878
3466
  if (!selectedLanguages.includes(fileLanguage) && fileLanguage !== 'unknown') {
3467
+ console.warn('⚠️ [FILE ANALYSIS] Skipping file:', {
3468
+ fileName: d.data.name,
3469
+ detectedLanguage: fileLanguage,
3470
+ selectedLanguages: selectedLanguages,
3471
+ reason: `${fileLanguage} not in selected languages`
3472
+ });
2879
3473
  this.showNotification(`Skipping ${d.data.name} - ${fileLanguage} not selected`, 'warning');
2880
3474
  return;
2881
3475
  }
@@ -2891,14 +3485,43 @@ class CodeTree {
2891
3485
 
2892
3486
  // Delay the socket request to ensure visual effects are rendered
2893
3487
  setTimeout(() => {
2894
-
2895
- if (this.socket) {
3488
+ console.log('🚀 [FILE ANALYSIS] Sending analysis request:', {
3489
+ fileName: d.data.name,
3490
+ originalPath: d.data.path,
3491
+ fullPath: fullPath,
3492
+ hasSocket: !!this.socket,
3493
+ socketConnected: this.socket?.connected
3494
+ });
3495
+
3496
+ if (this.socket && this.socket.connected) {
3497
+ console.log('📡 [FILE ANALYSIS] Using SocketIO for analysis:', {
3498
+ event: 'code:analyze:file',
3499
+ path: fullPath,
3500
+ socketConnected: this.socket.connected,
3501
+ socketId: this.socket.id
3502
+ });
3503
+
2896
3504
  this.socket.emit('code:analyze:file', {
2897
3505
  path: fullPath
2898
3506
  });
2899
-
3507
+
3508
+ // Set a shorter timeout since we have a stable server
3509
+ const analysisTimeout = setTimeout(() => {
3510
+ console.warn('⏰ [FILE ANALYSIS] SocketIO timeout, trying HTTP fallback for:', fullPath);
3511
+ this.analyzeFileHTTP(fullPath, d.data.name, d3.select(event.target.closest('g')));
3512
+ }, 5000); // 5 second timeout
3513
+
3514
+ // Store timeout ID for cleanup
3515
+ if (!this.analysisTimeouts) this.analysisTimeouts = new Map();
3516
+ this.analysisTimeouts.set(fullPath, analysisTimeout);
3517
+
3518
+ this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
3519
+ this.showNotification(`Analyzing: ${d.data.name}`, 'info');
3520
+ } else {
3521
+ console.log('🔄 [FILE ANALYSIS] SocketIO unavailable, using HTTP fallback');
2900
3522
  this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
2901
3523
  this.showNotification(`Analyzing: ${d.data.name}`, 'info');
3524
+ this.analyzeFileHTTP(fullPath, d.data.name, d3.select(event.target.closest('g')));
2902
3525
  }
2903
3526
  }, 100); // 100ms delay to ensure visual effects render first
2904
3527
  }
@@ -3215,14 +3838,294 @@ class CodeTree {
3215
3838
  }
3216
3839
 
3217
3840
  /**
3218
- * Reset zoom to fit the tree
3841
+ * Focus on a specific directory, hiding parent directories and showing only its contents
3219
3842
  */
3220
- resetZoom() {
3221
- // DISABLED: All zoom reset operations have been disabled to prevent tree centering/movement
3222
- // The tree should remain stationary and not center/move when interacting with nodes
3223
- console.log('[CodeTree] resetZoom called but disabled - no zoom reset will occur');
3224
- this.showNotification('Zoom reset disabled - tree remains stationary', 'info');
3225
- return;
3843
+ focusOnDirectory(node) {
3844
+ if (!node || node.data.type !== 'directory') return;
3845
+
3846
+ console.log('🎯 [FOCUS] Focusing on directory:', node.data.path);
3847
+
3848
+ // Store the focused node
3849
+ this.focusedNode = node;
3850
+
3851
+ // Create a temporary root for display purposes
3852
+ const focusedRoot = {
3853
+ ...node.data,
3854
+ name: `📁 ${node.data.name}`,
3855
+ children: node.data.children || []
3856
+ };
3857
+
3858
+ // Create new D3 hierarchy with focused node as root
3859
+ const tempRoot = d3.hierarchy(focusedRoot);
3860
+ tempRoot.x0 = this.height / 2;
3861
+ tempRoot.y0 = 0;
3862
+
3863
+ // Store original root for restoration
3864
+ if (!this.originalRoot) {
3865
+ this.originalRoot = this.root;
3866
+ }
3867
+
3868
+ // Update with focused view
3869
+ this.root = tempRoot;
3870
+ this.update(this.root);
3871
+
3872
+ // Add visual styling for focused mode
3873
+ d3.select('#code-tree-container').classed('focused', true);
3874
+
3875
+ // Update breadcrumb to show focused path
3876
+ this.updateBreadcrumb(`Focused on: ${node.data.name}`, 'info');
3877
+ this.showNotification(`Focused on directory: ${node.data.name}`, 'info');
3878
+
3879
+ // Add back button to toolbar
3880
+ this.addBackButton();
3881
+ }
3882
+
3883
+ /**
3884
+ * Return to the full tree view from focused directory view
3885
+ */
3886
+ unfocusDirectory() {
3887
+ if (!this.originalRoot) return;
3888
+
3889
+ console.log('🔙 [FOCUS] Returning to full tree view');
3890
+
3891
+ // Restore original root
3892
+ this.root = this.originalRoot;
3893
+ this.originalRoot = null;
3894
+ this.focusedNode = null;
3895
+
3896
+ // Update display
3897
+ this.update(this.root);
3898
+
3899
+ // Remove visual styling for focused mode
3900
+ d3.select('#code-tree-container').classed('focused', false);
3901
+
3902
+ // Remove back button
3903
+ this.removeBackButton();
3904
+
3905
+ this.updateBreadcrumb('Full tree view restored', 'success');
3906
+ this.showNotification('Returned to full tree view', 'success');
3907
+ }
3908
+
3909
+ /**
3910
+ * Add back button to return from focused view
3911
+ */
3912
+ addBackButton() {
3913
+ // Remove existing back button
3914
+ d3.select('#tree-back-button').remove();
3915
+
3916
+ const toolbar = d3.select('.tree-controls-toolbar');
3917
+ if (toolbar.empty()) return;
3918
+
3919
+ toolbar.insert('button', ':first-child')
3920
+ .attr('id', 'tree-back-button')
3921
+ .attr('class', 'tree-control-btn back-btn')
3922
+ .attr('title', 'Return to full tree view')
3923
+ .text('← Back')
3924
+ .on('click', () => this.unfocusDirectory());
3925
+ }
3926
+
3927
+ /**
3928
+ * Remove back button
3929
+ */
3930
+ removeBackButton() {
3931
+ d3.select('#tree-back-button').remove();
3932
+ }
3933
+
3934
+ /**
3935
+ * Reset zoom to fit the tree
3936
+ */
3937
+ resetZoom() {
3938
+ if (!this.svg || !this.zoom) return;
3939
+
3940
+ // Calculate bounds of the tree
3941
+ const bounds = this.treeGroup.node().getBBox();
3942
+ const fullWidth = this.width;
3943
+ const fullHeight = this.height;
3944
+ const width = bounds.width;
3945
+ const height = bounds.height;
3946
+ const midX = bounds.x + width / 2;
3947
+ const midY = bounds.y + height / 2;
3948
+
3949
+ if (width === 0 || height === 0) return; // Nothing to fit
3950
+
3951
+ // Calculate scale to fit tree in view with some padding
3952
+ const scale = Math.min(fullWidth / width, fullHeight / height) * 0.9;
3953
+
3954
+ // Calculate translate to center the tree
3955
+ const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
3956
+
3957
+ // Apply the transform with smooth transition
3958
+ this.svg.transition()
3959
+ .duration(750)
3960
+ .call(this.zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
3961
+
3962
+ this.showNotification('Zoom reset to fit tree', 'info');
3963
+ }
3964
+
3965
+ /**
3966
+ * Zoom in by a fixed factor
3967
+ */
3968
+ zoomIn() {
3969
+ if (!this.svg || !this.zoom) return;
3970
+
3971
+ this.svg.transition()
3972
+ .duration(300)
3973
+ .call(this.zoom.scaleBy, 1.5);
3974
+ }
3975
+
3976
+ /**
3977
+ * Zoom out by a fixed factor
3978
+ */
3979
+ zoomOut() {
3980
+ if (!this.svg || !this.zoom) return;
3981
+
3982
+ this.svg.transition()
3983
+ .duration(300)
3984
+ .call(this.zoom.scaleBy, 1 / 1.5);
3985
+ }
3986
+
3987
+ /**
3988
+ * Update zoom level display
3989
+ */
3990
+ updateZoomLevel(scale) {
3991
+ const zoomDisplay = document.getElementById('zoom-level-display');
3992
+ if (zoomDisplay) {
3993
+ zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
3994
+ }
3995
+ }
3996
+
3997
+ /**
3998
+ * Adjust text size to remain constant during zoom
3999
+ */
4000
+ adjustTextSizeForZoom(zoomScale) {
4001
+ if (!this.treeGroup) return;
4002
+
4003
+ // Calculate the inverse scale to keep text at consistent size
4004
+ const textScale = 1 / zoomScale;
4005
+
4006
+ // Apply inverse scaling to all text elements
4007
+ this.treeGroup.selectAll('text')
4008
+ .style('font-size', `${12 * textScale}px`)
4009
+ .attr('transform', function() {
4010
+ // Get existing transform if any
4011
+ const existingTransform = d3.select(this).attr('transform') || '';
4012
+ // Remove any existing scale transforms and add the new one
4013
+ const cleanTransform = existingTransform.replace(/scale\([^)]*\)/g, '').trim();
4014
+ return cleanTransform ? `${cleanTransform} scale(${textScale})` : `scale(${textScale})`;
4015
+ });
4016
+
4017
+ // Also adjust other UI elements that should maintain size
4018
+ this.treeGroup.selectAll('.expand-icon')
4019
+ .style('font-size', `${12 * textScale}px`)
4020
+ .attr('transform', function() {
4021
+ const existingTransform = d3.select(this).attr('transform') || '';
4022
+ const cleanTransform = existingTransform.replace(/scale\([^)]*\)/g, '').trim();
4023
+ return cleanTransform ? `${cleanTransform} scale(${textScale})` : `scale(${textScale})`;
4024
+ });
4025
+
4026
+ // Adjust item count badges
4027
+ this.treeGroup.selectAll('.item-count-badge')
4028
+ .style('font-size', `${10 * textScale}px`)
4029
+ .attr('transform', function() {
4030
+ const existingTransform = d3.select(this).attr('transform') || '';
4031
+ const cleanTransform = existingTransform.replace(/scale\([^)]*\)/g, '').trim();
4032
+ return cleanTransform ? `${cleanTransform} scale(${textScale})` : `scale(${textScale})`;
4033
+ });
4034
+ }
4035
+
4036
+ /**
4037
+ * Add keyboard shortcuts for zoom functionality
4038
+ */
4039
+ addZoomKeyboardShortcuts() {
4040
+ // Only add shortcuts when the code tab is active
4041
+ document.addEventListener('keydown', (event) => {
4042
+ // Check if code tab is active
4043
+ const codeTab = document.getElementById('code-tab');
4044
+ if (!codeTab || !codeTab.classList.contains('active')) {
4045
+ return;
4046
+ }
4047
+
4048
+ // Prevent shortcuts when typing in input fields
4049
+ if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
4050
+ return;
4051
+ }
4052
+
4053
+ // Handle zoom shortcuts
4054
+ if (event.ctrlKey || event.metaKey) {
4055
+ switch (event.key) {
4056
+ case '=':
4057
+ case '+':
4058
+ event.preventDefault();
4059
+ this.zoomIn();
4060
+ break;
4061
+ case '-':
4062
+ event.preventDefault();
4063
+ this.zoomOut();
4064
+ break;
4065
+ case '0':
4066
+ event.preventDefault();
4067
+ this.resetZoom();
4068
+ break;
4069
+ }
4070
+ }
4071
+ });
4072
+ }
4073
+
4074
+ /**
4075
+ * Check if a file path represents a source file that should show source viewer
4076
+ */
4077
+ isSourceFile(path) {
4078
+ if (!path) return false;
4079
+ const sourceExtensions = ['.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.cpp', '.c', '.h', '.cs', '.php', '.rb', '.go', '.rs', '.swift'];
4080
+ return sourceExtensions.some(ext => path.toLowerCase().endsWith(ext));
4081
+ }
4082
+
4083
+ /**
4084
+ * Show hierarchical source viewer for a source file
4085
+ */
4086
+ async showSourceViewer(node) {
4087
+ console.log('📄 [SOURCE VIEWER] Showing source for:', node.data.path);
4088
+
4089
+ // Create source viewer container
4090
+ const sourceViewer = document.createElement('div');
4091
+ sourceViewer.className = 'source-viewer';
4092
+
4093
+ // Create header
4094
+ const header = document.createElement('div');
4095
+ header.className = 'source-viewer-header';
4096
+ header.innerHTML = `
4097
+ <span>📄 ${node.data.name || 'Source File'}</span>
4098
+ <div class="source-viewer-controls">
4099
+ <button class="source-control-btn" id="expand-all-source" title="Expand all">⬇</button>
4100
+ <button class="source-control-btn" id="collapse-all-source" title="Collapse all">⬆</button>
4101
+ </div>
4102
+ `;
4103
+
4104
+ // Create content container
4105
+ const content = document.createElement('div');
4106
+ content.className = 'source-viewer-content';
4107
+ content.id = 'source-viewer-content';
4108
+
4109
+ sourceViewer.appendChild(header);
4110
+ sourceViewer.appendChild(content);
4111
+ this.structuredDataContent.appendChild(sourceViewer);
4112
+
4113
+ // Add control event listeners
4114
+ document.getElementById('expand-all-source')?.addEventListener('click', () => this.expandAllSource());
4115
+ document.getElementById('collapse-all-source')?.addEventListener('click', () => this.collapseAllSource());
4116
+
4117
+ // Load and display source code
4118
+ try {
4119
+ await this.loadSourceContent(node, content);
4120
+ } catch (error) {
4121
+ console.error('Failed to load source content:', error);
4122
+ content.innerHTML = `
4123
+ <div class="ast-data-placeholder">
4124
+ <div class="ast-placeholder-icon">❌</div>
4125
+ <div class="ast-placeholder-text">Failed to load source file</div>
4126
+ </div>
4127
+ `;
4128
+ }
3226
4129
  }
3227
4130
 
3228
4131
  /**
@@ -3384,6 +4287,35 @@ class CodeTree {
3384
4287
  }
3385
4288
  }
3386
4289
 
4290
+ /**
4291
+ * Debug function to clear loading state (for troubleshooting)
4292
+ */
4293
+ clearLoadingState() {
4294
+ console.log('🧹 [DEBUG] Clearing loading state:', {
4295
+ loadingNodesBefore: Array.from(this.loadingNodes),
4296
+ size: this.loadingNodes.size
4297
+ });
4298
+ this.loadingNodes.clear();
4299
+
4300
+ // Also reset any nodes marked as 'loading'
4301
+ this.resetLoadingFlags(this.treeData);
4302
+
4303
+ console.log('✅ [DEBUG] Loading state cleared');
4304
+ this.showNotification('Loading state cleared', 'info');
4305
+ }
4306
+
4307
+ /**
4308
+ * Recursively reset loading flags in tree data
4309
+ */
4310
+ resetLoadingFlags(node) {
4311
+ if (node.loaded === 'loading') {
4312
+ node.loaded = false;
4313
+ }
4314
+ if (node.children) {
4315
+ node.children.forEach(child => this.resetLoadingFlags(child));
4316
+ }
4317
+ }
4318
+
3387
4319
  /**
3388
4320
  * Export tree data
3389
4321
  */
@@ -3428,7 +4360,7 @@ class CodeTree {
3428
4360
  if (ticker) {
3429
4361
  ticker.textContent = message;
3430
4362
  ticker.className = `ticker ticker-${type}`;
3431
-
4363
+
3432
4364
  // Auto-hide after 5 seconds for non-error messages
3433
4365
  if (type !== 'error') {
3434
4366
  setTimeout(() => {
@@ -3441,6 +4373,790 @@ class CodeTree {
3441
4373
  }
3442
4374
  }
3443
4375
  }
4376
+
4377
+ /**
4378
+ * Initialize the structured data integration
4379
+ */
4380
+ initializeStructuredData() {
4381
+ this.structuredDataContent = document.getElementById('module-data-content');
4382
+
4383
+ if (!this.structuredDataContent) {
4384
+ console.warn('Structured data content element not found');
4385
+ return;
4386
+ }
4387
+
4388
+ console.log('✅ Structured data integration initialized');
4389
+ }
4390
+
4391
+ /**
4392
+ * Update structured data with node information
4393
+ */
4394
+ updateStructuredData(node) {
4395
+ if (!this.structuredDataContent) {
4396
+ return;
4397
+ }
4398
+
4399
+ console.log('🔍 [STRUCTURED DATA] Updating with node:', {
4400
+ name: node?.data?.name,
4401
+ type: node?.data?.type,
4402
+ hasChildren: !!(node?.children || node?._children),
4403
+ dataChildren: node?.data?.children?.length || 0
4404
+ });
4405
+
4406
+ // Clear previous content
4407
+ this.structuredDataContent.innerHTML = '';
4408
+
4409
+ // Check if this is a source file that should show source viewer
4410
+ if (node.data.type === 'file' && this.isSourceFile(node.data.path)) {
4411
+ this.showSourceViewer(node);
4412
+ } else {
4413
+ // Show children or functions for non-source files
4414
+ const children = node.children || node._children || [];
4415
+ const dataChildren = node.data.children || [];
4416
+
4417
+ if (children.length > 0 || dataChildren.length > 0) {
4418
+ this.showASTNodeChildren(node);
4419
+ } else if (node.data.type === 'file' && node.data.analyzed) {
4420
+ this.showASTFileDetails(node);
4421
+ } else {
4422
+ this.showASTNodeDetails(node);
4423
+ }
4424
+ }
4425
+ }
4426
+
4427
+ /**
4428
+ * Show child nodes in structured data
4429
+ */
4430
+ showASTNodeChildren(node) {
4431
+ const children = node.children || node._children || [];
4432
+ const dataChildren = node.data.children || [];
4433
+
4434
+ // Use D3 children if available, otherwise use data children
4435
+ const childrenToShow = children.length > 0 ? children : dataChildren;
4436
+
4437
+ if (childrenToShow.length === 0) {
4438
+ this.showASTEmptyState('No children found');
4439
+ return;
4440
+ }
4441
+
4442
+ // Create header
4443
+ const header = document.createElement('div');
4444
+ header.className = 'structured-view-header';
4445
+ header.innerHTML = `<h4>${this.getNodeIcon(node.data.type)} ${node.data.name || 'Node'} - Children (${childrenToShow.length})</h4>`;
4446
+ this.structuredDataContent.appendChild(header);
4447
+
4448
+ childrenToShow.forEach((child, index) => {
4449
+ const childData = child.data || child;
4450
+ const item = this.createASTDataViewerItem(childData, index);
4451
+ this.structuredDataContent.appendChild(item);
4452
+ });
4453
+ }
4454
+
4455
+ /**
4456
+ * Show file details in structured data
4457
+ */
4458
+ showASTFileDetails(node) {
4459
+ // Create header
4460
+ const header = document.createElement('div');
4461
+ header.className = 'structured-view-header';
4462
+ header.innerHTML = `<h4>${this.getNodeIcon(node.data.type)} ${node.data.name || 'File'} - Details</h4>`;
4463
+ this.structuredDataContent.appendChild(header);
4464
+
4465
+ const details = [];
4466
+
4467
+ if (node.data.language) {
4468
+ details.push({ label: 'Language', value: node.data.language });
4469
+ }
4470
+
4471
+ if (node.data.lines) {
4472
+ details.push({ label: 'Lines', value: node.data.lines });
4473
+ }
4474
+
4475
+ if (node.data.complexity !== undefined) {
4476
+ details.push({ label: 'Complexity', value: node.data.complexity });
4477
+ }
4478
+
4479
+ if (node.data.size) {
4480
+ details.push({ label: 'Size', value: this.formatFileSize(node.data.size) });
4481
+ }
4482
+
4483
+ if (details.length === 0) {
4484
+ this.showASTEmptyState('No details available');
4485
+ return;
4486
+ }
4487
+
4488
+ details.forEach((detail, index) => {
4489
+ const item = this.createASTDetailItem(detail, index);
4490
+ this.structuredDataContent.appendChild(item);
4491
+ });
4492
+ }
4493
+
4494
+ /**
4495
+ * Show basic node details in structured data
4496
+ */
4497
+ showASTNodeDetails(node) {
4498
+ // Create header
4499
+ const header = document.createElement('div');
4500
+ header.className = 'structured-view-header';
4501
+ header.innerHTML = `<h4>${this.getNodeIcon(node.data.type)} ${node.data.name || 'Node'} - Details</h4>`;
4502
+ this.structuredDataContent.appendChild(header);
4503
+
4504
+ const details = [];
4505
+
4506
+ details.push({ label: 'Type', value: node.data.type || 'unknown' });
4507
+ details.push({ label: 'Path', value: node.data.path || 'unknown' });
4508
+
4509
+ if (node.data.line) {
4510
+ details.push({ label: 'Line', value: node.data.line });
4511
+ }
4512
+
4513
+ details.forEach((detail, index) => {
4514
+ const item = this.createASTDetailItem(detail, index);
4515
+ this.structuredDataContent.appendChild(item);
4516
+ });
4517
+ }
4518
+
4519
+ /**
4520
+ * Create an AST data viewer item for a child node
4521
+ */
4522
+ createASTDataViewerItem(childData, index) {
4523
+ const item = document.createElement('div');
4524
+ item.className = 'ast-data-viewer-item';
4525
+ item.dataset.index = index;
4526
+
4527
+ const header = document.createElement('div');
4528
+ header.className = 'ast-data-item-header';
4529
+
4530
+ const name = document.createElement('div');
4531
+ name.className = 'ast-data-item-name';
4532
+ name.innerHTML = `${this.getNodeIcon(childData.type)} ${childData.name || 'Unknown'}`;
4533
+
4534
+ const type = document.createElement('div');
4535
+ type.className = `ast-data-item-type ${childData.type || 'unknown'}`;
4536
+ type.textContent = childData.type || 'unknown';
4537
+
4538
+ header.appendChild(name);
4539
+ header.appendChild(type);
4540
+
4541
+ const details = document.createElement('div');
4542
+ details.className = 'ast-data-item-details';
4543
+
4544
+ const detailParts = [];
4545
+
4546
+ if (childData.line) {
4547
+ detailParts.push(`<span class="ast-data-item-line">Line ${childData.line}</span>`);
4548
+ }
4549
+
4550
+ if (childData.complexity !== undefined) {
4551
+ const complexityLevel = this.getComplexityLevel(childData.complexity);
4552
+ detailParts.push(`<span class="ast-data-item-complexity">
4553
+ <span class="ast-complexity-indicator ${complexityLevel}"></span>
4554
+ Complexity: ${childData.complexity}
4555
+ </span>`);
4556
+ }
4557
+
4558
+ if (childData.docstring) {
4559
+ detailParts.push(`<div style="margin-top: 4px; font-style: italic;">${childData.docstring}</div>`);
4560
+ }
4561
+
4562
+ details.innerHTML = detailParts.join(' ');
4563
+
4564
+ item.appendChild(header);
4565
+ item.appendChild(details);
4566
+
4567
+ // Add click handler to select item
4568
+ item.addEventListener('click', () => {
4569
+ this.selectASTDataViewerItem(item);
4570
+ });
4571
+
4572
+ return item;
4573
+ }
4574
+
4575
+ /**
4576
+ * Create a detail item for simple key-value pairs
4577
+ */
4578
+ createASTDetailItem(detail, index) {
4579
+ const item = document.createElement('div');
4580
+ item.className = 'ast-data-viewer-item';
4581
+ item.dataset.index = index;
4582
+
4583
+ const header = document.createElement('div');
4584
+ header.className = 'ast-data-item-header';
4585
+
4586
+ const name = document.createElement('div');
4587
+ name.className = 'ast-data-item-name';
4588
+ name.textContent = detail.label;
4589
+
4590
+ const value = document.createElement('div');
4591
+ value.className = 'ast-data-item-details';
4592
+ value.textContent = detail.value;
4593
+
4594
+ header.appendChild(name);
4595
+ item.appendChild(header);
4596
+ item.appendChild(value);
4597
+
4598
+ return item;
4599
+ }
4600
+
4601
+ /**
4602
+ * Show empty state in structured data
4603
+ */
4604
+ showASTEmptyState(message) {
4605
+ this.structuredDataContent.innerHTML = `
4606
+ <div class="ast-data-placeholder">
4607
+ <div class="ast-placeholder-icon">📭</div>
4608
+ <div class="ast-placeholder-text">${message}</div>
4609
+ </div>
4610
+ `;
4611
+ }
4612
+
4613
+ /**
4614
+ * Select an AST data viewer item
4615
+ */
4616
+ selectASTDataViewerItem(item) {
4617
+ // Remove previous selection
4618
+ const previousSelected = this.structuredDataContent.querySelector('.ast-data-viewer-item.selected');
4619
+ if (previousSelected) {
4620
+ previousSelected.classList.remove('selected');
4621
+ }
4622
+
4623
+ // Select new item
4624
+ item.classList.add('selected');
4625
+ this.selectedASTItem = item;
4626
+ }
4627
+
4628
+ /**
4629
+ * Get icon for node type
4630
+ */
4631
+ getNodeIcon(type) {
4632
+ const icons = {
4633
+ 'directory': '📁',
4634
+ 'file': '📄',
4635
+ 'class': '🏛️',
4636
+ 'function': '⚡',
4637
+ 'method': '🔧',
4638
+ 'variable': '📦',
4639
+ 'import': '📥',
4640
+ 'module': '📦'
4641
+ };
4642
+ return icons[type] || '📄';
4643
+ }
4644
+
4645
+ /**
4646
+ * Get complexity level for styling
4647
+ */
4648
+ getComplexityLevel(complexity) {
4649
+ if (complexity <= 5) return 'low';
4650
+ if (complexity <= 10) return 'medium';
4651
+ return 'high';
4652
+ }
4653
+
4654
+ /**
4655
+ * Format file size for display
4656
+ */
4657
+ formatFileSize(bytes) {
4658
+ if (bytes === 0) return '0 B';
4659
+ const k = 1024;
4660
+ const sizes = ['B', 'KB', 'MB', 'GB'];
4661
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4662
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
4663
+ }
4664
+
4665
+ /**
4666
+ * Load source content and render with AST integration
4667
+ */
4668
+ async loadSourceContent(node, contentContainer) {
4669
+ // Try to read the file content
4670
+ const sourceContent = await this.readSourceFile(node.data.path);
4671
+ if (!sourceContent) {
4672
+ throw new Error('Could not read source file');
4673
+ }
4674
+
4675
+ // Get AST elements for this file
4676
+ const astElements = node.data.children || [];
4677
+
4678
+ // Parse and render source with AST integration
4679
+ this.renderSourceWithAST(sourceContent, astElements, contentContainer, node);
4680
+ }
4681
+
4682
+ /**
4683
+ * Read source file content
4684
+ */
4685
+ async readSourceFile(filePath) {
4686
+ try {
4687
+ console.log('📖 [SOURCE READER] Reading file:', filePath);
4688
+
4689
+ // Make API call to read the actual file content
4690
+ const response = await fetch(`/api/file/read?path=${encodeURIComponent(filePath)}`);
4691
+
4692
+ if (!response.ok) {
4693
+ const error = await response.json();
4694
+ console.error('Failed to read file:', error);
4695
+ // Fall back to placeholder for errors
4696
+ return this.generatePlaceholderSource(filePath);
4697
+ }
4698
+
4699
+ const data = await response.json();
4700
+ console.log('📖 [SOURCE READER] Read', data.lines, 'lines from', data.name);
4701
+ return data.content;
4702
+
4703
+ } catch (error) {
4704
+ console.error('Failed to read source file:', error);
4705
+ // Fall back to placeholder on error
4706
+ return this.generatePlaceholderSource(filePath);
4707
+ }
4708
+ }
4709
+
4710
+ /**
4711
+ * Generate placeholder source content for demonstration
4712
+ */
4713
+ generatePlaceholderSource(filePath) {
4714
+ const fileName = filePath.split('/').pop();
4715
+
4716
+ if (fileName.endsWith('.py')) {
4717
+ return `"""
4718
+ ${fileName}
4719
+ Generated placeholder content for demonstration
4720
+ """
4721
+
4722
+ import os
4723
+ import sys
4724
+ from typing import List, Dict, Optional
4725
+
4726
+ class ExampleClass:
4727
+ """Example class with methods."""
4728
+
4729
+ def __init__(self, name: str):
4730
+ """Initialize the example class."""
4731
+ self.name = name
4732
+ self.data = {}
4733
+
4734
+ def process_data(self, items: List[str]) -> Dict[str, int]:
4735
+ """Process a list of items and return counts."""
4736
+ result = {}
4737
+ for item in items:
4738
+ result[item] = result.get(item, 0) + 1
4739
+ return result
4740
+
4741
+ def get_summary(self) -> str:
4742
+ """Get a summary of the processed data."""
4743
+ if not self.data:
4744
+ return "No data processed"
4745
+ return f"Processed {len(self.data)} items"
4746
+
4747
+ def main():
4748
+ """Main function."""
4749
+ example = ExampleClass("demo")
4750
+ items = ["a", "b", "a", "c", "b", "a"]
4751
+ result = example.process_data(items)
4752
+ print(example.get_summary())
4753
+ return result
4754
+
4755
+ if __name__ == "__main__":
4756
+ main()
4757
+ `;
4758
+ } else {
4759
+ return `// ${fileName}
4760
+ // Generated placeholder content for demonstration
4761
+
4762
+ class ExampleClass {
4763
+ constructor(name) {
4764
+ this.name = name;
4765
+ this.data = {};
4766
+ }
4767
+
4768
+ processData(items) {
4769
+ const result = {};
4770
+ for (const item of items) {
4771
+ result[item] = (result[item] || 0) + 1;
4772
+ }
4773
+ return result;
4774
+ }
4775
+
4776
+ getSummary() {
4777
+ if (Object.keys(this.data).length === 0) {
4778
+ return "No data processed";
4779
+ }
4780
+ return \`Processed \${Object.keys(this.data).length} items\`;
4781
+ }
4782
+ }
4783
+
4784
+ function main() {
4785
+ const example = new ExampleClass("demo");
4786
+ const items = ["a", "b", "a", "c", "b", "a"];
4787
+ const result = example.processData(items);
4788
+ console.log(example.getSummary());
4789
+ return result;
4790
+ }
4791
+
4792
+ main();
4793
+ `;
4794
+ }
4795
+ }
4796
+
4797
+ /**
4798
+ * Render source code with AST integration and collapsible sections
4799
+ */
4800
+ renderSourceWithAST(sourceContent, astElements, container, node) {
4801
+ const lines = sourceContent.split('\n');
4802
+ const astMap = this.createASTLineMap(astElements);
4803
+
4804
+ console.log('🎨 [SOURCE RENDERER] Rendering source with AST:', {
4805
+ lines: lines.length,
4806
+ astElements: astElements.length,
4807
+ astMap: Object.keys(astMap).length
4808
+ });
4809
+
4810
+ // Create line elements with AST integration
4811
+ lines.forEach((line, index) => {
4812
+ const lineNumber = index + 1;
4813
+ const lineElement = this.createSourceLine(line, lineNumber, astMap[lineNumber], node);
4814
+ container.appendChild(lineElement);
4815
+ });
4816
+
4817
+ // Store reference for expand/collapse operations
4818
+ this.currentSourceContainer = container;
4819
+ this.currentASTElements = astElements;
4820
+ }
4821
+
4822
+ /**
4823
+ * Create AST line mapping for quick lookup
4824
+ */
4825
+ createASTLineMap(astElements) {
4826
+ const lineMap = {};
4827
+
4828
+ astElements.forEach(element => {
4829
+ if (element.line) {
4830
+ if (!lineMap[element.line]) {
4831
+ lineMap[element.line] = [];
4832
+ }
4833
+ lineMap[element.line].push(element);
4834
+ }
4835
+ });
4836
+
4837
+ return lineMap;
4838
+ }
4839
+
4840
+ /**
4841
+ * Create a source line element with AST integration
4842
+ */
4843
+ createSourceLine(content, lineNumber, astElements, node) {
4844
+ const lineDiv = document.createElement('div');
4845
+ lineDiv.className = 'source-line';
4846
+ lineDiv.dataset.lineNumber = lineNumber;
4847
+
4848
+ // Check if this line has AST elements
4849
+ const hasAST = astElements && astElements.length > 0;
4850
+ if (hasAST) {
4851
+ lineDiv.classList.add('ast-element');
4852
+ lineDiv.dataset.astElements = JSON.stringify(astElements);
4853
+ }
4854
+
4855
+ // Determine if this line should be collapsible
4856
+ const isCollapsible = this.isCollapsibleLine(content, astElements);
4857
+ if (isCollapsible) {
4858
+ lineDiv.classList.add('collapsible');
4859
+ }
4860
+
4861
+ // Create line number
4862
+ const lineNumberSpan = document.createElement('span');
4863
+ lineNumberSpan.className = 'line-number';
4864
+ lineNumberSpan.textContent = lineNumber;
4865
+
4866
+ // Create collapse indicator
4867
+ const collapseIndicator = document.createElement('span');
4868
+ collapseIndicator.className = 'collapse-indicator';
4869
+ if (isCollapsible) {
4870
+ collapseIndicator.classList.add('expanded');
4871
+ collapseIndicator.addEventListener('click', (e) => {
4872
+ e.stopPropagation();
4873
+ this.toggleSourceSection(lineDiv);
4874
+ });
4875
+ } else {
4876
+ collapseIndicator.classList.add('none');
4877
+ }
4878
+
4879
+ // Create line content with syntax highlighting
4880
+ const lineContentSpan = document.createElement('span');
4881
+ lineContentSpan.className = 'line-content';
4882
+ lineContentSpan.innerHTML = this.applySyntaxHighlighting(content);
4883
+
4884
+ // Add click handler for AST integration
4885
+ if (hasAST) {
4886
+ lineDiv.addEventListener('click', () => {
4887
+ this.onSourceLineClick(lineDiv, astElements, node);
4888
+ });
4889
+ }
4890
+
4891
+ lineDiv.appendChild(lineNumberSpan);
4892
+ lineDiv.appendChild(collapseIndicator);
4893
+ lineDiv.appendChild(lineContentSpan);
4894
+
4895
+ return lineDiv;
4896
+ }
4897
+
4898
+ /**
4899
+ * Check if a line should be collapsible (function/class definitions)
4900
+ */
4901
+ isCollapsibleLine(content, astElements) {
4902
+ const trimmed = content.trim();
4903
+
4904
+ // Python patterns
4905
+ if (trimmed.startsWith('def ') || trimmed.startsWith('class ') ||
4906
+ trimmed.startsWith('async def ')) {
4907
+ return true;
4908
+ }
4909
+
4910
+ // JavaScript patterns
4911
+ if (trimmed.includes('function ') || trimmed.includes('class ') ||
4912
+ trimmed.includes('=> {') || trimmed.match(/^\s*\w+\s*\([^)]*\)\s*{/)) {
4913
+ return true;
4914
+ }
4915
+
4916
+ // Check AST elements for function/class definitions
4917
+ if (astElements) {
4918
+ return astElements.some(el =>
4919
+ el.type === 'function' || el.type === 'class' ||
4920
+ el.type === 'method' || el.type === 'FunctionDef' ||
4921
+ el.type === 'ClassDef'
4922
+ );
4923
+ }
4924
+
4925
+ return false;
4926
+ }
4927
+
4928
+ /**
4929
+ * Apply basic syntax highlighting
4930
+ */
4931
+ applySyntaxHighlighting(content) {
4932
+ // First, properly escape HTML entities
4933
+ let highlighted = content
4934
+ .replace(/&/g, '&amp;')
4935
+ .replace(/</g, '&lt;')
4936
+ .replace(/>/g, '&gt;');
4937
+
4938
+ // Store markers for where we'll insert spans
4939
+ const replacements = [];
4940
+
4941
+ // Python and JavaScript keywords (combined)
4942
+ const keywords = /\b(def|class|import|from|if|else|elif|for|while|try|except|finally|with|as|return|yield|lambda|async|await|function|const|let|var|catch|export)\b/g;
4943
+
4944
+ // Find all matches first without replacing
4945
+ let match;
4946
+
4947
+ // Keywords
4948
+ while ((match = keywords.exec(highlighted)) !== null) {
4949
+ replacements.push({
4950
+ start: match.index,
4951
+ end: match.index + match[0].length,
4952
+ replacement: `<span class="keyword">${match[0]}</span>`
4953
+ });
4954
+ }
4955
+
4956
+ // Strings - simple pattern for now
4957
+ const stringPattern = /(["'`])([^"'`]*?)\1/g;
4958
+ while ((match = stringPattern.exec(highlighted)) !== null) {
4959
+ replacements.push({
4960
+ start: match.index,
4961
+ end: match.index + match[0].length,
4962
+ replacement: `<span class="string">${match[0]}</span>`
4963
+ });
4964
+ }
4965
+
4966
+ // Comments
4967
+ const commentPattern = /(#.*$|\/\/.*$)/gm;
4968
+ while ((match = commentPattern.exec(highlighted)) !== null) {
4969
+ replacements.push({
4970
+ start: match.index,
4971
+ end: match.index + match[0].length,
4972
+ replacement: `<span class="comment">${match[0]}</span>`
4973
+ });
4974
+ }
4975
+
4976
+ // Sort replacements by start position (reverse order to not mess up indices)
4977
+ replacements.sort((a, b) => b.start - a.start);
4978
+
4979
+ // Apply replacements
4980
+ for (const rep of replacements) {
4981
+ // Check for overlapping replacements and skip if needed
4982
+ const before = highlighted.substring(0, rep.start);
4983
+ const after = highlighted.substring(rep.end);
4984
+
4985
+ // Only apply if we're not inside another replacement
4986
+ if (!before.includes('<span') || before.lastIndexOf('</span>') > before.lastIndexOf('<span')) {
4987
+ highlighted = before + rep.replacement + after;
4988
+ }
4989
+ }
4990
+
4991
+ return highlighted;
4992
+ }
4993
+
4994
+ /**
4995
+ * Toggle collapse/expand of a source section
4996
+ */
4997
+ toggleSourceSection(lineElement) {
4998
+ const indicator = lineElement.querySelector('.collapse-indicator');
4999
+ const isExpanded = indicator.classList.contains('expanded');
5000
+
5001
+ if (isExpanded) {
5002
+ this.collapseSourceSection(lineElement);
5003
+ } else {
5004
+ this.expandSourceSection(lineElement);
5005
+ }
5006
+ }
5007
+
5008
+ /**
5009
+ * Collapse a source section
5010
+ */
5011
+ collapseSourceSection(lineElement) {
5012
+ const indicator = lineElement.querySelector('.collapse-indicator');
5013
+ indicator.classList.remove('expanded');
5014
+ indicator.classList.add('collapsed');
5015
+
5016
+ // Find and hide related lines (simple implementation)
5017
+ const startLine = parseInt(lineElement.dataset.lineNumber);
5018
+ const container = lineElement.parentElement;
5019
+ const lines = Array.from(container.children);
5020
+
5021
+ // Hide subsequent indented lines
5022
+ let currentIndex = lines.indexOf(lineElement) + 1;
5023
+ const baseIndent = this.getLineIndentation(lineElement.querySelector('.line-content').textContent);
5024
+
5025
+ while (currentIndex < lines.length) {
5026
+ const nextLine = lines[currentIndex];
5027
+ const nextContent = nextLine.querySelector('.line-content').textContent;
5028
+ const nextIndent = this.getLineIndentation(nextContent);
5029
+
5030
+ // Stop if we hit a line at the same or lower indentation level
5031
+ if (nextContent.trim() && nextIndent <= baseIndent) {
5032
+ break;
5033
+ }
5034
+
5035
+ nextLine.classList.add('collapsed-content');
5036
+ currentIndex++;
5037
+ }
5038
+
5039
+ // Add collapsed placeholder
5040
+ const placeholder = document.createElement('div');
5041
+ placeholder.className = 'source-line collapsed-placeholder';
5042
+ placeholder.innerHTML = `
5043
+ <span class="line-number"></span>
5044
+ <span class="collapse-indicator none"></span>
5045
+ <span class="line-content"> ... (collapsed)</span>
5046
+ `;
5047
+ lineElement.insertAdjacentElement('afterend', placeholder);
5048
+ }
5049
+
5050
+ /**
5051
+ * Expand a source section
5052
+ */
5053
+ expandSourceSection(lineElement) {
5054
+ const indicator = lineElement.querySelector('.collapse-indicator');
5055
+ indicator.classList.remove('collapsed');
5056
+ indicator.classList.add('expanded');
5057
+
5058
+ // Show hidden lines
5059
+ const container = lineElement.parentElement;
5060
+ const lines = Array.from(container.children);
5061
+
5062
+ lines.forEach(line => {
5063
+ if (line.classList.contains('collapsed-content')) {
5064
+ line.classList.remove('collapsed-content');
5065
+ }
5066
+ });
5067
+
5068
+ // Remove placeholder
5069
+ const placeholder = lineElement.nextElementSibling;
5070
+ if (placeholder && placeholder.classList.contains('collapsed-placeholder')) {
5071
+ placeholder.remove();
5072
+ }
5073
+ }
5074
+
5075
+ /**
5076
+ * Get indentation level of a line
5077
+ */
5078
+ getLineIndentation(content) {
5079
+ const match = content.match(/^(\s*)/);
5080
+ return match ? match[1].length : 0;
5081
+ }
5082
+
5083
+ /**
5084
+ * Handle click on source line with AST elements
5085
+ */
5086
+ onSourceLineClick(lineElement, astElements, node) {
5087
+ console.log('🎯 [SOURCE LINE CLICK] Line clicked:', {
5088
+ line: lineElement.dataset.lineNumber,
5089
+ astElements: astElements.length
5090
+ });
5091
+
5092
+ // Highlight the clicked line
5093
+ this.highlightSourceLine(lineElement);
5094
+
5095
+ // Show AST details for this line
5096
+ if (astElements.length > 0) {
5097
+ this.showASTElementDetails(astElements[0], node);
5098
+ }
5099
+
5100
+ // If this is a collapsible line, also toggle it
5101
+ if (lineElement.classList.contains('collapsible')) {
5102
+ this.toggleSourceSection(lineElement);
5103
+ }
5104
+ }
5105
+
5106
+ /**
5107
+ * Highlight a source line
5108
+ */
5109
+ highlightSourceLine(lineElement) {
5110
+ // Remove previous highlights
5111
+ if (this.currentSourceContainer) {
5112
+ const lines = this.currentSourceContainer.querySelectorAll('.source-line');
5113
+ lines.forEach(line => line.classList.remove('highlighted'));
5114
+ }
5115
+
5116
+ // Add highlight to clicked line
5117
+ lineElement.classList.add('highlighted');
5118
+ }
5119
+
5120
+ /**
5121
+ * Show AST element details
5122
+ */
5123
+ showASTElementDetails(astElement, node) {
5124
+ // This could open a detailed view or update another panel
5125
+ console.log('📋 [AST DETAILS] Showing details for:', astElement);
5126
+
5127
+ // For now, just log the details
5128
+ // In a full implementation, this might update a details panel
5129
+ }
5130
+
5131
+ /**
5132
+ * Expand all collapsible sections in source viewer
5133
+ */
5134
+ expandAllSource() {
5135
+ if (!this.currentSourceContainer) return;
5136
+
5137
+ const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
5138
+ collapsibleLines.forEach(line => {
5139
+ const indicator = line.querySelector('.collapse-indicator');
5140
+ if (indicator.classList.contains('collapsed')) {
5141
+ this.expandSourceSection(line);
5142
+ }
5143
+ });
5144
+ }
5145
+
5146
+ /**
5147
+ * Collapse all collapsible sections in source viewer
5148
+ */
5149
+ collapseAllSource() {
5150
+ if (!this.currentSourceContainer) return;
5151
+
5152
+ const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
5153
+ collapsibleLines.forEach(line => {
5154
+ const indicator = line.querySelector('.collapse-indicator');
5155
+ if (indicator.classList.contains('expanded')) {
5156
+ this.collapseSourceSection(line);
5157
+ }
5158
+ });
5159
+ }
3444
5160
  }
3445
5161
 
3446
5162
  // Export for use in other modules
@@ -3451,7 +5167,35 @@ document.addEventListener('DOMContentLoaded', () => {
3451
5167
  // Check if we're on a page with code tree container
3452
5168
  if (document.getElementById('code-tree-container')) {
3453
5169
  window.codeTree = new CodeTree();
3454
-
5170
+
5171
+ // Expose debug functions globally for troubleshooting
5172
+ window.debugCodeTree = {
5173
+ clearLoadingState: () => window.codeTree?.clearLoadingState(),
5174
+ showLoadingNodes: () => {
5175
+ console.log('Current loading nodes:', Array.from(window.codeTree?.loadingNodes || []));
5176
+ return Array.from(window.codeTree?.loadingNodes || []);
5177
+ },
5178
+ resetTree: () => {
5179
+ if (window.codeTree) {
5180
+ window.codeTree.clearLoadingState();
5181
+ window.codeTree.initializeTreeData();
5182
+ console.log('Tree reset complete');
5183
+ }
5184
+ },
5185
+ focusOnPath: (path) => {
5186
+ if (window.codeTree) {
5187
+ const node = window.codeTree.findD3NodeByPath(path);
5188
+ if (node) {
5189
+ window.codeTree.focusOnDirectory(node);
5190
+ console.log('Focused on:', path);
5191
+ } else {
5192
+ console.log('Node not found:', path);
5193
+ }
5194
+ }
5195
+ },
5196
+ unfocus: () => window.codeTree?.unfocusDirectory()
5197
+ };
5198
+
3455
5199
  // Listen for tab changes to initialize when code tab is selected
3456
5200
  document.addEventListener('click', (e) => {
3457
5201
  if (e.target.matches('[data-tab="code"]')) {