claude-mpm 4.2.1__py3-none-any.whl → 4.2.3__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 (57) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/agent-manager.json +1 -1
  3. claude_mpm/agents/templates/agentic_coder_optimizer.json +1 -1
  4. claude_mpm/agents/templates/api_qa.json +1 -1
  5. claude_mpm/agents/templates/code_analyzer.json +1 -1
  6. claude_mpm/agents/templates/data_engineer.json +1 -1
  7. claude_mpm/agents/templates/documentation.json +1 -1
  8. claude_mpm/agents/templates/engineer.json +2 -2
  9. claude_mpm/agents/templates/gcp_ops_agent.json +14 -9
  10. claude_mpm/agents/templates/imagemagick.json +1 -1
  11. claude_mpm/agents/templates/memory_manager.json +1 -1
  12. claude_mpm/agents/templates/ops.json +1 -1
  13. claude_mpm/agents/templates/project_organizer.json +1 -1
  14. claude_mpm/agents/templates/qa.json +2 -2
  15. claude_mpm/agents/templates/refactoring_engineer.json +1 -1
  16. claude_mpm/agents/templates/research.json +3 -3
  17. claude_mpm/agents/templates/security.json +1 -1
  18. claude_mpm/agents/templates/test-non-mpm.json +20 -0
  19. claude_mpm/agents/templates/ticketing.json +1 -1
  20. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  21. claude_mpm/agents/templates/version_control.json +1 -1
  22. claude_mpm/agents/templates/web_qa.json +3 -8
  23. claude_mpm/agents/templates/web_ui.json +1 -1
  24. claude_mpm/cli/commands/agents.py +3 -0
  25. claude_mpm/cli/commands/dashboard.py +3 -3
  26. claude_mpm/cli/commands/monitor.py +227 -64
  27. claude_mpm/core/config.py +25 -0
  28. claude_mpm/core/unified_agent_registry.py +2 -2
  29. claude_mpm/dashboard/static/css/code-tree.css +220 -1
  30. claude_mpm/dashboard/static/css/dashboard.css +286 -0
  31. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  32. claude_mpm/dashboard/static/js/components/code-simple.js +507 -15
  33. claude_mpm/dashboard/static/js/components/code-tree.js +2044 -124
  34. claude_mpm/dashboard/static/js/socket-client.js +5 -2
  35. claude_mpm/dashboard/templates/code_simple.html +79 -0
  36. claude_mpm/dashboard/templates/index.html +42 -41
  37. claude_mpm/services/agents/deployment/agent_deployment.py +4 -1
  38. claude_mpm/services/agents/deployment/agent_discovery_service.py +101 -2
  39. claude_mpm/services/agents/deployment/agent_format_converter.py +53 -9
  40. claude_mpm/services/agents/deployment/agent_template_builder.py +355 -25
  41. claude_mpm/services/agents/deployment/agent_validator.py +11 -6
  42. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +83 -15
  43. claude_mpm/services/agents/deployment/validation/template_validator.py +51 -40
  44. claude_mpm/services/cli/agent_listing_service.py +2 -2
  45. claude_mpm/services/dashboard/stable_server.py +389 -0
  46. claude_mpm/services/socketio/client_proxy.py +16 -0
  47. claude_mpm/services/socketio/dashboard_server.py +360 -0
  48. claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
  49. claude_mpm/services/socketio/monitor_client.py +366 -0
  50. claude_mpm/services/socketio/monitor_server.py +505 -0
  51. claude_mpm/tools/code_tree_analyzer.py +95 -17
  52. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/METADATA +1 -1
  53. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/RECORD +57 -52
  54. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/WHEEL +0 -0
  55. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/entry_points.txt +0 -0
  56. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/licenses/LICENSE +0 -0
  57. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/top_level.txt +0 -0
@@ -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,
@@ -440,15 +448,43 @@ class CodeTree {
440
448
  */
441
449
  subscribeToEvents() {
442
450
  if (!this.socket) {
443
- if (window.socket) {
451
+ // CRITICAL FIX: Create our own socket connection if no shared socket exists
452
+ // This ensures the tree view has a working WebSocket connection
453
+ if (window.socket && window.socket.connected) {
454
+ console.log('[CodeTree] Using existing global socket');
444
455
  this.socket = window.socket;
445
456
  this.setupEventHandlers();
446
- } else if (window.dashboard?.socketClient?.socket) {
457
+ } else if (window.dashboard?.socketClient?.socket && window.dashboard.socketClient.socket.connected) {
458
+ console.log('[CodeTree] Using dashboard socket');
447
459
  this.socket = window.dashboard.socketClient.socket;
448
460
  this.setupEventHandlers();
449
- } else if (window.socketClient?.socket) {
461
+ } else if (window.socketClient?.socket && window.socketClient.socket.connected) {
462
+ console.log('[CodeTree] Using socketClient socket');
450
463
  this.socket = window.socketClient.socket;
451
464
  this.setupEventHandlers();
465
+ } else if (window.io) {
466
+ // Create our own socket connection like the simple view does
467
+ console.log('[CodeTree] Creating new socket connection');
468
+ try {
469
+ this.socket = io('/');
470
+
471
+ this.socket.on('connect', () => {
472
+ console.log('[CodeTree] Socket connected successfully');
473
+ this.setupEventHandlers();
474
+ });
475
+
476
+ this.socket.on('disconnect', () => {
477
+ console.log('[CodeTree] Socket disconnected');
478
+ });
479
+
480
+ this.socket.on('connect_error', (error) => {
481
+ console.error('[CodeTree] Socket connection error:', error);
482
+ });
483
+ } catch (error) {
484
+ console.error('[CodeTree] Failed to create socket connection:', error);
485
+ }
486
+ } else {
487
+ console.error('[CodeTree] Socket.IO not available - cannot subscribe to events');
452
488
  }
453
489
  }
454
490
  }
@@ -503,7 +539,7 @@ class CodeTree {
503
539
  const dirName = workingDir.split('/').pop() || 'Project Root';
504
540
  this.treeData = {
505
541
  name: dirName,
506
- path: '.', // Use '.' for root to maintain consistency with relative path handling
542
+ path: workingDir, // Use absolute path for consistency with API expectations
507
543
  type: 'root',
508
544
  children: [],
509
545
  loaded: false,
@@ -521,10 +557,7 @@ class CodeTree {
521
557
  this.updateBreadcrumb(`Discovering structure in ${dirName}...`, 'info');
522
558
 
523
559
  // Get selected languages from checkboxes
524
- const selectedLanguages = [];
525
- document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
526
- selectedLanguages.push(cb.value);
527
- });
560
+ const selectedLanguages = this.getSelectedLanguages();
528
561
 
529
562
  // Get ignore patterns
530
563
  const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
@@ -615,7 +648,37 @@ class CodeTree {
615
648
  .attr('title', 'Toggle between radial and linear layouts')
616
649
  .text('◎')
617
650
  .on('click', () => this.toggleLayout());
618
-
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
+
619
682
  // Path Search
620
683
  const searchInput = toolbar.append('input')
621
684
  .attr('class', 'tree-control-btn')
@@ -920,11 +983,44 @@ class CodeTree {
920
983
  this.socket.on('code:top_level:discovered', (data) => this.onTopLevelDiscovered(data));
921
984
  this.socket.on('code:directory:discovered', (data) => this.onDirectoryDiscovered(data));
922
985
  this.socket.on('code:file:discovered', (data) => this.onFileDiscovered(data));
923
- 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
+ });
924
990
  this.socket.on('code:node:found', (data) => this.onNodeFound(data));
925
991
 
926
992
  // Progress updates
927
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
+ });
928
1024
 
929
1025
  // Lazy loading responses
930
1026
  this.socket.on('code:directory:contents', (data) => {
@@ -993,13 +1089,16 @@ class CodeTree {
993
1089
  // Find the D3 node again after hierarchy recreation
994
1090
  const updatedD3Node = this.findD3NodeByPath(searchPath);
995
1091
  if (updatedD3Node) {
996
- // Ensure children are visible (not collapsed)
997
- updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
998
- updatedD3Node._children = null;
999
- updatedD3Node.data.expanded = true;
1092
+ // D3.hierarchy already creates the children - just ensure visible
1093
+ if (updatedD3Node.children && updatedD3Node.children.length > 0) {
1094
+ updatedD3Node._children = null;
1095
+ updatedD3Node.data.expanded = true;
1096
+ console.log('✅ [D3 UPDATE] Node expanded after loading:', searchPath);
1097
+ }
1000
1098
  }
1001
1099
 
1002
- this.update(this.root);
1100
+ // Update with the specific node for smooth animation
1101
+ this.update(updatedD3Node || this.root);
1003
1102
  }
1004
1103
 
1005
1104
  // Update stats based on discovered contents
@@ -1105,10 +1204,11 @@ class CodeTree {
1105
1204
  // Add to events display
1106
1205
  this.addEventToDisplay(`📁 Found ${(data.items || []).length} top-level items in project root`, 'info');
1107
1206
 
1108
- // The root node (with path '.') should receive the children
1109
- const rootNode = this.findNodeByPath('.');
1110
-
1111
- 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 ? {
1112
1212
  name: rootNode.name,
1113
1213
  path: rootNode.path,
1114
1214
  currentChildren: rootNode.children ? rootNode.children.length : 0
@@ -1119,16 +1219,18 @@ class CodeTree {
1119
1219
 
1120
1220
  // Update the root node with discovered children
1121
1221
  rootNode.children = data.items.map(child => {
1122
- // Items at root level get their name as the path
1123
- const childPath = child.name;
1124
-
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
+
1125
1227
  console.log(` Adding child: ${child.name} with path: ${childPath}`);
1126
-
1228
+
1127
1229
  return {
1128
1230
  name: child.name,
1129
- path: childPath, // Just the name for top-level items
1231
+ path: childPath, // Use absolute path for consistency
1130
1232
  type: child.type,
1131
- loaded: child.type === 'directory' ? false : undefined,
1233
+ loaded: child.type === 'directory' ? false : undefined, // Explicitly false for directories
1132
1234
  analyzed: child.type === 'file' ? false : undefined,
1133
1235
  expanded: false,
1134
1236
  children: child.type === 'directory' ? [] : undefined,
@@ -1142,10 +1244,30 @@ class CodeTree {
1142
1244
 
1143
1245
  // Update D3 hierarchy and render
1144
1246
  if (this.root && this.svg) {
1145
- // Recreate hierarchy with new data
1146
- this.root = d3.hierarchy(this.treeData);
1147
- this.root.x0 = this.height / 2;
1148
- this.root.y0 = 0;
1247
+ // CRITICAL FIX: Preserve existing D3 node structure when possible
1248
+ // Instead of recreating the entire hierarchy, update the existing root
1249
+ if (this.root.data === this.treeData) {
1250
+ // Same root data object - update children in place
1251
+ console.log('📊 Updating existing D3 tree structure');
1252
+
1253
+ // Create D3 hierarchy nodes for the new children
1254
+ this.root.children = rootNode.children.map(childData => {
1255
+ const childNode = d3.hierarchy(childData);
1256
+ childNode.parent = this.root;
1257
+ childNode.depth = 1;
1258
+ return childNode;
1259
+ });
1260
+
1261
+ // Ensure root is marked as expanded
1262
+ this.root._children = null;
1263
+ this.root.data.expanded = true;
1264
+ } else {
1265
+ // Different root - need to recreate
1266
+ console.log('🔄 Recreating D3 tree structure');
1267
+ this.root = d3.hierarchy(this.treeData);
1268
+ this.root.x0 = this.height / 2;
1269
+ this.root.y0 = 0;
1270
+ }
1149
1271
 
1150
1272
  // Update the tree visualization
1151
1273
  this.update(this.root);
@@ -1297,15 +1419,34 @@ class CodeTree {
1297
1419
 
1298
1420
  // Find the D3 node again after hierarchy recreation
1299
1421
  const updatedD3Node = this.findD3NodeByPath(searchPath);
1300
- if (updatedD3Node && updatedD3Node.data.children && updatedD3Node.data.children.length > 0) {
1301
- // Ensure the node is expanded to show children
1302
- updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
1303
- updatedD3Node._children = null;
1304
- // Mark data as expanded
1305
- updatedD3Node.data.expanded = true;
1422
+ if (updatedD3Node) {
1423
+ // CRITICAL FIX: D3.hierarchy() creates nodes with children already set
1424
+ // We just need to ensure they're not hidden in _children
1425
+ // When d3.hierarchy creates the tree, it puts all children in the 'children' array
1426
+
1427
+ // If the node has children from d3.hierarchy, make sure they're visible
1428
+ if (updatedD3Node.children && updatedD3Node.children.length > 0) {
1429
+ // Children are already there from d3.hierarchy - just ensure not hidden
1430
+ updatedD3Node._children = null;
1431
+ updatedD3Node.data.expanded = true;
1432
+
1433
+ console.log('✅ [D3 UPDATE] Node expanded with children:', {
1434
+ path: searchPath,
1435
+ d3ChildrenCount: updatedD3Node.children.length,
1436
+ dataChildrenCount: updatedD3Node.data.children ? updatedD3Node.data.children.length : 0,
1437
+ childPaths: updatedD3Node.children.map(c => c.data.path)
1438
+ });
1439
+ } else if (!updatedD3Node.children && updatedD3Node.data.children && updatedD3Node.data.children.length > 0) {
1440
+ // This shouldn't happen if d3.hierarchy is working correctly
1441
+ console.error('⚠️ [D3 UPDATE] Data has children but D3 node does not!', {
1442
+ path: searchPath,
1443
+ dataChildren: updatedD3Node.data.children
1444
+ });
1445
+ }
1306
1446
  }
1307
1447
 
1308
- this.update(this.root);
1448
+ // Force update with the source node for smooth animation
1449
+ this.update(updatedD3Node || this.root);
1309
1450
  }
1310
1451
 
1311
1452
  // Provide better feedback for empty vs populated directories
@@ -1400,6 +1541,45 @@ class CodeTree {
1400
1541
  * Handle file analyzed event
1401
1542
  */
1402
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
+
1403
1583
  // Remove loading pulse if this file was being analyzed
1404
1584
  const d3Node = this.findD3NodeByPath(data.path);
1405
1585
  if (d3Node && this.loadingNodes.has(data.path)) {
@@ -1414,13 +1594,14 @@ class CodeTree {
1414
1594
 
1415
1595
  const fileNode = this.findNodeByPath(data.path);
1416
1596
  if (fileNode) {
1597
+ console.log('🔍 [FILE NODE] Found file node for:', data.path);
1417
1598
  fileNode.analyzed = true;
1418
1599
  fileNode.complexity = data.complexity || 0;
1419
1600
  fileNode.lines = data.lines || 0;
1420
-
1601
+
1421
1602
  // Add code elements as children
1422
1603
  if (data.elements && Array.isArray(data.elements)) {
1423
- fileNode.children = data.elements.map(elem => ({
1604
+ const children = data.elements.map(elem => ({
1424
1605
  name: elem.name,
1425
1606
  type: elem.type.toLowerCase(),
1426
1607
  path: `${data.path}#${elem.name}`,
@@ -1436,8 +1617,17 @@ class CodeTree {
1436
1617
  docstring: m.docstring || ''
1437
1618
  })) : []
1438
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');
1439
1629
  }
1440
-
1630
+
1441
1631
  // Update stats
1442
1632
  if (data.stats) {
1443
1633
  this.stats.classes += data.stats.classes || 0;
@@ -1445,13 +1635,48 @@ class CodeTree {
1445
1635
  this.stats.methods += data.stats.methods || 0;
1446
1636
  this.stats.lines += data.stats.lines || 0;
1447
1637
  }
1448
-
1638
+
1449
1639
  this.updateStats();
1450
- 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) {
1451
1674
  this.update(this.root);
1452
1675
  }
1453
-
1676
+
1454
1677
  this.updateBreadcrumb(`Analyzed: ${data.path}`, 'success');
1678
+ } else {
1679
+ console.error('❌ [FILE NODE] Could not find file node for path:', data.path);
1455
1680
  }
1456
1681
  }
1457
1682
 
@@ -1895,12 +2120,12 @@ class CodeTree {
1895
2120
  * Update statistics display
1896
2121
  */
1897
2122
  updateStats() {
1898
- // Update stats display - use correct IDs from HTML
2123
+ // Update stats display - use correct IDs from corner controls
1899
2124
  const statsElements = {
1900
- 'file-count': this.stats.files,
1901
- 'class-count': this.stats.classes,
1902
- 'function-count': this.stats.functions,
1903
- '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
1904
2129
  };
1905
2130
 
1906
2131
  for (const [id, value] of Object.entries(statsElements)) {
@@ -1931,6 +2156,145 @@ class CodeTree {
1931
2156
  }
1932
2157
  }
1933
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
+
1934
2298
  /**
1935
2299
  * Detect language from file extension
1936
2300
  */
@@ -2012,6 +2376,172 @@ class CodeTree {
2012
2376
  return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
2013
2377
  }
2014
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
+
2015
2545
  /**
2016
2546
  * Update D3 tree visualization
2017
2547
  */
@@ -2025,6 +2555,9 @@ class CodeTree {
2025
2555
  const nodes = treeData.descendants();
2026
2556
  const links = treeData.descendants().slice(1);
2027
2557
 
2558
+ // Apply horizontal layout for singleton chains
2559
+ this.applySingletonHorizontalLayout(nodes);
2560
+
2028
2561
  if (this.isRadialLayout) {
2029
2562
  // Radial layout adjustments
2030
2563
  nodes.forEach(d => {
@@ -2103,20 +2636,42 @@ class CodeTree {
2103
2636
 
2104
2637
  // Add labels for nodes with smart positioning
2105
2638
  nodeEnter.append('text')
2106
- .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
+ })
2107
2648
  .attr('dy', '.35em')
2108
2649
  .attr('x', d => {
2109
2650
  if (this.isRadialLayout) {
2110
2651
  // For radial layout, initial position
2111
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;
2112
2659
  } else {
2113
2660
  // Linear layout: standard positioning
2661
+ console.log(`📝 [TEXT] Positioning vertical text for: ${d.data.name} (depth: ${d.depth}, path: ${d.data.path})`);
2114
2662
  return d.children || d._children ? -13 : 13;
2115
2663
  }
2116
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
+ })
2117
2669
  .attr('text-anchor', d => {
2118
2670
  if (this.isRadialLayout) {
2119
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';
2120
2675
  } else {
2121
2676
  // Linear layout: standard anchoring
2122
2677
  return d.children || d._children ? 'end' : 'start';
@@ -2133,6 +2688,22 @@ class CodeTree {
2133
2688
  .style('font-size', '12px')
2134
2689
  .style('font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif')
2135
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
+ })
2136
2707
  .on('click', (event, d) => this.onNodeClick(event, d)) // CRITICAL FIX: Add click handler to labels
2137
2708
  .style('cursor', 'pointer');
2138
2709
 
@@ -2171,6 +2742,10 @@ class CodeTree {
2171
2742
  // CRITICAL FIX: Ensure ALL nodes (new and existing) have click handlers
2172
2743
  // This fixes the issue where subdirectory clicks stop working after tree updates
2173
2744
  nodeUpdate.on('click', (event, d) => this.onNodeClick(event, d));
2745
+
2746
+ // ADDITIONAL FIX: Also ensure click handlers on all child elements
2747
+ nodeUpdate.selectAll('circle').on('click', (event, d) => this.onNodeClick(event, d));
2748
+ nodeUpdate.selectAll('text').on('click', (event, d) => this.onNodeClick(event, d));
2174
2749
 
2175
2750
  nodeUpdate.transition()
2176
2751
  .duration(this.duration)
@@ -2227,6 +2802,7 @@ class CodeTree {
2227
2802
 
2228
2803
  // Update text labels with proper rotation for radial layout
2229
2804
  const isRadial = this.isRadialLayout; // Capture the layout type
2805
+ const horizontalNodes = this.horizontalNodes; // Capture horizontal nodes set
2230
2806
  nodeUpdate.select('text.node-label')
2231
2807
  .style('fill-opacity', 1)
2232
2808
  .style('fill', '#333')
@@ -2257,12 +2833,26 @@ class CodeTree {
2257
2833
  .attr('dy', '.35em');
2258
2834
  }
2259
2835
  } else {
2260
- // Linear layout - no rotation needed
2261
- selection
2262
- .attr('transform', null)
2263
- .attr('x', d.children || d._children ? -13 : 13)
2264
- .attr('text-anchor', d.children || d._children ? 'end' : 'start')
2265
- .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
+ }
2266
2856
  }
2267
2857
  });
2268
2858
 
@@ -2330,6 +2920,14 @@ class CodeTree {
2330
2920
  d.x0 = d.x;
2331
2921
  d.y0 = d.y;
2332
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
+ }
2333
2931
  }
2334
2932
 
2335
2933
  /**
@@ -2448,10 +3046,11 @@ class CodeTree {
2448
3046
 
2449
3047
  /**
2450
3048
  * Remove pulsing animation when loading complete
3049
+ * Note: This function only handles visual animation removal.
3050
+ * The caller is responsible for managing the loadingNodes Set.
2451
3051
  */
2452
3052
  removeLoadingPulse(d) {
2453
- // Remove from loading set
2454
- this.loadingNodes.delete(d.data.path);
3053
+ // Note: loadingNodes.delete() is handled by the caller for explicit control
2455
3054
 
2456
3055
  // Use consistent selection pattern
2457
3056
  const node = this.treeGroup.selectAll('g.node')
@@ -2503,6 +3102,21 @@ class CodeTree {
2503
3102
  * Handle node click - implement lazy loading with enhanced visual feedback
2504
3103
  */
2505
3104
  onNodeClick(event, d) {
3105
+ const clickId = Date.now() + Math.random();
3106
+ // DEBUG: Log all clicks to verify handler is working
3107
+ console.log(`🖱️ [NODE CLICK] Clicked on node (ID: ${clickId}):`, {
3108
+ name: d?.data?.name,
3109
+ path: d?.data?.path,
3110
+ type: d?.data?.type,
3111
+ loaded: d?.data?.loaded,
3112
+ hasChildren: !!(d?.children || d?._children),
3113
+ dataChildren: d?.data?.children?.length || 0,
3114
+ loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined'
3115
+ });
3116
+
3117
+ // Update structured data with clicked node
3118
+ this.updateStructuredData(d);
3119
+
2506
3120
  // Handle node click interaction
2507
3121
 
2508
3122
  // Check event parameter
@@ -2601,11 +3215,8 @@ class CodeTree {
2601
3215
 
2602
3216
 
2603
3217
  // Get selected languages from checkboxes
2604
- const selectedLanguages = [];
2605
- const checkboxes = document.querySelectorAll('.language-checkbox:checked');
2606
- checkboxes.forEach(cb => {
2607
- selectedLanguages.push(cb.value);
2608
- });
3218
+ const selectedLanguages = this.getSelectedLanguages();
3219
+ console.log('🔍 [LANGUAGE] Selected languages:', selectedLanguages);
2609
3220
 
2610
3221
  // Get ignore patterns
2611
3222
  const ignorePatternsElement = document.getElementById('ignore-patterns');
@@ -2616,13 +3227,54 @@ class CodeTree {
2616
3227
  // Add a small delay to ensure visual effects are rendered first
2617
3228
 
2618
3229
  // For directories that haven't been loaded yet, request discovery
3230
+ console.log('🔍 [LOAD CHECK]', {
3231
+ type: d.data.type,
3232
+ loaded: d.data.loaded,
3233
+ loadedType: typeof d.data.loaded,
3234
+ isDirectory: d.data.type === 'directory',
3235
+ notLoaded: !d.data.loaded,
3236
+ shouldLoad: d.data.type === 'directory' && !d.data.loaded
3237
+ });
2619
3238
  if (d.data.type === 'directory' && !d.data.loaded) {
2620
- // Prevent duplicate requests
2621
- if (this.loadingNodes.has(d.data.path)) {
2622
- this.showNotification(`Already loading: ${d.data.name}`, 'warning');
2623
- 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');
2624
3274
  }
2625
-
3275
+
3276
+ console.log('✅ [SUBDIRECTORY LOADING] No duplicate request, proceeding to mark as loading');
3277
+
2626
3278
  // Mark as loading immediately to prevent duplicate requests
2627
3279
  d.data.loaded = 'loading';
2628
3280
  this.loadingNodes.add(d.data.path);
@@ -2646,30 +3298,134 @@ class CodeTree {
2646
3298
  const clickedD3Node = d;
2647
3299
 
2648
3300
  // Delay the socket request to ensure visual effects are rendered
3301
+ // Use arrow function to preserve 'this' context
2649
3302
  setTimeout(() => {
2650
3303
 
2651
- // Request directory contents via Socket.IO
2652
- if (this.socket) {
2653
- console.log('📡 [SUBDIRECTORY LOADING] Emitting WebSocket request:', {
2654
- event: 'code:discover:directory',
2655
- data: {
2656
- path: fullPath,
2657
- depth: this.bulkLoadMode ? 2 : 1,
2658
- languages: selectedLanguages,
2659
- ignore_patterns: ignorePatterns
2660
- }
2661
- });
2662
-
2663
- this.socket.emit('code:discover:directory', {
2664
- path: fullPath,
2665
- depth: this.bulkLoadMode ? 2 : 1, // Load 2 levels if bulk mode enabled
2666
- languages: selectedLanguages,
2667
- ignore_patterns: ignorePatterns
3304
+ // CRITICAL FIX: Use REST API instead of WebSocket for reliability
3305
+ // The simple view works because it uses REST API, so let's do the same
3306
+ console.log('📡 [SUBDIRECTORY LOADING] Using REST API for directory:', {
3307
+ originalPath: d.data.path,
3308
+ fullPath: fullPath,
3309
+ apiUrl: `${window.location.origin}/api/directory/list?path=${encodeURIComponent(fullPath)}`,
3310
+ loadingNodesSize: this.loadingNodes.size,
3311
+ loadingNodesContent: Array.from(this.loadingNodes)
3312
+ });
3313
+
3314
+ const apiUrl = `${window.location.origin}/api/directory/list?path=${encodeURIComponent(fullPath)}`;
3315
+
3316
+ fetch(apiUrl)
3317
+ .then(response => {
3318
+ if (!response.ok) {
3319
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
3320
+ }
3321
+ return response.json();
3322
+ })
3323
+ .then(data => {
3324
+ console.log('✅ [SUBDIRECTORY LOADING] REST API response:', {
3325
+ data: data,
3326
+ pathToDelete: d.data.path,
3327
+ loadingNodesBefore: Array.from(this.loadingNodes)
3328
+ });
3329
+
3330
+ // Remove from loading set
3331
+ const deleted = this.loadingNodes.delete(d.data.path);
3332
+ d.data.loaded = true;
3333
+
3334
+ console.log('🧹 [SUBDIRECTORY LOADING] Cleanup result:', {
3335
+ pathDeleted: d.data.path,
3336
+ wasDeleted: deleted,
3337
+ loadingNodesAfter: Array.from(this.loadingNodes)
3338
+ });
3339
+
3340
+ // Remove loading animation
3341
+ const d3Node = this.findD3NodeByPath(d.data.path);
3342
+ if (d3Node) {
3343
+ this.removeLoadingPulse(d3Node);
3344
+ }
3345
+
3346
+ // Process the directory contents
3347
+ if (data.exists && data.is_directory && data.contents) {
3348
+ const node = this.findNodeByPath(d.data.path);
3349
+ if (node) {
3350
+ console.log('🔧 [SUBDIRECTORY LOADING] Creating children with paths:',
3351
+ data.contents.map(item => ({ name: item.name, path: item.path })));
3352
+
3353
+ // Add children to the node
3354
+ node.children = data.contents.map(item => ({
3355
+ name: item.name,
3356
+ path: item.path, // Use the full path from API response
3357
+ type: item.is_directory ? 'directory' : 'file',
3358
+ loaded: item.is_directory ? false : undefined,
3359
+ analyzed: !item.is_directory ? false : undefined,
3360
+ expanded: false,
3361
+ children: item.is_directory ? [] : undefined
3362
+ }));
3363
+ node.loaded = true;
3364
+ node.expanded = true;
3365
+
3366
+ // Update D3 hierarchy
3367
+ if (this.root && this.svg) {
3368
+ const oldRoot = this.root;
3369
+ this.root = d3.hierarchy(this.treeData);
3370
+ this.root.x0 = this.height / 2;
3371
+ this.root.y0 = 0;
3372
+
3373
+ this.preserveExpansionState(oldRoot, this.root);
3374
+
3375
+ const updatedD3Node = this.findD3NodeByPath(d.data.path);
3376
+ if (updatedD3Node && updatedD3Node.children && updatedD3Node.children.length > 0) {
3377
+ updatedD3Node._children = null;
3378
+ updatedD3Node.data.expanded = true;
3379
+ }
3380
+
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
+ }
3389
+ }
3390
+
3391
+ this.updateBreadcrumb(`Loaded ${data.contents.length} items`, 'success');
3392
+ this.showNotification(`Loaded ${data.contents.length} items from ${d.data.name}`, 'success');
3393
+ }
3394
+ } else {
3395
+ this.showNotification(`Directory ${d.data.name} is empty or inaccessible`, 'warning');
3396
+ }
3397
+ })
3398
+ .catch(error => {
3399
+ console.error('❌ [SUBDIRECTORY LOADING] REST API error:', {
3400
+ error: error.message,
3401
+ stack: error.stack,
3402
+ pathToDelete: d.data.path,
3403
+ loadingNodesBefore: Array.from(this.loadingNodes)
3404
+ });
3405
+
3406
+ // Clean up loading state
3407
+ const deleted = this.loadingNodes.delete(d.data.path);
3408
+ d.data.loaded = false;
3409
+
3410
+ console.log('🧹 [SUBDIRECTORY LOADING] Error cleanup:', {
3411
+ pathDeleted: d.data.path,
3412
+ wasDeleted: deleted,
3413
+ loadingNodesAfter: Array.from(this.loadingNodes)
3414
+ });
3415
+
3416
+ const d3Node = this.findD3NodeByPath(d.data.path);
3417
+ if (d3Node) {
3418
+ this.removeLoadingPulse(d3Node);
3419
+ }
3420
+
3421
+ this.showNotification(`Failed to load ${d.data.name}: ${error.message}`, 'error');
2668
3422
  });
2669
-
2670
- this.updateBreadcrumb(`Loading ${d.data.name}...`, 'info');
2671
- this.showNotification(`Loading directory: ${d.data.name}`, 'info');
2672
- } else {
3423
+
3424
+ this.updateBreadcrumb(`Loading ${d.data.name}...`, 'info');
3425
+ this.showNotification(`Loading directory: ${d.data.name}`, 'info');
3426
+
3427
+ // Keep the original else clause for when fetch isn't available
3428
+ if (!window.fetch) {
2673
3429
  console.error('❌ [SUBDIRECTORY LOADING] No WebSocket connection available!');
2674
3430
  this.showNotification(`Cannot load directory: No connection`, 'error');
2675
3431
 
@@ -2683,12 +3439,37 @@ class CodeTree {
2683
3439
  d.data.loaded = false;
2684
3440
  }
2685
3441
  }, 100); // 100ms delay to ensure visual effects render first
2686
- }
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
+ }
2687
3453
  // For files that haven't been analyzed, request analysis
2688
3454
  else if (d.data.type === 'file' && !d.data.analyzed) {
2689
3455
  // Only analyze files of selected languages
2690
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
+
2691
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
+ });
2692
3473
  this.showNotification(`Skipping ${d.data.name} - ${fileLanguage} not selected`, 'warning');
2693
3474
  return;
2694
3475
  }
@@ -2704,14 +3485,43 @@ class CodeTree {
2704
3485
 
2705
3486
  // Delay the socket request to ensure visual effects are rendered
2706
3487
  setTimeout(() => {
2707
-
2708
- 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
+
2709
3504
  this.socket.emit('code:analyze:file', {
2710
3505
  path: fullPath
2711
3506
  });
2712
-
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');
2713
3522
  this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
2714
3523
  this.showNotification(`Analyzing: ${d.data.name}`, 'info');
3524
+ this.analyzeFileHTTP(fullPath, d.data.name, d3.select(event.target.closest('g')));
2715
3525
  }
2716
3526
  }, 100); // 100ms delay to ensure visual effects render first
2717
3527
  }
@@ -3027,15 +3837,295 @@ class CodeTree {
3027
3837
  this.showNotification('All nodes collapsed', 'info');
3028
3838
  }
3029
3839
 
3840
+ /**
3841
+ * Focus on a specific directory, hiding parent directories and showing only its contents
3842
+ */
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
+
3030
3934
  /**
3031
3935
  * Reset zoom to fit the tree
3032
3936
  */
3033
3937
  resetZoom() {
3034
- // DISABLED: All zoom reset operations have been disabled to prevent tree centering/movement
3035
- // The tree should remain stationary and not center/move when interacting with nodes
3036
- console.log('[CodeTree] resetZoom called but disabled - no zoom reset will occur');
3037
- this.showNotification('Zoom reset disabled - tree remains stationary', 'info');
3038
- return;
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
+ }
3039
4129
  }
3040
4130
 
3041
4131
  /**
@@ -3197,6 +4287,35 @@ class CodeTree {
3197
4287
  }
3198
4288
  }
3199
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
+
3200
4319
  /**
3201
4320
  * Export tree data
3202
4321
  */
@@ -3241,7 +4360,7 @@ class CodeTree {
3241
4360
  if (ticker) {
3242
4361
  ticker.textContent = message;
3243
4362
  ticker.className = `ticker ticker-${type}`;
3244
-
4363
+
3245
4364
  // Auto-hide after 5 seconds for non-error messages
3246
4365
  if (type !== 'error') {
3247
4366
  setTimeout(() => {
@@ -3254,6 +4373,779 @@ class CodeTree {
3254
4373
  }
3255
4374
  }
3256
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
+ // For now, we'll use a placeholder since we don't have direct file access
4688
+ // In a real implementation, this would make an API call to read the file
4689
+ console.log('📖 [SOURCE READER] Would read file:', filePath);
4690
+
4691
+ // Return placeholder content for demonstration
4692
+ return this.generatePlaceholderSource(filePath);
4693
+ } catch (error) {
4694
+ console.error('Failed to read source file:', error);
4695
+ return null;
4696
+ }
4697
+ }
4698
+
4699
+ /**
4700
+ * Generate placeholder source content for demonstration
4701
+ */
4702
+ generatePlaceholderSource(filePath) {
4703
+ const fileName = filePath.split('/').pop();
4704
+
4705
+ if (fileName.endsWith('.py')) {
4706
+ return `"""
4707
+ ${fileName}
4708
+ Generated placeholder content for demonstration
4709
+ """
4710
+
4711
+ import os
4712
+ import sys
4713
+ from typing import List, Dict, Optional
4714
+
4715
+ class ExampleClass:
4716
+ """Example class with methods."""
4717
+
4718
+ def __init__(self, name: str):
4719
+ """Initialize the example class."""
4720
+ self.name = name
4721
+ self.data = {}
4722
+
4723
+ def process_data(self, items: List[str]) -> Dict[str, int]:
4724
+ """Process a list of items and return counts."""
4725
+ result = {}
4726
+ for item in items:
4727
+ result[item] = result.get(item, 0) + 1
4728
+ return result
4729
+
4730
+ def get_summary(self) -> str:
4731
+ """Get a summary of the processed data."""
4732
+ if not self.data:
4733
+ return "No data processed"
4734
+ return f"Processed {len(self.data)} items"
4735
+
4736
+ def main():
4737
+ """Main function."""
4738
+ example = ExampleClass("demo")
4739
+ items = ["a", "b", "a", "c", "b", "a"]
4740
+ result = example.process_data(items)
4741
+ print(example.get_summary())
4742
+ return result
4743
+
4744
+ if __name__ == "__main__":
4745
+ main()
4746
+ `;
4747
+ } else {
4748
+ return `// ${fileName}
4749
+ // Generated placeholder content for demonstration
4750
+
4751
+ class ExampleClass {
4752
+ constructor(name) {
4753
+ this.name = name;
4754
+ this.data = {};
4755
+ }
4756
+
4757
+ processData(items) {
4758
+ const result = {};
4759
+ for (const item of items) {
4760
+ result[item] = (result[item] || 0) + 1;
4761
+ }
4762
+ return result;
4763
+ }
4764
+
4765
+ getSummary() {
4766
+ if (Object.keys(this.data).length === 0) {
4767
+ return "No data processed";
4768
+ }
4769
+ return \`Processed \${Object.keys(this.data).length} items\`;
4770
+ }
4771
+ }
4772
+
4773
+ function main() {
4774
+ const example = new ExampleClass("demo");
4775
+ const items = ["a", "b", "a", "c", "b", "a"];
4776
+ const result = example.processData(items);
4777
+ console.log(example.getSummary());
4778
+ return result;
4779
+ }
4780
+
4781
+ main();
4782
+ `;
4783
+ }
4784
+ }
4785
+
4786
+ /**
4787
+ * Render source code with AST integration and collapsible sections
4788
+ */
4789
+ renderSourceWithAST(sourceContent, astElements, container, node) {
4790
+ const lines = sourceContent.split('\n');
4791
+ const astMap = this.createASTLineMap(astElements);
4792
+
4793
+ console.log('🎨 [SOURCE RENDERER] Rendering source with AST:', {
4794
+ lines: lines.length,
4795
+ astElements: astElements.length,
4796
+ astMap: Object.keys(astMap).length
4797
+ });
4798
+
4799
+ // Create line elements with AST integration
4800
+ lines.forEach((line, index) => {
4801
+ const lineNumber = index + 1;
4802
+ const lineElement = this.createSourceLine(line, lineNumber, astMap[lineNumber], node);
4803
+ container.appendChild(lineElement);
4804
+ });
4805
+
4806
+ // Store reference for expand/collapse operations
4807
+ this.currentSourceContainer = container;
4808
+ this.currentASTElements = astElements;
4809
+ }
4810
+
4811
+ /**
4812
+ * Create AST line mapping for quick lookup
4813
+ */
4814
+ createASTLineMap(astElements) {
4815
+ const lineMap = {};
4816
+
4817
+ astElements.forEach(element => {
4818
+ if (element.line) {
4819
+ if (!lineMap[element.line]) {
4820
+ lineMap[element.line] = [];
4821
+ }
4822
+ lineMap[element.line].push(element);
4823
+ }
4824
+ });
4825
+
4826
+ return lineMap;
4827
+ }
4828
+
4829
+ /**
4830
+ * Create a source line element with AST integration
4831
+ */
4832
+ createSourceLine(content, lineNumber, astElements, node) {
4833
+ const lineDiv = document.createElement('div');
4834
+ lineDiv.className = 'source-line';
4835
+ lineDiv.dataset.lineNumber = lineNumber;
4836
+
4837
+ // Check if this line has AST elements
4838
+ const hasAST = astElements && astElements.length > 0;
4839
+ if (hasAST) {
4840
+ lineDiv.classList.add('ast-element');
4841
+ lineDiv.dataset.astElements = JSON.stringify(astElements);
4842
+ }
4843
+
4844
+ // Determine if this line should be collapsible
4845
+ const isCollapsible = this.isCollapsibleLine(content, astElements);
4846
+ if (isCollapsible) {
4847
+ lineDiv.classList.add('collapsible');
4848
+ }
4849
+
4850
+ // Create line number
4851
+ const lineNumberSpan = document.createElement('span');
4852
+ lineNumberSpan.className = 'line-number';
4853
+ lineNumberSpan.textContent = lineNumber;
4854
+
4855
+ // Create collapse indicator
4856
+ const collapseIndicator = document.createElement('span');
4857
+ collapseIndicator.className = 'collapse-indicator';
4858
+ if (isCollapsible) {
4859
+ collapseIndicator.classList.add('expanded');
4860
+ collapseIndicator.addEventListener('click', (e) => {
4861
+ e.stopPropagation();
4862
+ this.toggleSourceSection(lineDiv);
4863
+ });
4864
+ } else {
4865
+ collapseIndicator.classList.add('none');
4866
+ }
4867
+
4868
+ // Create line content with syntax highlighting
4869
+ const lineContentSpan = document.createElement('span');
4870
+ lineContentSpan.className = 'line-content';
4871
+ lineContentSpan.innerHTML = this.applySyntaxHighlighting(content);
4872
+
4873
+ // Add click handler for AST integration
4874
+ if (hasAST) {
4875
+ lineDiv.addEventListener('click', () => {
4876
+ this.onSourceLineClick(lineDiv, astElements, node);
4877
+ });
4878
+ }
4879
+
4880
+ lineDiv.appendChild(lineNumberSpan);
4881
+ lineDiv.appendChild(collapseIndicator);
4882
+ lineDiv.appendChild(lineContentSpan);
4883
+
4884
+ return lineDiv;
4885
+ }
4886
+
4887
+ /**
4888
+ * Check if a line should be collapsible (function/class definitions)
4889
+ */
4890
+ isCollapsibleLine(content, astElements) {
4891
+ const trimmed = content.trim();
4892
+
4893
+ // Python patterns
4894
+ if (trimmed.startsWith('def ') || trimmed.startsWith('class ') ||
4895
+ trimmed.startsWith('async def ')) {
4896
+ return true;
4897
+ }
4898
+
4899
+ // JavaScript patterns
4900
+ if (trimmed.includes('function ') || trimmed.includes('class ') ||
4901
+ trimmed.includes('=> {') || trimmed.match(/^\s*\w+\s*\([^)]*\)\s*{/)) {
4902
+ return true;
4903
+ }
4904
+
4905
+ // Check AST elements for function/class definitions
4906
+ if (astElements) {
4907
+ return astElements.some(el =>
4908
+ el.type === 'function' || el.type === 'class' ||
4909
+ el.type === 'method' || el.type === 'FunctionDef' ||
4910
+ el.type === 'ClassDef'
4911
+ );
4912
+ }
4913
+
4914
+ return false;
4915
+ }
4916
+
4917
+ /**
4918
+ * Apply basic syntax highlighting
4919
+ */
4920
+ applySyntaxHighlighting(content) {
4921
+ // First, properly escape HTML entities
4922
+ let highlighted = content
4923
+ .replace(/&/g, '&amp;')
4924
+ .replace(/</g, '&lt;')
4925
+ .replace(/>/g, '&gt;');
4926
+
4927
+ // Store markers for where we'll insert spans
4928
+ const replacements = [];
4929
+
4930
+ // Python and JavaScript keywords (combined)
4931
+ 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;
4932
+
4933
+ // Find all matches first without replacing
4934
+ let match;
4935
+
4936
+ // Keywords
4937
+ while ((match = keywords.exec(highlighted)) !== null) {
4938
+ replacements.push({
4939
+ start: match.index,
4940
+ end: match.index + match[0].length,
4941
+ replacement: `<span class="keyword">${match[0]}</span>`
4942
+ });
4943
+ }
4944
+
4945
+ // Strings - simple pattern for now
4946
+ const stringPattern = /(["'`])([^"'`]*?)\1/g;
4947
+ while ((match = stringPattern.exec(highlighted)) !== null) {
4948
+ replacements.push({
4949
+ start: match.index,
4950
+ end: match.index + match[0].length,
4951
+ replacement: `<span class="string">${match[0]}</span>`
4952
+ });
4953
+ }
4954
+
4955
+ // Comments
4956
+ const commentPattern = /(#.*$|\/\/.*$)/gm;
4957
+ while ((match = commentPattern.exec(highlighted)) !== null) {
4958
+ replacements.push({
4959
+ start: match.index,
4960
+ end: match.index + match[0].length,
4961
+ replacement: `<span class="comment">${match[0]}</span>`
4962
+ });
4963
+ }
4964
+
4965
+ // Sort replacements by start position (reverse order to not mess up indices)
4966
+ replacements.sort((a, b) => b.start - a.start);
4967
+
4968
+ // Apply replacements
4969
+ for (const rep of replacements) {
4970
+ // Check for overlapping replacements and skip if needed
4971
+ const before = highlighted.substring(0, rep.start);
4972
+ const after = highlighted.substring(rep.end);
4973
+
4974
+ // Only apply if we're not inside another replacement
4975
+ if (!before.includes('<span') || before.lastIndexOf('</span>') > before.lastIndexOf('<span')) {
4976
+ highlighted = before + rep.replacement + after;
4977
+ }
4978
+ }
4979
+
4980
+ return highlighted;
4981
+ }
4982
+
4983
+ /**
4984
+ * Toggle collapse/expand of a source section
4985
+ */
4986
+ toggleSourceSection(lineElement) {
4987
+ const indicator = lineElement.querySelector('.collapse-indicator');
4988
+ const isExpanded = indicator.classList.contains('expanded');
4989
+
4990
+ if (isExpanded) {
4991
+ this.collapseSourceSection(lineElement);
4992
+ } else {
4993
+ this.expandSourceSection(lineElement);
4994
+ }
4995
+ }
4996
+
4997
+ /**
4998
+ * Collapse a source section
4999
+ */
5000
+ collapseSourceSection(lineElement) {
5001
+ const indicator = lineElement.querySelector('.collapse-indicator');
5002
+ indicator.classList.remove('expanded');
5003
+ indicator.classList.add('collapsed');
5004
+
5005
+ // Find and hide related lines (simple implementation)
5006
+ const startLine = parseInt(lineElement.dataset.lineNumber);
5007
+ const container = lineElement.parentElement;
5008
+ const lines = Array.from(container.children);
5009
+
5010
+ // Hide subsequent indented lines
5011
+ let currentIndex = lines.indexOf(lineElement) + 1;
5012
+ const baseIndent = this.getLineIndentation(lineElement.querySelector('.line-content').textContent);
5013
+
5014
+ while (currentIndex < lines.length) {
5015
+ const nextLine = lines[currentIndex];
5016
+ const nextContent = nextLine.querySelector('.line-content').textContent;
5017
+ const nextIndent = this.getLineIndentation(nextContent);
5018
+
5019
+ // Stop if we hit a line at the same or lower indentation level
5020
+ if (nextContent.trim() && nextIndent <= baseIndent) {
5021
+ break;
5022
+ }
5023
+
5024
+ nextLine.classList.add('collapsed-content');
5025
+ currentIndex++;
5026
+ }
5027
+
5028
+ // Add collapsed placeholder
5029
+ const placeholder = document.createElement('div');
5030
+ placeholder.className = 'source-line collapsed-placeholder';
5031
+ placeholder.innerHTML = `
5032
+ <span class="line-number"></span>
5033
+ <span class="collapse-indicator none"></span>
5034
+ <span class="line-content"> ... (collapsed)</span>
5035
+ `;
5036
+ lineElement.insertAdjacentElement('afterend', placeholder);
5037
+ }
5038
+
5039
+ /**
5040
+ * Expand a source section
5041
+ */
5042
+ expandSourceSection(lineElement) {
5043
+ const indicator = lineElement.querySelector('.collapse-indicator');
5044
+ indicator.classList.remove('collapsed');
5045
+ indicator.classList.add('expanded');
5046
+
5047
+ // Show hidden lines
5048
+ const container = lineElement.parentElement;
5049
+ const lines = Array.from(container.children);
5050
+
5051
+ lines.forEach(line => {
5052
+ if (line.classList.contains('collapsed-content')) {
5053
+ line.classList.remove('collapsed-content');
5054
+ }
5055
+ });
5056
+
5057
+ // Remove placeholder
5058
+ const placeholder = lineElement.nextElementSibling;
5059
+ if (placeholder && placeholder.classList.contains('collapsed-placeholder')) {
5060
+ placeholder.remove();
5061
+ }
5062
+ }
5063
+
5064
+ /**
5065
+ * Get indentation level of a line
5066
+ */
5067
+ getLineIndentation(content) {
5068
+ const match = content.match(/^(\s*)/);
5069
+ return match ? match[1].length : 0;
5070
+ }
5071
+
5072
+ /**
5073
+ * Handle click on source line with AST elements
5074
+ */
5075
+ onSourceLineClick(lineElement, astElements, node) {
5076
+ console.log('🎯 [SOURCE LINE CLICK] Line clicked:', {
5077
+ line: lineElement.dataset.lineNumber,
5078
+ astElements: astElements.length
5079
+ });
5080
+
5081
+ // Highlight the clicked line
5082
+ this.highlightSourceLine(lineElement);
5083
+
5084
+ // Show AST details for this line
5085
+ if (astElements.length > 0) {
5086
+ this.showASTElementDetails(astElements[0], node);
5087
+ }
5088
+
5089
+ // If this is a collapsible line, also toggle it
5090
+ if (lineElement.classList.contains('collapsible')) {
5091
+ this.toggleSourceSection(lineElement);
5092
+ }
5093
+ }
5094
+
5095
+ /**
5096
+ * Highlight a source line
5097
+ */
5098
+ highlightSourceLine(lineElement) {
5099
+ // Remove previous highlights
5100
+ if (this.currentSourceContainer) {
5101
+ const lines = this.currentSourceContainer.querySelectorAll('.source-line');
5102
+ lines.forEach(line => line.classList.remove('highlighted'));
5103
+ }
5104
+
5105
+ // Add highlight to clicked line
5106
+ lineElement.classList.add('highlighted');
5107
+ }
5108
+
5109
+ /**
5110
+ * Show AST element details
5111
+ */
5112
+ showASTElementDetails(astElement, node) {
5113
+ // This could open a detailed view or update another panel
5114
+ console.log('📋 [AST DETAILS] Showing details for:', astElement);
5115
+
5116
+ // For now, just log the details
5117
+ // In a full implementation, this might update a details panel
5118
+ }
5119
+
5120
+ /**
5121
+ * Expand all collapsible sections in source viewer
5122
+ */
5123
+ expandAllSource() {
5124
+ if (!this.currentSourceContainer) return;
5125
+
5126
+ const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
5127
+ collapsibleLines.forEach(line => {
5128
+ const indicator = line.querySelector('.collapse-indicator');
5129
+ if (indicator.classList.contains('collapsed')) {
5130
+ this.expandSourceSection(line);
5131
+ }
5132
+ });
5133
+ }
5134
+
5135
+ /**
5136
+ * Collapse all collapsible sections in source viewer
5137
+ */
5138
+ collapseAllSource() {
5139
+ if (!this.currentSourceContainer) return;
5140
+
5141
+ const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
5142
+ collapsibleLines.forEach(line => {
5143
+ const indicator = line.querySelector('.collapse-indicator');
5144
+ if (indicator.classList.contains('expanded')) {
5145
+ this.collapseSourceSection(line);
5146
+ }
5147
+ });
5148
+ }
3257
5149
  }
3258
5150
 
3259
5151
  // Export for use in other modules
@@ -3264,7 +5156,35 @@ document.addEventListener('DOMContentLoaded', () => {
3264
5156
  // Check if we're on a page with code tree container
3265
5157
  if (document.getElementById('code-tree-container')) {
3266
5158
  window.codeTree = new CodeTree();
3267
-
5159
+
5160
+ // Expose debug functions globally for troubleshooting
5161
+ window.debugCodeTree = {
5162
+ clearLoadingState: () => window.codeTree?.clearLoadingState(),
5163
+ showLoadingNodes: () => {
5164
+ console.log('Current loading nodes:', Array.from(window.codeTree?.loadingNodes || []));
5165
+ return Array.from(window.codeTree?.loadingNodes || []);
5166
+ },
5167
+ resetTree: () => {
5168
+ if (window.codeTree) {
5169
+ window.codeTree.clearLoadingState();
5170
+ window.codeTree.initializeTreeData();
5171
+ console.log('Tree reset complete');
5172
+ }
5173
+ },
5174
+ focusOnPath: (path) => {
5175
+ if (window.codeTree) {
5176
+ const node = window.codeTree.findD3NodeByPath(path);
5177
+ if (node) {
5178
+ window.codeTree.focusOnDirectory(node);
5179
+ console.log('Focused on:', path);
5180
+ } else {
5181
+ console.log('Node not found:', path);
5182
+ }
5183
+ }
5184
+ },
5185
+ unfocus: () => window.codeTree?.unfocusDirectory()
5186
+ };
5187
+
3268
5188
  // Listen for tab changes to initialize when code tab is selected
3269
5189
  document.addEventListener('click', (e) => {
3270
5190
  if (e.target.matches('[data-tab="code"]')) {