claude-mpm 4.1.8__py3-none-any.whl → 4.1.11__py3-none-any.whl

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