Monorepo for Aesthetic.Computer
aesthetic.computer
1// Auto-generated - DO NOT EDIT DIRECTLY
2// Edit views/process-tree-2d.js and views/ast-tree.js instead and run: node build-views.mjs
3
4export const PROCESS_TREE_JS = "// 2D Canvas Process Tree Visualization\n// Dense, responsive, real-time process tree view\n\n(function() {\n 'use strict';\n\n const isVSCode = typeof acquireVsCodeApi === 'function';\n\n // Color schemes\n const colorSchemes = {\n dark: {\n bg: '#181318',\n fg: '#fff',\n fgMuted: '#666',\n accent: '#ff69b4',\n online: '#0f0',\n line: 'rgba(255, 255, 255, 0.15)',\n nodeStroke: 'rgba(255, 255, 255, 0.3)',\n categories: {\n editor: '#b05070',\n tui: '#ff5294',\n bridge: '#6c757f',\n db: '#ffb84b',\n proxy: '#6bbd9f',\n ai: '#ff95db',\n shell: '#6c77ff',\n dev: '#6c757f',\n ide: '#6bbd9f',\n lsp: '#889098',\n kernel: '#889fff'\n }\n },\n light: {\n bg: '#fcf7c5',\n fg: '#281e5a',\n fgMuted: '#806060',\n accent: '#006400',\n online: '#006400',\n line: 'rgba(0, 0, 0, 0.15)',\n nodeStroke: 'rgba(0, 0, 0, 0.3)',\n categories: {\n editor: '#804050',\n tui: '#d02070',\n bridge: '#202530',\n db: '#a08020',\n proxy: '#206050',\n ai: '#c05090',\n shell: '#004080',\n dev: '#202530',\n ide: '#206050',\n lsp: '#606068',\n kernel: '#387adf'\n }\n }\n };\n\n let currentTheme = document.body.dataset.theme || 'dark';\n let scheme = colorSchemes[currentTheme];\n\n // Canvas setup\n const canvas = document.getElementById('canvas');\n const ctx = canvas.getContext('2d');\n let width, height;\n let dpr = window.devicePixelRatio || 1;\n\n function resizeCanvas() {\n width = canvas.clientWidth;\n height = canvas.clientHeight;\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n ctx.scale(dpr, dpr);\n }\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n\n // View transform (pan & zoom)\n let offsetX = 0, offsetY = 0, scale = 1;\n let isDragging = false, dragStartX = 0, dragStartY = 0;\n\n // Process tree data\n let processTree = [];\n let nodeMap = new Map(); // pid -> node info\n\n // Layout constants\n const NODE_RADIUS = 6;\n const NODE_SPACING_X = 180;\n const NODE_SPACING_Y = 30;\n const INDENT = 20;\n\n // Build tree structure from flat process list\n function buildTree(processes) {\n if (!processes || !processes.length) return [];\n\n const byPid = new Map();\n const children = new Map();\n\n processes.forEach(p => {\n byPid.set(String(p.pid), p);\n children.set(String(p.pid), []);\n });\n\n let roots = [];\n processes.forEach(p => {\n const ppid = String(p.ppid);\n if (ppid && byPid.has(ppid) && ppid !== String(p.pid)) {\n children.get(ppid).push(p);\n } else {\n roots.push(p);\n }\n });\n\n // Layout tree with coordinates\n nodeMap.clear();\n let yOffset = 20;\n\n function layoutNode(proc, depth, parentY) {\n const pid = String(proc.pid);\n const x = depth * INDENT;\n const y = yOffset;\n yOffset += NODE_SPACING_Y;\n\n const node = {\n pid,\n name: proc.name || proc.command || `PID ${pid}`,\n cpu: proc.cpu || 0,\n mem: proc.mem || 0,\n category: proc.category || 'shell',\n x,\n y,\n depth,\n parentY,\n children: []\n };\n\n nodeMap.set(pid, node);\n\n const kids = children.get(pid) || [];\n kids.forEach(child => {\n const childNode = layoutNode(child, depth + 1, y);\n if (childNode) node.children.push(childNode);\n });\n\n return node;\n }\n\n return roots.map(r => layoutNode(r, 0, null)).filter(Boolean);\n }\n\n // Draw the tree\n function draw() {\n ctx.clearRect(0, 0, width, height);\n\n ctx.save();\n ctx.translate(offsetX, offsetY);\n ctx.scale(scale, scale);\n\n // Draw connections first\n nodeMap.forEach(node => {\n if (node.parentY !== null) {\n ctx.strokeStyle = scheme.line;\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(node.x, node.y);\n ctx.lineTo(node.x - INDENT, node.parentY);\n ctx.stroke();\n }\n });\n\n // Draw nodes\n nodeMap.forEach(node => {\n const color = scheme.categories[node.category] || scheme.categories.shell;\n\n // Node circle\n ctx.fillStyle = color;\n ctx.strokeStyle = scheme.nodeStroke;\n ctx.lineWidth = 1.5;\n ctx.beginPath();\n ctx.arc(node.x, node.y, NODE_RADIUS, 0, Math.PI * 2);\n ctx.fill();\n ctx.stroke();\n\n // Node label\n ctx.fillStyle = scheme.fg;\n ctx.font = '11px monospace';\n ctx.textAlign = 'left';\n ctx.textBaseline = 'middle';\n ctx.fillText(node.name, node.x + NODE_RADIUS + 6, node.y);\n\n // CPU/MEM info\n if (node.cpu > 0.1 || node.mem > 0) {\n ctx.fillStyle = scheme.fgMuted;\n ctx.font = '9px monospace';\n const info = `${node.cpu.toFixed(1)}% • ${node.mem.toFixed(0)}MB`;\n ctx.fillText(info, node.x + NODE_RADIUS + 6, node.y + 11);\n }\n });\n\n ctx.restore();\n }\n\n // Mouse interaction\n let hoveredNode = null;\n\n function screenToWorld(screenX, screenY) {\n return {\n x: (screenX - offsetX) / scale,\n y: (screenY - offsetY) / scale\n };\n }\n\n function findNodeAt(worldX, worldY) {\n for (const [pid, node] of nodeMap) {\n const dx = worldX - node.x;\n const dy = worldY - node.y;\n if (Math.sqrt(dx * dx + dy * dy) <= NODE_RADIUS + 2) {\n return node;\n }\n }\n return null;\n }\n\n canvas.addEventListener('mousedown', e => {\n isDragging = true;\n dragStartX = e.clientX - offsetX;\n dragStartY = e.clientY - offsetY;\n canvas.style.cursor = 'grabbing';\n });\n\n canvas.addEventListener('mousemove', e => {\n const rect = canvas.getBoundingClientRect();\n const mouseX = e.clientX - rect.left;\n const mouseY = e.clientY - rect.top;\n\n if (isDragging) {\n offsetX = e.clientX - dragStartX;\n offsetY = e.clientY - dragStartY;\n draw();\n } else {\n const world = screenToWorld(mouseX, mouseY);\n const node = findNodeAt(world.x, world.y);\n\n if (node !== hoveredNode) {\n hoveredNode = node;\n if (node) {\n showTooltip(e.clientX, e.clientY, node);\n canvas.style.cursor = 'pointer';\n } else {\n hideTooltip();\n canvas.style.cursor = 'default';\n }\n }\n }\n });\n\n canvas.addEventListener('mouseup', () => {\n isDragging = false;\n canvas.style.cursor = 'default';\n });\n\n canvas.addEventListener('mouseleave', () => {\n isDragging = false;\n hideTooltip();\n canvas.style.cursor = 'default';\n });\n\n canvas.addEventListener('wheel', e => {\n e.preventDefault();\n const delta = e.deltaY > 0 ? 0.9 : 1.1;\n const newScale = Math.max(0.1, Math.min(5, scale * delta));\n\n const rect = canvas.getBoundingClientRect();\n const mouseX = e.clientX - rect.left;\n const mouseY = e.clientY - rect.top;\n\n offsetX = mouseX - (mouseX - offsetX) * (newScale / scale);\n offsetY = mouseY - (mouseY - offsetY) * (newScale / scale);\n scale = newScale;\n\n draw();\n });\n\n // Tooltip\n const tooltip = document.getElementById('tooltip');\n\n function showTooltip(x, y, node) {\n tooltip.style.display = 'block';\n tooltip.style.left = (x + 10) + 'px';\n tooltip.style.top = (y + 10) + 'px';\n tooltip.textContent = `${node.name}\\nPID: ${node.pid}\\nCPU: ${node.cpu.toFixed(1)}%\\nMem: ${node.mem.toFixed(0)} MB`;\n }\n\n function hideTooltip() {\n tooltip.style.display = 'none';\n }\n\n // Update from WebSocket data\n function updateViz(processData) {\n if (!processData?.interesting) return;\n\n const processes = processData.interesting;\n document.getElementById('process-count').textContent = `${processes.length} processes`;\n\n processTree = buildTree(processes);\n draw();\n }\n\n // WebSocket connection\n let ws;\n function connectWS() {\n const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';\n const port = location.port || (protocol === 'wss:' ? '443' : '80');\n const wsUrl = `${protocol}//${location.hostname}:${port}/process-tree-ws`;\n\n try {\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n console.log('Connected to process tree');\n document.getElementById('status-dot').classList.add('online');\n };\n\n ws.onclose = () => {\n document.getElementById('status-dot').classList.remove('online');\n setTimeout(connectWS, 2000);\n };\n\n ws.onmessage = (e) => {\n try {\n const data = JSON.parse(e.data);\n if (data.system) {\n document.getElementById('uptime').textContent = data.system.uptime?.formatted || '—';\n document.getElementById('cpu-info').textContent = `${data.system.cpus || 0} CPUs`;\n const m = data.system.memory;\n document.getElementById('mem-info').textContent = `${m?.used || 0} / ${m?.total || 0} MB`;\n }\n updateViz(data.processes);\n } catch (err) {\n console.error('Parse error:', err);\n }\n };\n } catch (err) {\n console.error('WebSocket error:', err);\n setTimeout(connectWS, 2000);\n }\n }\n\n // Theme toggle\n function toggleTheme() {\n currentTheme = currentTheme === 'dark' ? 'light' : 'dark';\n scheme = colorSchemes[currentTheme];\n document.body.dataset.theme = currentTheme;\n draw();\n }\n\n // Reset view\n function resetView() {\n offsetX = 0;\n offsetY = 0;\n scale = 1;\n draw();\n }\n\n // Export API\n window.ProcessTree2D = {\n toggleTheme,\n resetView,\n updateViz\n };\n\n // Start\n connectWS();\n\n // Animation loop for smooth updates\n function animate() {\n // Could add animation logic here\n requestAnimationFrame(animate);\n }\n animate();\n\n})();\n";
5
6export const AST_TREE_JS = "// 3D Source Code Visualization\n// Renders JavaScript/TypeScript files as interactive 3D trees with click navigation\n\n(function() {\n 'use strict';\n \n // Wait for ProcessTreeViz if it's not ready yet\n if (!window.ProcessTreeViz) {\n console.log('⏳ Waiting for ProcessTreeViz...');\n const checkInterval = setInterval(() => {\n if (window.ProcessTreeViz) {\n clearInterval(checkInterval);\n initASTVisualization();\n }\n }, 100);\n return;\n } else {\n initASTVisualization();\n }\n\n function initASTVisualization() {\n console.log('🚀 Initializing Source Tree Visualization');\n \n const { scene, camera, renderer, colorSchemes } = window.ProcessTreeViz;\n // Update theme reference dynamically as it might change\n let scheme = window.ProcessTreeViz.getScheme();\n\n // ... rest of initialization ...\n // Tab system state\n let currentTab = 'processes'; // 'processes' or 'sources'\n let sourcesVisible = false;\n \n // Source visualization state\n const sourceFiles = new Map(); // fileName -> { rootMesh, nodes: Map<id, mesh>, connections: [] }\n const sourceConnections = new Map();\n let astFiles = [];\n let focusedSourceNode = null;\n let hoveredNode = null;\n \n // Raycaster for click detection\n const raycaster = new THREE.Raycaster();\n const mouse = new THREE.Vector2();\n \n // Icon mapping for node types\n const nodeIcons = {\n\n Program: '📄',\n FunctionDeclaration: '🔧',\n FunctionExpression: '🔧',\n ArrowFunctionExpression: '➡️',\n AsyncFunctionDeclaration: '⚡',\n AsyncArrowFunctionExpression: '⚡',\n ClassDeclaration: '🏛️',\n ClassExpression: '🏛️',\n MethodDefinition: '🔩',\n VariableDeclaration: '📦',\n VariableDeclarator: '📦',\n ImportDeclaration: '📥',\n ImportSpecifier: '📥',\n ExportNamedDeclaration: '📤',\n ExportDefaultDeclaration: '📤',\n CallExpression: '📞',\n NewExpression: '🆕',\n MemberExpression: '🔗',\n Identifier: '🏷️',\n Literal: '✨',\n StringLiteral: '💬',\n NumericLiteral: '🔢',\n ObjectExpression: '{}',\n ObjectPattern: '{}',\n ArrayExpression: '[]',\n ArrayPattern: '[]',\n IfStatement: '❓',\n ConditionalExpression: '❓',\n ForStatement: '🔄',\n ForOfStatement: '🔄',\n ForInStatement: '🔄',\n WhileStatement: '🔁',\n DoWhileStatement: '🔁',\n SwitchStatement: '🔀',\n TryStatement: '🛡️',\n CatchClause: '🎣',\n ThrowStatement: '💥',\n ReturnStatement: '↩️',\n AwaitExpression: '⏳',\n YieldExpression: '🌾',\n SpreadElement: '...',\n TemplateLiteral: '📝',\n BlockStatement: '📦',\n Property: '🔑',\n AssignmentExpression: '=',\n BinaryExpression: '➕',\n LogicalExpression: '🧮',\n UnaryExpression: '!',\n UpdateExpression: '++',\n SequenceExpression: ',',\n ExpressionStatement: '💭',\n };\n \n // Color mapping for node types (more vibrant)\n const nodeColors = {\n Program: 0x88ccff,\n FunctionDeclaration: 0xff6b9f,\n FunctionExpression: 0xff6b9f,\n ArrowFunctionExpression: 0xff8faf,\n AsyncFunctionDeclaration: 0xffaf6b,\n ClassDeclaration: 0xb06bff,\n ClassExpression: 0xb06bff,\n MethodDefinition: 0xd080ff,\n VariableDeclaration: 0x6bff9f,\n VariableDeclarator: 0x50d080,\n ImportDeclaration: 0xffeb6b,\n ImportSpecifier: 0xffd040,\n ExportNamedDeclaration: 0xffc040,\n ExportDefaultDeclaration: 0xffa030,\n CallExpression: 0x6bb4ff,\n NewExpression: 0x80c0ff,\n MemberExpression: 0x5090d0,\n Identifier: 0x9999aa,\n Literal: 0x77aa77,\n StringLiteral: 0x88cc88,\n NumericLiteral: 0x88aacc,\n ObjectExpression: 0x6bffff,\n ArrayExpression: 0x50d0d0,\n IfStatement: 0xff9f6b,\n ConditionalExpression: 0xffaf80,\n ForStatement: 0xe08050,\n ForOfStatement: 0xe09060,\n WhileStatement: 0xd07040,\n SwitchStatement: 0xc06030,\n TryStatement: 0x60c0a0,\n CatchClause: 0x50b090,\n ReturnStatement: 0x80ff80,\n AwaitExpression: 0xffd080,\n BlockStatement: 0x555566,\n ExpressionStatement: 0x444455,\n Property: 0x8899aa,\n };\n \n // Get display name for a node (actual code identifier, not generic type)\n function getNodeDisplayName(node) {\n // Priority: actual identifier names\n if (node.name && node.name !== node.type) return node.name;\n \n // For different node types, try to extract meaningful names\n switch (node.type) {\n case 'FunctionDeclaration':\n case 'FunctionExpression':\n case 'ArrowFunctionExpression':\n return node.name || 'λ';\n case 'ClassDeclaration':\n case 'ClassExpression':\n return node.name || 'Class';\n case 'MethodDefinition':\n return node.name || 'method';\n case 'VariableDeclaration':\n return node.kind || 'var'; // const, let, var\n case 'VariableDeclarator':\n return node.name || 'binding';\n case 'ImportDeclaration':\n return node.source || 'import';\n case 'ExportNamedDeclaration':\n case 'ExportDefaultDeclaration':\n return node.name || 'export';\n case 'CallExpression':\n return node.callee || 'call()';\n case 'MemberExpression':\n return node.property || 'member';\n case 'Identifier':\n return node.name || 'id';\n case 'Literal':\n const val = String(node.value || '');\n return val.length > 12 ? val.slice(0, 10) + '…' : val;\n case 'Property':\n return node.name || 'prop';\n case 'Program':\n return '📄 ' + (node.fileName || 'source');\n default:\n return node.name || node.type.replace(/Declaration|Expression|Statement/g, '');\n }\n }\n \n function getNodeIcon(type) {\n return nodeIcons[type] || '●';\n }\n \n function getNodeColor(type) {\n return nodeColors[type] || 0x666688;\n }\n \n function getNodeSize(node) {\n const span = (node.end || 0) - (node.start || 0);\n const baseSize = Math.max(4, Math.min(14, 3 + Math.log(span + 1) * 0.9));\n \n // Boost root and important nodes\n const important = ['Program', 'FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', 'ExportDefaultDeclaration'];\n if (important.includes(node.type)) return baseSize * 1.4;\n return baseSize;\n }\n \n function createSourceNodeMesh(node, fileInfo) {\n const size = getNodeSize(node);\n const color = getNodeColor(node.type);\n \n // Support legacy string arg or object\n const fileName = (typeof fileInfo === 'string') ? fileInfo : fileInfo.fileName;\n const filePath = (typeof fileInfo === 'object') ? fileInfo.filePath : undefined;\n const fileId = (typeof fileInfo === 'object') ? (fileInfo.id || fileName) : fileName;\n\n // Use icosahedron for functions/classes, sphere for others\n const isImportant = ['FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', 'Program'].includes(node.type);\n const geo = isImportant \n ? new THREE.IcosahedronGeometry(size, 1)\n : new THREE.SphereGeometry(size, 12, 12);\n \n const mat = new THREE.MeshBasicMaterial({\n color: color,\n transparent: true,\n opacity: 0.85,\n });\n \n const mesh = new THREE.Mesh(geo, mat);\n mesh.userData = {\n ...node,\n fileName,\n filePath, // Store full path for navigation\n fileId, // Store unique ID for management\n size,\n baseColor: color,\n displayName: getNodeDisplayName(node),\n icon: getNodeIcon(node.type),\n targetPos: new THREE.Vector3(),\n pulsePhase: Math.random() * Math.PI * 2,\n isSourceNode: true,\n };\n \n return mesh;\n }\n \n function createSourceConnection(thickness = 1.2) {\n const geo = new THREE.CylinderGeometry(thickness, thickness, 1, 8);\n const mat = new THREE.MeshBasicMaterial({\n color: scheme.three.connectionLine,\n transparent: true,\n opacity: 0.6,\n });\n return new THREE.Mesh(geo, mat);\n }\n \n function updateConnectionMesh(conn, childPos, parentPos) {\n const mesh = conn.line;\n const mid = new THREE.Vector3().addVectors(childPos, parentPos).multiplyScalar(0.5);\n mesh.position.copy(mid);\n const dir = new THREE.Vector3().subVectors(parentPos, childPos);\n const length = dir.length();\n mesh.scale.set(1, length, 1);\n mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize());\n }\n \n // Improved tree layout with more spacing\n function layoutSourceTree(root, fileIndex = 0, totalFiles = 1) {\n if (!root) return;\n \n const levelHeight = 45; // More vertical spacing\n const baseSpacing = 300;\n const fileSpread = totalFiles > 1 ? 360 / totalFiles : 0;\n const fileAngle = (fileIndex / totalFiles) * Math.PI * 2 - Math.PI / 2;\n const baseX = Math.cos(fileAngle) * baseSpacing;\n const baseZ = Math.sin(fileAngle) * baseSpacing;\n \n function countDescendants(node) {\n if (!node.children || node.children.length === 0) return 1;\n return node.children.reduce((sum, child) => sum + countDescendants(child), 0);\n }\n \n function positionNode(node, depth, angle, radius, parentX, parentZ) {\n const childCount = node.children?.length || 0;\n \n const x = parentX + Math.cos(angle) * radius;\n const z = parentZ + Math.sin(angle) * radius;\n const y = -depth * levelHeight + 50; // Start above center\n \n node.targetX = x;\n node.targetY = y;\n node.targetZ = z;\n \n if (childCount > 0) {\n // Calculate arc spread based on descendants for better spacing\n const totalDescendants = node.children.reduce((sum, child) => sum + countDescendants(child), 0);\n const arcSpread = Math.min(Math.PI * 1.5, Math.PI * 0.15 * totalDescendants);\n const startAngle = angle - arcSpread / 2;\n \n let currentAngle = startAngle;\n node.children.forEach((child, i) => {\n const childDescendants = countDescendants(child);\n const childArcPortion = (childDescendants / totalDescendants) * arcSpread;\n const childAngle = currentAngle + childArcPortion / 2;\n currentAngle += childArcPortion;\n \n const childRadius = 30 + Math.sqrt(childDescendants) * 8;\n positionNode(child, depth + 1, childAngle, childRadius, x, z);\n });\n }\n }\n \n positionNode(root, 0, fileAngle, 0, baseX, baseZ);\n }\n \n // Helper to get consistent ID\n const getFileId = (f) => f.id || f.fileName;\n \n // Get icon/color for non-AST buffer files (markdown, lisp)\n function getBufferIcon(fileName) {\n if (fileName.endsWith('.md')) return '📝';\n if (fileName.endsWith('.lisp')) return '🔮';\n return '📄';\n }\n \n function getBufferColor(fileName) {\n if (fileName.endsWith('.md')) return 0x88ccff; // Markdown - light blue\n if (fileName.endsWith('.lisp')) return 0xff79c6; // Lisp - pink\n return 0x666688;\n }\n \n // Create a simple buffer node mesh for non-AST files\n function createBufferNodeMesh(file) {\n const size = 12;\n const color = getBufferColor(file.fileName);\n const icon = getBufferIcon(file.fileName);\n \n const geo = new THREE.IcosahedronGeometry(size, 1);\n const mat = new THREE.MeshBasicMaterial({\n color: color,\n transparent: true,\n opacity: 0.85,\n });\n \n const mesh = new THREE.Mesh(geo, mat);\n mesh.userData = {\n type: 'Buffer',\n fileName: file.fileName,\n filePath: file.filePath,\n fileId: getFileId(file),\n size,\n baseColor: color,\n displayName: file.fileName,\n icon: icon,\n targetPos: new THREE.Vector3(),\n pulsePhase: Math.random() * Math.PI * 2,\n isSourceNode: true,\n isBuffer: true,\n };\n \n return mesh;\n }\n\n function updateSourceVisualization(files) {\n astFiles = files;\n \n if (!sourcesVisible) return;\n \n const currentFileIds = new Set(files.map(f => getFileId(f)));\n \n // Remove old visualizations\n sourceFiles.forEach((fileData, fileId) => {\n if (!currentFileIds.has(fileId)) {\n fileData.nodes.forEach(mesh => {\n scene.remove(mesh);\n mesh.geometry?.dispose();\n mesh.material?.dispose();\n });\n sourceFiles.delete(fileId);\n }\n });\n \n // Remove old connections\n sourceConnections.forEach((conn, key) => {\n // Compatibility: check fileId if available, else fallback to fileName check?\n // Actually we should store fileId in conn\n const idToCheck = conn.fileId || conn.fileName;\n if (!currentFileIds.has(idToCheck)) {\n scene.remove(conn.line);\n conn.line.geometry?.dispose();\n conn.line.material?.dispose();\n sourceConnections.delete(key);\n }\n });\n \n // Create/update visualizations\n const astFiles = files.filter(f => f.ast);\n const bufferFiles = files.filter(f => !f.ast); // Markdown, Lisp, etc.\n const totalAstFiles = astFiles.length;\n const totalBufferFiles = bufferFiles.length;\n let fileIndex = 0;\n \n // Layout AST files (existing behavior)\n astFiles.forEach((file) => {\n const fileId = getFileId(file);\n\n // Add fileName to root node\n file.ast.fileName = file.fileName;\n \n layoutSourceTree(file.ast, fileIndex, totalAstFiles);\n fileIndex++;\n \n let fileData = sourceFiles.get(fileId);\n if (!fileData) {\n fileData = { nodes: new Map() };\n sourceFiles.set(fileId, fileData);\n }\n \n const currentNodeIds = new Set();\n \n function processNode(node, parentId) {\n if (!node) return;\n \n currentNodeIds.add(node.id);\n \n let mesh = fileData.nodes.get(node.id);\n if (!mesh) {\n mesh = createSourceNodeMesh(node, file); // Pass full file object\n mesh.position.set(node.targetX || 0, node.targetY || 0, node.targetZ || 0);\n scene.add(mesh);\n fileData.nodes.set(node.id, mesh);\n }\n \n // Update mesh data\n mesh.userData.targetPos.set(node.targetX || 0, node.targetY || 0, node.targetZ || 0);\n mesh.userData.displayName = getNodeDisplayName(node);\n mesh.userData.loc = node.loc;\n mesh.visible = sourcesVisible;\n \n // Create connection to parent\n if (parentId) {\n const connKey = `${fileId}:${node.id}->${parentId}`;\n if (!sourceConnections.has(connKey)) {\n const thickness = node.depth < 3 ? 2 : 1.2;\n const line = createSourceConnection(thickness);\n line.visible = sourcesVisible;\n scene.add(line);\n sourceConnections.set(connKey, { \n line, \n childId: node.id, \n parentId, \n fileId: fileId, // Store ID\n fileName: file.fileName,\n depth: node.depth \n });\n } else {\n sourceConnections.get(connKey).line.visible = sourcesVisible;\n }\n }\n \n // Process children\n if (node.children) {\n node.children.forEach(child => processNode(child, node.id));\n }\n }\n \n processNode(file.ast, null);\n \n // Remove deleted nodes\n fileData.nodes.forEach((mesh, nodeId) => {\n if (!currentNodeIds.has(nodeId)) {\n scene.remove(mesh);\n mesh.geometry?.dispose();\n mesh.material?.dispose();\n fileData.nodes.delete(nodeId);\n }\n });\n });\n \n // Layout buffer files (markdown, lisp) - simple nodes in an arc\n bufferFiles.forEach((file, bufferIndex) => {\n const fileId = getFileId(file);\n \n let fileData = sourceFiles.get(fileId);\n if (!fileData) {\n fileData = { nodes: new Map(), isBuffer: true };\n sourceFiles.set(fileId, fileData);\n }\n \n const bufferId = `buffer-${fileId}`;\n let mesh = fileData.nodes.get(bufferId);\n \n // Position buffer files in an arc below AST files\n const bufferRadius = 80;\n const angleSpread = Math.PI * 0.8;\n const startAngle = Math.PI + (Math.PI - angleSpread) / 2;\n const angle = totalBufferFiles === 1 \n ? Math.PI * 1.5 \n : startAngle + (angleSpread / (totalBufferFiles - 1)) * bufferIndex;\n \n const targetX = Math.cos(angle) * bufferRadius;\n const targetY = -60; // Below the main AST view\n const targetZ = Math.sin(angle) * bufferRadius;\n \n if (!mesh) {\n mesh = createBufferNodeMesh(file);\n mesh.position.set(targetX, targetY, targetZ);\n scene.add(mesh);\n fileData.nodes.set(bufferId, mesh);\n }\n \n mesh.userData.targetPos.set(targetX, targetY, targetZ);\n mesh.visible = sourcesVisible;\n });\n \n // Clean orphaned connections\n sourceConnections.forEach((conn, key) => {\n // Use fileId to lookup\n const idToCheck = conn.fileId || conn.fileName;\n const fileData = sourceFiles.get(idToCheck);\n \n if (!fileData || !fileData.nodes.has(conn.childId)) {\n scene.remove(conn.line);\n conn.line.geometry?.dispose();\n conn.line.material?.dispose();\n sourceConnections.delete(key);\n }\n });\n }\n \n // Tab switching\n function setTab(tab) {\n if (tab !== 'processes' && tab !== 'sources') return;\n currentTab = tab;\n \n // Toggle visibility\n sourcesVisible = (tab === 'sources');\n \n // Hide/show process meshes\n window.ProcessTreeViz.meshes?.forEach(mesh => {\n mesh.visible = !sourcesVisible;\n });\n window.ProcessTreeViz.connections?.forEach(conn => {\n if (conn.line) conn.line.visible = !sourcesVisible;\n });\n window.ProcessTreeViz.graveyard?.forEach(grave => {\n if (grave.mesh) grave.mesh.visible = !sourcesVisible;\n });\n \n // Hide/show source meshes\n sourceFiles.forEach(fileData => {\n fileData.nodes.forEach(mesh => {\n mesh.visible = sourcesVisible;\n });\n });\n sourceConnections.forEach(conn => {\n conn.line.visible = sourcesVisible;\n });\n \n // Link Update Labels\n const processLabels = document.getElementById('labels');\n const sourceLabels = document.getElementById('source-labels');\n const hudCenter = document.querySelector('.hud.center');\n \n if (processLabels) processLabels.style.display = sourcesVisible ? 'none' : 'block';\n if (sourceLabels) sourceLabels.style.display = sourcesVisible ? 'block' : 'none';\n if (hudCenter) hudCenter.style.display = sourcesVisible ? 'none' : 'flex';\n \n // Reset camera for sources view\n if (sourcesVisible) {\n // Re-run visualization with current files\n updateSourceVisualization(astFiles);\n }\n \n updateTabUI();\n }\n \n // Tour Mode for Sources\n let tourMode = false;\n let tourList = [];\n let tourIndex = 0;\n let tourAutoPlay = false;\n let tourInterval = null;\n\n function buildTourList() {\n const list = [];\n sourceFiles.forEach(fileData => {\n // Add file root\n // list.push(fileData.nodes.get(fileData.rootId)); \n \n // Add interesting nodes\n fileData.nodes.forEach(mesh => {\n const d = mesh.userData;\n const important = ['Program', 'FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', 'ExportDefaultDeclaration'];\n if (important.includes(d.type)) {\n list.push(mesh);\n }\n });\n });\n \n // Sort by position roughly to make a logical path? \n // Or just keep them in file/traversal order which map iteration should mostly preserve\n return list;\n }\n\n function startSourceTour() {\n tourMode = true;\n tourList = buildTourList();\n tourIndex = 0;\n \n if (tourList.length > 0) {\n focusOnNode(tourList[0]);\n }\n \n // Notify ProcessTree to update UI button state if needed\n if (window.ProcessTreeViz) {\n const btn = document.getElementById('tour-btn');\n if (btn) btn.textContent = '⏹ Stop Tour';\n }\n }\n\n function stopSourceTour() {\n tourMode = false;\n tourAutoPlay = false;\n if (tourInterval) {\n clearInterval(tourInterval);\n tourInterval = null;\n }\n focusedSourceNode = null;\n \n if (window.ProcessTreeViz) {\n const btn = document.getElementById('tour-btn');\n if (btn) btn.textContent = '🎬 Tour';\n window.ProcessTreeViz.controls.autoRotate = false;\n }\n }\n\n function tourNext() {\n if (!tourMode || tourList.length === 0) return;\n tourIndex = (tourIndex + 1) % tourList.length;\n focusOnNode(tourList[tourIndex]);\n }\n\n function tourPrev() {\n if (!tourMode || tourList.length === 0) return;\n tourIndex = (tourIndex - 1 + tourList.length) % tourList.length;\n focusOnNode(tourList[tourIndex]);\n }\n\n function focusOnNode(mesh) {\n if (!mesh) return;\n focusedSourceNode = mesh.userData.id;\n \n const controls = window.ProcessTreeViz.controls;\n const camera = window.ProcessTreeViz.camera;\n \n if (controls) {\n controls.target.copy(mesh.position);\n controls.autoRotate = true;\n controls.autoRotateSpeed = 2.0; // Slow rotation\n \n // Zoom in appropriately\n const dist = 100 + (mesh.userData.size || 10) * 5;\n const currentDist = camera.position.distanceTo(controls.target);\n \n // Smoothly move camera distance in animate loop? \n // For now, let's just set a target for the animate loop to handle if we add that support\n // Or just jump\n /* \n const direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize();\n camera.position.copy(controls.target).add(direction.multiplyScalar(dist));\n */\n }\n updateSourceLabels();\n }\n\n function toggleTourAutoPlay() {\n tourAutoPlay = !tourAutoPlay;\n if (tourAutoPlay) {\n tourInterval = setInterval(tourNext, 3000); // 3 seconds per node\n } else {\n if (tourInterval) {\n clearInterval(tourInterval);\n tourInterval = null;\n }\n }\n }\n\n function updateTabUI() {\n // Insert tabs into #header-center if it exists, otherwise create floating tabs\n let headerCenter = document.getElementById('header-center');\n let tabBar = document.getElementById('view-tabs');\n \n if (headerCenter) {\n // Use existing header structure\n if (!tabBar) {\n tabBar = document.createElement('div');\n tabBar.id = 'view-tabs';\n tabBar.style.cssText = 'display: flex; gap: 4px; margin-left: 16px;';\n headerCenter.appendChild(tabBar);\n }\n } else {\n // Fallback: create in header-right area\n const headerRight = document.getElementById('header-right');\n if (headerRight && !tabBar) {\n tabBar = document.createElement('div');\n tabBar.id = 'view-tabs';\n tabBar.style.cssText = 'display: flex; gap: 4px;';\n headerRight.insertBefore(tabBar, headerRight.firstChild);\n }\n }\n \n if (!tabBar) return; // No place to put tabs\n \n const processActive = currentTab === 'processes';\n const sourceActive = currentTab === 'sources';\n const fileCount = astFiles.filter(f => f.ast).length;\n \n tabBar.innerHTML = `\n <button id=\"tab-processes\" class=\"hdr-btn\" style=\"\n background: ${processActive ? (scheme?.accent || '#ff69b4') : 'rgba(255,255,255,0.08)'};\n color: ${processActive ? '#000' : '#888'};\n \">Proc</button>\n <button id=\"tab-sources\" class=\"hdr-btn\" style=\"\n background: ${sourceActive ? (scheme?.accent || '#ff69b4') : 'rgba(255,255,255,0.08)'};\n color: ${sourceActive ? '#000' : '#888'};\n \">Src${fileCount > 0 ? ' ' + fileCount : ''}</button>\n `;\n \n document.getElementById('tab-processes').onclick = () => setTab('processes');\n document.getElementById('tab-sources').onclick = () => setTab('sources');\n }\n \n // Source labels with rich info\n function updateSourceLabels() {\n if (!sourcesVisible) return;\n \n let container = document.getElementById('source-labels');\n if (!container) {\n container = document.createElement('div');\n container.id = 'source-labels';\n container.className = 'label-container';\n document.body.appendChild(container);\n }\n container.innerHTML = '';\n container.style.display = sourcesVisible ? 'block' : 'none';\n \n const width = window.innerWidth;\n const height = window.innerHeight;\n const camera = window.ProcessTreeViz.camera;\n \n sourceFiles.forEach((fileData, fileName) => {\n fileData.nodes.forEach((mesh) => {\n if (!mesh.visible) return;\n \n const pos = new THREE.Vector3();\n mesh.getWorldPosition(pos);\n const labelPos = pos.clone();\n labelPos.y += (mesh.userData.size || 6) + 4;\n labelPos.project(camera);\n \n const x = (labelPos.x * 0.5 + 0.5) * width;\n const y = (-labelPos.y * 0.5 + 0.5) * height;\n \n if (labelPos.z < 1 && x > -50 && x < width + 50 && y > -50 && y < height + 50) {\n const d = mesh.userData;\n const distToCamera = camera.position.distanceTo(pos);\n \n // Show more labels when zoomed in\n // Relaxed thresholds for visibility\n const important = ['Program', 'FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', \n 'VariableDeclaration', 'ImportDeclaration', 'ExportDefaultDeclaration', 'ExportNamedDeclaration',\n 'Buffer']; // Buffer files (markdown, lisp) are always visible\n const isImportant = important.includes(d.type) || d.isBuffer;\n \n if (!isImportant && distToCamera > 800) return;\n if (distToCamera > 1200) return;\n \n const proximityScale = Math.max(0.5, Math.min(2.5, 300 / distToCamera));\n const opacity = Math.max(0.6, Math.min(1, 400 / distToCamera));\n const color = '#' + (d.baseColor || 0x666666).toString(16).padStart(6, '0');\n const isFocused = focusedSourceNode === d.id;\n const isHovered = hoveredNode === d.id;\n \n const label = document.createElement('div');\n label.className = 'proc-label source-label' + (isFocused ? ' focused' : '') + (isHovered ? ' hovered' : '') + (d.isBuffer ? ' buffer' : '');\n label.style.cssText = `\n left: ${x}px; top: ${y}px;\n opacity: ${isFocused || isHovered ? 1 : opacity};\n transform: translate(-50%, -100%) scale(${isFocused || isHovered ? proximityScale * 1.3 : proximityScale});\n cursor: pointer;\n pointer-events: auto;\n ${isFocused ? 'z-index: 100;' : ''}\n text-align: center;\n `;\n \n label.innerHTML = `\n <div style=\"\n font-size: ${d.isBuffer ? '10px' : '8px'}; font-weight: bold; color: ${color};\n text-shadow: 0 1px 2px rgba(0,0,0,0.8);\n white-space: nowrap;\n background: rgba(0,0,0,0.4);\n padding: 2px 4px;\n border-radius: 4px;\n display: inline-flex; overflow: visible; align-items: center; gap: 4px;\n \">\n <span style=\"font-size: ${d.isBuffer ? '14px' : '10px'};\">${d.icon}</span>${d.displayName}\n </div>\n ${isHovered || isFocused ? `<div style=\"font-size: 7px; color: #888; margin-top: 1px;\">${d.type}</div>` : ''}\n `;\n \n // Click to focus/navigate\n \n // Click to focus/navigate\n label.onclick = (e) => {\n e.stopPropagation();\n handleNodeClick(mesh);\n };\n \n container.appendChild(label);\n }\n });\n });\n }\n \n // Click handling for source nodes\n function handleNodeClick(mesh) {\n const d = mesh.userData;\n \n if (focusedSourceNode === d.id) {\n // Double-click to navigate to source\n // Prefer filePath if available, fallback to fileName (legacy/simple)\n const targetFile = d.filePath || d.fileName;\n \n if (d.loc && targetFile) {\n // Send message to VS Code to open file at line\n const vscode = window.vscodeApi || (typeof acquireVsCodeApi !== 'undefined' ? acquireVsCodeApi() : null);\n if (vscode) {\n vscode.postMessage({\n command: 'navigateToSource',\n filePath: d.filePath, // Explicitly send both\n fileName: d.fileName,\n line: d.loc.start.line,\n column: d.loc.start.column\n });\n }\n console.log(`📍 Navigate to ${targetFile}:${d.loc.start.line}`);\n }\n focusedSourceNode = null;\n } else {\n // Single click to focus\n focusedSourceNode = d.id;\n \n // Move camera to focus on node\n const controls = window.ProcessTreeViz.controls;\n if (controls) {\n controls.target.copy(mesh.position);\n }\n }\n \n updateSourceLabels();\n }\n \n // Click detection on 3D scene\n function onCanvasClick(event) {\n if (!sourcesVisible) return;\n \n const canvas = renderer.domElement;\n const rect = canvas.getBoundingClientRect();\n mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;\n \n raycaster.setFromCamera(mouse, camera);\n \n // Get all source meshes\n const meshArray = [];\n sourceFiles.forEach(fileData => {\n fileData.nodes.forEach(mesh => {\n if (mesh.visible) meshArray.push(mesh);\n });\n });\n \n const intersects = raycaster.intersectObjects(meshArray);\n \n if (intersects.length > 0) {\n handleNodeClick(intersects[0].object);\n } else {\n focusedSourceNode = null;\n updateSourceLabels();\n }\n }\n \n // Hover detection\n function onCanvasMove(event) {\n if (!sourcesVisible) return;\n \n const canvas = renderer.domElement;\n const rect = canvas.getBoundingClientRect();\n mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;\n \n raycaster.setFromCamera(mouse, camera);\n \n const meshArray = [];\n sourceFiles.forEach(fileData => {\n fileData.nodes.forEach(mesh => {\n if (mesh.visible) meshArray.push(mesh);\n });\n });\n \n const intersects = raycaster.intersectObjects(meshArray);\n const newHovered = intersects.length > 0 ? intersects[0].object.userData.id : null;\n \n if (newHovered !== hoveredNode) {\n hoveredNode = newHovered;\n canvas.style.cursor = hoveredNode ? 'pointer' : 'default';\n }\n }\n \n // Animation hook\n let time = 0;\n function animateAST() {\n if (!sourcesVisible) return;\n \n time += 0.016;\n \n // Animate source nodes\n sourceFiles.forEach((fileData) => {\n fileData.nodes.forEach((mesh) => {\n if (!mesh.visible) return;\n const d = mesh.userData;\n if (!d.targetPos) return;\n \n // Smooth movement\n mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.06;\n mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.06;\n mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.06;\n \n // Pulse effect\n const isFocused = focusedSourceNode === d.id;\n const isHovered = hoveredNode === d.id;\n const pulseAmp = isFocused ? 0.25 : (isHovered ? 0.15 : 0.08);\n const pulse = 1 + Math.sin(time * (isFocused ? 2 : 0.8) + d.pulsePhase) * pulseAmp;\n const sizeMult = isFocused ? 1.5 : (isHovered ? 1.2 : 1);\n mesh.scale.setScalar((d.size / 6) * pulse * sizeMult);\n \n // Opacity\n mesh.material.opacity = isFocused ? 1 : (isHovered ? 0.95 : 0.85);\n });\n });\n \n // Update connections\n sourceConnections.forEach(conn => {\n if (!conn.line.visible) return;\n \n const fileData = sourceFiles.get(conn.fileName);\n if (!fileData) return;\n \n const childMesh = fileData.nodes.get(conn.childId);\n const parentMesh = fileData.nodes.get(conn.parentId);\n if (childMesh && parentMesh) {\n updateConnectionMesh(conn, childMesh.position, parentMesh.position);\n \n // Highlight connections to focused node\n const isFocusPath = focusedSourceNode && \n (conn.childId === focusedSourceNode || conn.parentId === focusedSourceNode);\n conn.line.material.opacity = isFocusPath ? 0.9 : 0.5;\n conn.line.material.color.setHex(isFocusPath ? scheme.three.connectionActive : scheme.three.connectionLine);\n }\n });\n \n updateSourceLabels();\n }\n \n // Initialize\n renderer.domElement.addEventListener('click', onCanvasClick);\n renderer.domElement.addEventListener('mousemove', onCanvasMove);\n \n // Create initial tab UI\n updateTabUI();\n \n // Keyboard shortcut: Tab to switch views\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Tab' && !e.ctrlKey && !e.altKey && !e.shiftKey) {\n e.preventDefault();\n setTab(currentTab === 'processes' ? 'sources' : 'processes');\n }\n // Escape to unfocus\n if (e.key === 'Escape' && sourcesVisible) {\n focusedSourceNode = null;\n updateSourceLabels();\n }\n });\n \n // Expose API\n window.ASTTreeViz = {\n updateASTVisualization: updateSourceVisualization,\n animateAST,\n setTab,\n getTab: () => currentTab,\n sourceFiles,\n sourceConnections,\n focusNode: (id) => { focusedSourceNode = id; },\n // Tour API\n startTour: startSourceTour,\n stopTour: stopSourceTour,\n tourNext,\n tourPrev,\n toggleTourAutoPlay,\n isTourMode: () => tourMode,\n };\n \n console.log('📜 Source Tree Visualization loaded - Press Tab to switch views');\n \n // Request initial data after a short delay to ensure VSCode API is ready\n setTimeout(() => {\n const vscode = window.vscodeApi || (typeof acquireVsCodeApi !== 'undefined' ? acquireVsCodeApi() : null);\n if (vscode) {\n console.log('📡 Requesting initial AST data...');\n vscode.postMessage({ command: 'requestAST' });\n }\n }, 200);\n\n } // End initASTVisualization\n})();\n";