claude-mpm 4.1.10__py3-none-any.whl → 4.1.12__py3-none-any.whl

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