Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

Replace full-screen connecting overlay with corner status log in process tree

+69 -29
+1 -1
vscode-extension/generated-views.ts
··· 1 1 // Auto-generated - DO NOT EDIT DIRECTLY 2 2 // Edit views/process-tree.js and views/ast-tree.js instead and run: node build-views.mjs 3 3 4 - export const PROCESS_TREE_JS = "// 3D Process Tree Visualization\n// Shared between VS Code extension and local dev testing\n\n(function() {\n 'use strict';\n \n // Check if we're in VS Code webview or standalone\n const isVSCode = typeof acquireVsCodeApi === 'function';\n \n // 🎨 Color Schemes (imported from color-schemes.js or embedded)\n const colorSchemes = window.AestheticColorSchemes?.schemes || {\n \"dark\": {\n \"background\": \"#181318\",\n \"backgroundAlt\": \"#141214\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#a87090\",\n \"accentBright\": \"#ff69b4\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1577752,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16738740,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"light\": {\n \"background\": \"#fcf7c5\",\n \"backgroundAlt\": \"#f5f0c0\",\n \"foreground\": \"#281e5a\",\n \"foregroundBright\": \"#281e5a\",\n \"foregroundMuted\": \"#806060\",\n \"accent\": \"#387adf\",\n \"accentBright\": \"#006400\",\n \"statusOnline\": \"#006400\",\n \"categories\": {\n \"editor\": 8405200,\n \"tui\": 13648000,\n \"bridge\": 2129984,\n \"db\": 10518528,\n \"proxy\": 2121920,\n \"ai\": 12607520,\n \"shell\": 32896,\n \"dev\": 2129984,\n \"ide\": 2121920,\n \"lsp\": 6316128,\n \"kernel\": 3701471\n },\n \"three\": {\n \"sceneBackground\": 16578501,\n \"kernelOuter\": 3701471,\n \"kernelRing\": 25600,\n \"kernelCore\": 3701471,\n \"connectionLine\": 11051136,\n \"connectionActive\": 25600,\n \"deadProcess\": 11051136\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.2)\",\n \"overlay\": \"rgba(252, 247, 197, 0.95)\"\n }\n },\n \"red\": {\n \"background\": \"#181010\",\n \"backgroundAlt\": \"#140c0c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#ff5555\",\n \"accentBright\": \"#ff8888\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1576976,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16746632,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"orange\": {\n \"background\": \"#181410\",\n \"backgroundAlt\": \"#14100c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#ffb86c\",\n \"accentBright\": \"#ffd8a8\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1578000,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16767144,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"yellow\": {\n \"background\": \"#181810\",\n \"backgroundAlt\": \"#14140c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#f1fa8c\",\n \"accentBright\": \"#ffffa0\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1579024,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16777120,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"green\": {\n \"background\": \"#101810\",\n \"backgroundAlt\": \"#0c140c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#50fa7b\",\n \"accentBright\": \"#80ffae\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1054736,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 8454062,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"blue\": {\n \"background\": \"#101418\",\n \"backgroundAlt\": \"#0c1014\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#61afef\",\n \"accentBright\": \"#8cd0ff\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1053720,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 9228543,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"indigo\": {\n \"background\": \"#121018\",\n \"backgroundAlt\": \"#0e0c14\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#6272a4\",\n \"accentBright\": \"#8be9fd\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1183768,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 9169405,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"violet\": {\n \"background\": \"#161016\",\n \"backgroundAlt\": \"#120c12\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#bd93f9\",\n \"accentBright\": \"#ff79c6\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1445910,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16742854,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"pink\": {\n \"background\": \"#181014\",\n \"backgroundAlt\": \"#140c10\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#ff79c6\",\n \"accentBright\": \"#ff9ce6\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1576980,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16751846,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"pencil\": {\n \"background\": \"#181818\",\n \"backgroundAlt\": \"#141414\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#e0e0e0\",\n \"accentBright\": \"#ffffff\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1579032,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16777215,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n }\n};\n \n // Detect theme from data attribute, URL param, VS Code CSS vars, or OS preference\n function detectTheme() {\n // Check data attribute first (set by the HTML)\n const dataTheme = document.body.dataset.theme;\n if (colorSchemes[dataTheme]) return dataTheme;\n \n // Check URL param\n const urlParams = new URLSearchParams(window.location.search);\n const urlTheme = urlParams.get('theme');\n if (colorSchemes[urlTheme]) return urlTheme;\n \n // Check VS Code CSS variables\n if (typeof getComputedStyle !== 'undefined') {\n const bgColor = getComputedStyle(document.body).getPropertyValue('--vscode-editor-background').trim();\n \n if (bgColor && bgColor.startsWith('#')) {\n // Exact match against known backgrounds\n const bgLower = bgColor.toLowerCase();\n for (const [key, scheme] of Object.entries(colorSchemes)) {\n if (scheme.background.toLowerCase() === bgLower) {\n return key;\n }\n }\n\n // Check for Light vs Dark if no exact match\n const r = parseInt(bgColor.slice(1, 3), 16);\n const g = parseInt(bgColor.slice(3, 5), 16);\n const b = parseInt(bgColor.slice(5, 7), 16);\n const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n return luminance > 0.5 ? 'light' : 'dark';\n }\n }\n \n // Fall back to OS preference (prefers-color-scheme)\n if (typeof window !== 'undefined' && window.matchMedia) {\n if (window.matchMedia('(prefers-color-scheme: light)').matches) {\n return 'light';\n }\n }\n \n return 'dark';\n }\n \n let currentTheme = detectTheme();\n let scheme = colorSchemes[currentTheme];\n let colors = scheme.categories;\n \n // Apply initial body styling based on detected theme\n document.body.style.background = scheme.background;\n document.body.style.color = scheme.foreground;\n document.body.dataset.theme = currentTheme;\n \n // Dev badge is now in the HTML for dev.html - no need to create dynamically\n \n let width = window.innerWidth, height = window.innerHeight;\n let meshes = new Map(), connections = new Map(), ws;\n let graveyard = [];\n const MAX_GRAVEYARD = 30;\n const GRAVEYARD_Y = -200;\n \n // Three.js setup\n const scene = new THREE.Scene();\n scene.background = new THREE.Color(scheme.three.sceneBackground);\n \n const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 5000);\n camera.position.set(0, 150, 400);\n camera.lookAt(0, 0, 0);\n \n const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });\n renderer.setSize(width, height);\n renderer.setPixelRatio(window.devicePixelRatio);\n renderer.setClearColor(scheme.three.sceneBackground);\n \n const controls = new THREE.OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.05;\n controls.minDistance = 20;\n controls.maxDistance = 3000;\n controls.enablePan = true;\n controls.autoRotate = true;\n controls.autoRotateSpeed = 0.3;\n controls.target.set(0, 0, 0);\n \n let focusedPid = null;\n let focusTarget = new THREE.Vector3(0, 0, 0);\n let focusDistance = null;\n let transitioning = false;\n \n // Tour mode state\n let tourMode = false;\n let tourIndex = 0;\n let tourProcessList = [];\n let tourAutoPlay = false;\n let tourAutoPlayInterval = null;\n const TOUR_SPEED = 2500; // ms between auto-advances\n \n const raycaster = new THREE.Raycaster();\n const mouse = new THREE.Vector2();\n \n renderer.domElement.addEventListener('click', (e) => {\n mouse.x = (e.clientX / width) * 2 - 1;\n mouse.y = -(e.clientY / height) * 2 + 1;\n \n raycaster.setFromCamera(mouse, camera);\n const meshArray = Array.from(meshes.values());\n const intersects = raycaster.intersectObjects(meshArray);\n \n if (intersects.length > 0) {\n const clicked = intersects[0].object;\n const pid = clicked.userData.pid;\n \n if (focusedPid === String(pid)) {\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n } else {\n focusedPid = String(pid);\n focusTarget.copy(clicked.position);\n focusDistance = 80 + (clicked.userData.size || 6) * 3;\n }\n transitioning = true;\n controls.autoRotate = true;\n } else if (!e.shiftKey) {\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n transitioning = true;\n }\n });\n \n renderer.domElement.addEventListener('dblclick', () => {\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n transitioning = true;\n camera.position.set(0, 150, 400);\n });\n \n // Tour Mode Functions\n function updateTourUI() {\n let tourUI = document.getElementById('tour-ui');\n if (!tourUI) {\n tourUI = document.createElement('div');\n tourUI.id = 'tour-ui';\n document.body.appendChild(tourUI);\n }\n // Tour UI positioned above center panel, non-overlapping\n tourUI.style.cssText = `\n position: fixed;\n bottom: 180px;\n left: 50%;\n transform: translateX(-50%);\n background: ${scheme.ui.overlay};\n padding: 16px 24px;\n border-radius: 12px;\n color: ${scheme.foregroundBright};\n font-family: monospace;\n font-size: 12px;\n z-index: 1000;\n display: none;\n text-align: center;\n border: 1px solid ${scheme.foregroundMuted}40;\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n min-width: 280px;\n `;\n \n if (tourMode && tourProcessList.length > 0) {\n const current = tourProcessList[tourIndex];\n const mesh = meshes.get(current);\n const name = mesh?.userData?.name || current;\n const icon = mesh?.userData?.icon || '●';\n const category = mesh?.userData?.category || '';\n \n tourUI.style.display = 'block';\n tourUI.innerHTML = `\n <div style=\"margin-bottom:10px;font-size:12px;color:${scheme.accent};text-transform:uppercase;letter-spacing:1px;\">🎬 Tour Mode</div>\n <div style=\"font-size:24px;margin-bottom:4px;color:${scheme.foregroundBright};\">${icon}</div>\n <div style=\"font-size:14px;font-weight:bold;color:${scheme.foregroundBright};margin-bottom:2px;\">${name}</div>\n <div style=\"color:${scheme.foregroundMuted};margin-bottom:14px;font-size:11px;\">${category} • ${tourIndex + 1}/${tourProcessList.length}</div>\n <div style=\"display:flex;gap:8px;justify-content:center;\">\n <button onclick=\"ProcessTreeViz.tourPrev()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">← Prev</button>\n <button onclick=\"ProcessTreeViz.toggleAutoPlay()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">${tourAutoPlay ? '⏸ Stop' : '▶ Auto'}</button>\n <button onclick=\"ProcessTreeViz.tourNext()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">Next →</button>\n <button onclick=\"ProcessTreeViz.exitTour()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">✕</button>\n </div>\n ${tourAutoPlay ? `<div style=\"color:${scheme.accentBright};margin-top:10px;font-size:10px;\">▶ Auto-playing...</div>` : ''}\n `;\n // Hide the tour button when in tour mode\n const btn = document.getElementById('tour-btn');\n if (btn) btn.style.display = 'none';\n } else {\n tourUI.style.display = 'none';\n // Show the tour button when not in tour mode\n const btn = document.getElementById('tour-btn');\n if (btn) btn.style.display = '';\n }\n }\n \n function buildTourList() {\n // Build ordered list: kernel first, then by category, then by tree depth\n const categoryOrder = ['kernel', 'ide', 'editor', 'tui', 'dev', 'db', 'shell', 'ai', 'lsp', 'proxy', 'bridge'];\n const list = Array.from(meshes.keys());\n \n list.sort((a, b) => {\n const meshA = meshes.get(a);\n const meshB = meshes.get(b);\n const catA = meshA?.userData?.category || 'zzz';\n const catB = meshB?.userData?.category || 'zzz';\n const orderA = categoryOrder.indexOf(catA);\n const orderB = categoryOrder.indexOf(catB);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n \n return list;\n }\n \n function focusOnProcess(pid) {\n const mesh = meshes.get(pid);\n if (!mesh) return;\n \n focusedPid = pid;\n focusTarget.copy(mesh.position);\n focusDistance = 80 + (mesh.userData.size || 6) * 3;\n transitioning = true;\n controls.autoRotate = true;\n }\n \n function startTour() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.startTour();\n // Ensure local state reflects we are \"busy\" or just let AST handle it\n return;\n }\n\n tourMode = true;\n tourProcessList = buildTourList();\n tourIndex = 0;\n if (tourProcessList.length > 0) {\n focusOnProcess(tourProcessList[0]);\n }\n updateTourUI();\n }\n \n function exitTour() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.stopTour();\n return;\n }\n\n tourMode = false;\n tourAutoPlay = false;\n if (tourAutoPlayInterval) {\n clearInterval(tourAutoPlayInterval);\n tourAutoPlayInterval = null;\n }\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n transitioning = true;\n updateTourUI();\n }\n \n function tourNext() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.tourNext();\n return;\n }\n\n if (!tourMode || tourProcessList.length === 0) return;\n tourIndex = (tourIndex + 1) % tourProcessList.length;\n focusOnProcess(tourProcessList[tourIndex]);\n updateTourUI();\n }\n \n function tourPrev() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.tourPrev();\n return;\n }\n\n if (!tourMode || tourProcessList.length === 0) return;\n tourIndex = (tourIndex - 1 + tourProcessList.length) % tourProcessList.length;\n focusOnProcess(tourProcessList[tourIndex]);\n updateTourUI();\n }\n \n function toggleAutoPlay() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.toggleTourAutoPlay();\n return;\n }\n\n tourAutoPlay = !tourAutoPlay;\n if (tourAutoPlay) {\n tourAutoPlayInterval = setInterval(tourNext, TOUR_SPEED);\n } else {\n if (tourAutoPlayInterval) {\n clearInterval(tourAutoPlayInterval);\n tourAutoPlayInterval = null;\n }\n }\n updateTourUI();\n }\n \n // Keyboard controls\n document.addEventListener('keydown', (e) => {\n const isSourceTour = window.ASTTreeViz?.getTab() === 'sources' && window.ASTTreeViz?.isTourMode();\n const isTourActive = tourMode || isSourceTour;\n\n // T to start tour\n if (e.key === 't' || e.key === 'T') {\n if (!isTourActive) {\n startTour();\n } else {\n exitTour();\n }\n return;\n }\n \n if (isTourActive) {\n switch(e.key) {\n case 'ArrowRight':\n case 'l':\n case 'L':\n tourNext();\n e.preventDefault();\n break;\n case 'ArrowLeft':\n case 'h':\n case 'H':\n tourPrev();\n e.preventDefault();\n break;\n case ' ':\n toggleAutoPlay();\n e.preventDefault();\n break;\n case 'Escape':\n case 'q':\n case 'Q':\n exitTour();\n e.preventDefault();\n break;\n }\n }\n });\n \n let processTree = { roots: [], byPid: new Map() };\n \n let kernelMesh = null, kernelGlow = null, kernelCore = null;\n function createKernelNode() {\n const group = new THREE.Group();\n \n const outerGeo = new THREE.SphereGeometry(35, 32, 32);\n const outerMat = new THREE.MeshBasicMaterial({\n color: scheme.three.kernelOuter, transparent: true, opacity: 0.15, wireframe: true\n });\n group.add(new THREE.Mesh(outerGeo, outerMat));\n \n const ringGeo = new THREE.TorusGeometry(25, 1.5, 8, 48);\n const ringMat = new THREE.MeshBasicMaterial({\n color: scheme.three.kernelRing, transparent: true, opacity: 0.4\n });\n const ring = new THREE.Mesh(ringGeo, ringMat);\n ring.rotation.x = Math.PI / 2;\n group.add(ring);\n kernelGlow = ring;\n \n const coreGeo = new THREE.SphereGeometry(12, 24, 24);\n const coreMat = new THREE.MeshBasicMaterial({\n color: scheme.three.kernelCore, transparent: true, opacity: 0.7\n });\n const core = new THREE.Mesh(coreGeo, coreMat);\n group.add(core);\n kernelCore = core;\n \n group.userData = {\n pid: 'kernel', name: 'Fedora Linux', icon: '🐧', category: 'kernel',\n cpu: 0, rss: 0, size: 35, targetPos: new THREE.Vector3(0, 0, 0), pulsePhase: 0\n };\n return group;\n }\n \n kernelMesh = createKernelNode();\n scene.add(kernelMesh);\n meshes.set('kernel', kernelMesh);\n \n function createNodeMesh(node) {\n const cpu = node.cpu || 0;\n const memMB = (node.rss || 10000) / 1024;\n const baseColor = colors[node.category] || 0x666666;\n const size = Math.max(4, Math.min(12, 3 + memMB * 0.05 + cpu * 0.1));\n \n const geo = new THREE.SphereGeometry(size, 12, 12);\n const mat = new THREE.MeshBasicMaterial({\n color: baseColor, transparent: true, opacity: 0.7 + cpu * 0.003\n });\n \n const mesh = new THREE.Mesh(geo, mat);\n mesh.userData = { \n ...node, size, baseColor, targetPos: new THREE.Vector3(),\n pulsePhase: Math.random() * Math.PI * 2\n };\n return mesh;\n }\n \n function createConnectionLine(color) {\n const geo = new THREE.CylinderGeometry(1.5, 1.5, 1, 8);\n const mat = new THREE.MeshBasicMaterial({\n color: color || scheme.three.connectionLine, transparent: true, opacity: 0.5\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 function layoutTree(processes) {\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 const roots = [];\n processes.forEach(p => {\n const parentPid = String(p.parentInteresting || 0);\n if (parentPid && byPid.has(parentPid)) {\n children.get(parentPid).push(p);\n } else {\n roots.push(p);\n }\n });\n \n const categoryOrder = ['ide', 'editor', 'tui', 'dev', 'db', 'shell', 'ai', 'lsp', 'proxy', 'bridge'];\n roots.sort((a, b) => {\n const ai = categoryOrder.indexOf(a.category);\n const bi = categoryOrder.indexOf(b.category);\n return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);\n });\n \n const levelHeight = 50, baseRadius = 100;\n \n function countDescendants(pid) {\n const nodeChildren = children.get(pid) || [];\n let count = nodeChildren.length;\n nodeChildren.forEach(c => count += countDescendants(String(c.pid)));\n return count;\n }\n \n function positionNode(node, depth, angle, radius, parentX, parentZ) {\n const pid = String(node.pid);\n const nodeChildren = children.get(pid) || [];\n const childCount = nodeChildren.length;\n \n const x = parentX + Math.cos(angle) * radius;\n const z = parentZ + Math.sin(angle) * radius;\n \n node.targetX = x;\n node.targetY = -depth * levelHeight;\n node.targetZ = z;\n \n if (childCount > 0) {\n const arcSpread = Math.min(Math.PI * 0.9, Math.PI * 0.3 * childCount);\n const startAngle = angle - arcSpread / 2;\n const childRadius = 35 + childCount * 10;\n \n nodeChildren.forEach((child, i) => {\n const childAngle = childCount === 1 ? angle : startAngle + (arcSpread / (childCount - 1)) * i;\n positionNode(child, depth + 1, childAngle, childRadius, x, z);\n });\n }\n }\n \n const totalRoots = roots.length;\n if (totalRoots > 0) {\n const weights = roots.map(r => 1 + countDescendants(String(r.pid)) * 0.5);\n const totalWeight = weights.reduce((a, b) => a + b, 0);\n \n let currentAngle = -Math.PI / 2;\n roots.forEach((root, i) => {\n const angleSpan = (weights[i] / totalWeight) * Math.PI * 2;\n const angle = currentAngle + angleSpan / 2;\n currentAngle += angleSpan;\n positionNode(root, 0, angle, baseRadius, 0, 0);\n });\n }\n \n return { roots, byPid, children };\n }\n \n function updateLabels() {\n const container = document.getElementById('labels');\n container.innerHTML = '';\n scene.updateMatrixWorld();\n \n meshes.forEach((mesh, pid) => {\n const pos = new THREE.Vector3();\n mesh.getWorldPosition(pos);\n const labelPos = pos.clone();\n labelPos.y += (mesh.userData.size || 8) + 5;\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 > -100 && x < width + 100 && y > -100 && y < height + 100) {\n const d = mesh.userData;\n const color = '#' + (colors[d.category] || 0x666666).toString(16).padStart(6, '0');\n const distToCamera = camera.position.distanceTo(pos);\n // Larger base scale, less reduction with distance\n const proximityScale = Math.max(0.7, Math.min(3, 200 / distToCamera));\n // Higher minimum opacity - always readable\n const opacity = focusedPid \n ? (pid === focusedPid ? 1 : (d.parentInteresting === parseInt(focusedPid) ? 0.95 : 0.7))\n : Math.max(0.85, Math.min(1, 400 / distToCamera));\n \n const cpuPct = Math.min(100, d.cpu || 0);\n const memMB = ((d.rss || 0) / 1024).toFixed(0);\n \n // Extract short command for display (first 40 chars of cmdShort or cmd)\n const cmdDisplay = d.cmdShort || d.cmd || '';\n const cmdShort = cmdDisplay.length > 50 ? cmdDisplay.slice(0, 47) + '...' : cmdDisplay;\n \n // Calculate rotation based on connection to parent (make label parallel to line)\n let rotation = 0;\n const parentPid = String(d.parentInteresting || 0);\n const parentMesh = meshes.has(parentPid) ? meshes.get(parentPid) : meshes.get('kernel');\n if (parentMesh && pid !== 'kernel') {\n const parentPos = new THREE.Vector3();\n parentMesh.getWorldPosition(parentPos);\n // Project both positions to 2D screen space\n const childScreen = pos.clone().project(camera);\n const parentScreen = parentPos.clone().project(camera);\n // Calculate angle in screen space\n const dx = (parentScreen.x - childScreen.x);\n const dy = (parentScreen.y - childScreen.y);\n rotation = Math.atan2(-dy, dx) * (180 / Math.PI);\n // Clamp rotation to reasonable range (-45 to 45 degrees)\n rotation = Math.max(-45, Math.min(45, rotation));\n }\n \n const label = document.createElement('div');\n label.className = 'proc-label';\n label.style.left = x + 'px';\n label.style.top = y + 'px';\n label.style.opacity = opacity;\n label.style.transform = 'translate(-50%, -100%) scale(' + proximityScale + ') rotate(' + rotation + 'deg)';\n // Show name, then command on second line, then stats (no background)\n label.innerHTML = '<div class=\"icon\">' + (d.icon || '●') + '</div>' +\n '<div class=\"name\" style=\"color:' + color + '\">' + (d.name || pid) + '</div>' +\n (cmdShort ? '<div class=\"cmd\" style=\"color:' + scheme.foregroundMuted + ';font-size:8px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\">' + cmdShort + '</div>' : '') +\n '<div class=\"info\" style=\"color:' + scheme.foregroundMuted + ';\">' + memMB + 'MB · ' + cpuPct.toFixed(0) + '%</div>';\n container.appendChild(label);\n }\n });\n \n // Add labels for graveyard (dead) processes\n graveyard.forEach((grave) => {\n const mesh = grave.mesh;\n if (!mesh) return;\n \n const pos = new THREE.Vector3();\n mesh.getWorldPosition(pos);\n const labelPos = pos.clone();\n labelPos.y += 8;\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 > -100 && x < width + 100 && y > -100 && y < height + 100) {\n const distToCamera = camera.position.distanceTo(pos);\n const proximityScale = Math.max(0.5, Math.min(2, 150 / distToCamera));\n const age = (Date.now() - grave.deathTime) / 1000;\n const opacity = Math.max(0.3, 0.7 - age * 0.01);\n \n const cmdShort = grave.cmd ? (grave.cmd.length > 30 ? grave.cmd.slice(0, 27) + '...' : grave.cmd) : '';\n const timeAgo = age < 60 ? Math.floor(age) + 's ago' : Math.floor(age / 60) + 'm ago';\n \n const label = document.createElement('div');\n label.className = 'proc-label graveyard';\n label.style.left = x + 'px';\n label.style.top = y + 'px';\n label.style.opacity = opacity;\n label.style.transform = 'translate(-50%, -100%) scale(' + proximityScale + ')';\n label.innerHTML = '<div class=\"icon\">💀</div>' +\n '<div class=\"name\" style=\"color:' + scheme.foregroundMuted + ';text-decoration:line-through;\">' + (grave.name || grave.pid) + '</div>' +\n (cmdShort ? '<div class=\"cmd\" style=\"color:' + scheme.foregroundMuted + ';font-size:7px;opacity:0.7;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\">' + cmdShort + '</div>' : '') +\n '<div class=\"info\" style=\"color:' + scheme.foregroundMuted + ';opacity:0.6;\">' + timeAgo + '</div>';\n container.appendChild(label);\n }\n });\n }\n \n function updateViz(processData) {\n if (!processData?.interesting) return;\n \n const processes = processData.interesting;\n document.getElementById('process-count').textContent = processes.length;\n \n processTree = layoutTree(processes);\n const currentPids = new Set(processes.map(p => String(p.pid)));\n \n processes.forEach(p => {\n const pid = String(p.pid);\n \n if (!meshes.has(pid)) {\n const mesh = createNodeMesh(p);\n mesh.position.set(p.targetX || 0, p.targetY || 0, p.targetZ || 0);\n mesh.userData.targetPos.set(p.targetX || 0, p.targetY || 0, p.targetZ || 0);\n scene.add(mesh);\n meshes.set(pid, mesh);\n \n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n mesh.visible = !isSourcesTab;\n } else {\n const mesh = meshes.get(pid);\n const d = mesh.userData;\n d.cpu = p.cpu; d.mem = p.mem; d.rss = p.rss; d.name = p.name;\n d.targetPos.set(p.targetX || d.targetPos.x, p.targetY || d.targetPos.y, p.targetZ || d.targetPos.z);\n \n const memMB = (p.rss || 10000) / 1024;\n d.size = Math.max(4, Math.min(12, 3 + memMB * 0.05 + p.cpu * 0.1));\n mesh.scale.setScalar(d.size / 6);\n \n const baseColor = colors[p.category] || 0x666666;\n const brighten = Math.min(1.8, 1 + p.cpu * 0.02);\n const r = ((baseColor >> 16) & 255) * brighten;\n const g = ((baseColor >> 8) & 255) * brighten;\n const b = (baseColor & 255) * brighten;\n mesh.material.color.setRGB(Math.min(255, r) / 255, Math.min(255, g) / 255, Math.min(255, b) / 255);\n mesh.material.opacity = 0.7 + p.cpu * 0.003;\n }\n \n const parentPid = String(p.parentInteresting || 0);\n const childColor = colors[p.category] || 0x666666;\n if (parentPid && meshes.has(parentPid)) {\n const connKey = pid + '->' + parentPid;\n if (!connections.has(connKey)) {\n const line = createConnectionLine(childColor);\n scene.add(line);\n connections.set(connKey, { line, childPid: pid, parentPid, childColor });\n \n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n line.visible = !isSourcesTab;\n }\n } else {\n const connKey = pid + '->kernel';\n if (!connections.has(connKey)) {\n const line = createConnectionLine(childColor);\n scene.add(line);\n connections.set(connKey, { line, childPid: pid, parentPid: 'kernel', childColor });\n \n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n line.visible = !isSourcesTab;\n }\n }\n });\n \n meshes.forEach((mesh, pid) => {\n if (pid === 'kernel') return;\n if (!currentPids.has(pid) && !mesh.userData.isDead) {\n mesh.userData.isDead = true;\n mesh.userData.deathTime = Date.now();\n \n const graveyardIndex = graveyard.length;\n const col = graveyardIndex % 10;\n const row = Math.floor(graveyardIndex / 10);\n mesh.userData.targetPos.set((col - 4.5) * 25, GRAVEYARD_Y - row * 20, 0);\n \n mesh.material.opacity = 0.25;\n mesh.material.color.setHex(scheme.three.deadProcess);\n \n // Store full process info for graveyard labels\n const d = mesh.userData;\n const graveItem = { \n pid, \n mesh, \n name: d.name,\n icon: d.icon || '💀',\n cmd: d.cmdShort || d.cmd || '',\n category: d.category,\n deathTime: Date.now() \n };\n graveyard.push(graveItem);\n meshes.delete(pid);\n\n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n mesh.visible = !isSourcesTab;\n \n while (graveyard.length > MAX_GRAVEYARD) {\n const oldest = graveyard.shift();\n scene.remove(oldest.mesh);\n if (oldest.mesh.geometry) oldest.mesh.geometry.dispose();\n if (oldest.mesh.material) oldest.mesh.material.dispose();\n }\n }\n });\n \n const graveyardPids = new Set(graveyard.map(g => g.pid));\n connections.forEach((conn, key) => {\n const childExists = meshes.has(conn.childPid) || graveyardPids.has(conn.childPid);\n const parentExists = meshes.has(conn.parentPid) || graveyardPids.has(conn.parentPid);\n if (!childExists || !parentExists) {\n scene.remove(conn.line);\n conn.line.geometry.dispose();\n conn.line.material.dispose();\n connections.delete(key);\n }\n });\n \n // Refresh tour list if in tour mode (processes may have changed)\n if (tourMode) {\n const oldPid = tourProcessList[tourIndex];\n tourProcessList = buildTourList();\n // Try to stay on the same process if it still exists\n const newIndex = tourProcessList.indexOf(oldPid);\n if (newIndex !== -1) {\n tourIndex = newIndex;\n } else if (tourIndex >= tourProcessList.length) {\n tourIndex = Math.max(0, tourProcessList.length - 1);\n }\n updateTourUI();\n }\n }\n \n let time = 0;\n function animate() {\n requestAnimationFrame(animate);\n time += 0.016;\n \n if (focusedPid && meshes.has(focusedPid)) {\n focusTarget.lerp(meshes.get(focusedPid).position, 0.08);\n }\n \n controls.target.lerp(focusTarget, transitioning ? 0.06 : 0.02);\n \n if (focusDistance !== null) {\n const currentDist = camera.position.distanceTo(controls.target);\n if (Math.abs(currentDist - focusDistance) > 5) {\n const dir = camera.position.clone().sub(controls.target).normalize();\n const targetPos = controls.target.clone().add(dir.multiplyScalar(focusDistance));\n camera.position.lerp(targetPos, 0.04);\n } else {\n transitioning = false;\n }\n } else {\n transitioning = false;\n }\n \n controls.update();\n \n if (kernelGlow) {\n kernelGlow.rotation.z = time * 0.3;\n kernelGlow.rotation.x = Math.PI / 2 + Math.sin(time * 0.5) * 0.1;\n }\n if (kernelCore) {\n const pulse = 1 + Math.sin(time * 0.8) * 0.1;\n kernelCore.scale.setScalar(pulse);\n }\n if (kernelMesh) {\n kernelMesh.rotation.y = time * 0.1;\n }\n \n // Graveyard animation with null checks\n graveyard.forEach((grave, i) => {\n const mesh = grave.mesh;\n if (mesh && mesh.userData && mesh.material) {\n const d = mesh.userData;\n mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.02;\n mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.015;\n mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.02;\n mesh.position.x += Math.sin(time * 0.3 + i) * 0.05;\n const age = (Date.now() - grave.deathTime) / 1000;\n mesh.material.opacity = Math.max(0.1, 0.3 - age * 0.005);\n }\n });\n \n // Active meshes animation with null checks\n meshes.forEach((mesh, pid) => {\n if (!mesh || !mesh.userData || !mesh.material) return;\n const d = mesh.userData;\n const cpu = d.cpu || 0;\n const isFocused = focusedPid === pid;\n const isRelated = focusedPid && (d.parentInteresting === parseInt(focusedPid) || String(d.parentInteresting) === focusedPid);\n \n mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.03;\n mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.03;\n mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.03;\n \n const float = Math.sin(time * 0.5 + d.pulsePhase) * 2;\n mesh.position.y += float * 0.02;\n \n const pulseAmp = isFocused ? 0.2 : (0.1 + cpu * 0.005);\n const pulse = 1 + Math.sin(time * (1 + cpu * 0.05) + d.pulsePhase) * pulseAmp;\n const sizeMultiplier = isFocused ? 1.5 : (isRelated ? 1.2 : 1);\n mesh.scale.setScalar((d.size / 6) * pulse * sizeMultiplier);\n \n if (focusedPid) {\n mesh.material.opacity = isFocused ? 1 : (isRelated ? 0.8 : 0.3);\n } else {\n mesh.material.opacity = 0.7 + cpu * 0.003;\n }\n });\n \n connections.forEach(conn => {\n const childMesh = meshes.get(conn.childPid);\n const parentMesh = meshes.get(conn.parentPid);\n if (childMesh && parentMesh) {\n updateConnectionMesh(conn, childMesh.position, parentMesh.position);\n const involvesFocus = focusedPid && (conn.childPid === focusedPid || conn.parentPid === focusedPid);\n conn.line.material.opacity = focusedPid ? (involvesFocus ? 0.9 : 0.15) : 0.6;\n // Use child's category color for the line (color-coded connections)\n const childCategory = childMesh.userData?.category;\n const lineColor = involvesFocus ? scheme.three.connectionActive : (colors[childCategory] || conn.childColor || scheme.three.connectionLine);\n conn.line.material.color.setHex(lineColor);\n const thickness = involvesFocus ? 2.5 : 1.5;\n conn.line.scale.x = thickness / 1.5;\n conn.line.scale.z = thickness / 1.5;\n }\n });\n \n // 🌳 AST Tree Animation (if loaded)\n if (window.ASTTreeViz?.animateAST) {\n window.ASTTreeViz.animateAST();\n }\n \n renderer.render(scene, camera);\n updateLabels();\n }\n \n // Connection state tracking\n let connectionState = 'disconnected'; // disconnected, connecting, connected\n let reconnectAttempts = 0;\n let lastConnectTime = 0;\n \n function updateConnectionUI() {\n const dot = document.getElementById('status-dot');\n let overlay = document.getElementById('connection-overlay');\n \n if (!overlay) {\n overlay = document.createElement('div');\n overlay.id = 'connection-overlay';\n overlay.style.cssText = `\n position: fixed; top: 0; left: 0; right: 0; bottom: 0;\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n background: ${scheme.ui.overlay}; z-index: 500; pointer-events: none;\n transition: opacity 0.5s ease;\n `;\n document.body.appendChild(overlay);\n }\n \n if (connectionState === 'connected') {\n dot?.classList.add('online');\n overlay.style.opacity = '0';\n setTimeout(() => { if (connectionState === 'connected') overlay.style.display = 'none'; }, 500);\n } else {\n dot?.classList.remove('online');\n overlay.style.display = 'flex';\n overlay.style.opacity = '1';\n \n const statusText = connectionState === 'connecting' \n ? `Connecting to process server...` \n : `Waiting for process server (attempt ${reconnectAttempts})`;\n const subText = reconnectAttempts > 3 \n ? 'Server may still be starting up...' \n : 'ws://127.0.0.1:7890';\n \n overlay.innerHTML = `\n <div style=\"font-size: 48px; margin-bottom: 16px;\">${connectionState === 'connecting' ? '🔄' : '⏳'}</div>\n <div style=\"font-size: 16px; color: ${scheme.foregroundBright}; margin-bottom: 8px;\">${statusText}</div>\n <div style=\"font-size: 12px; color: ${scheme.foregroundMuted};\">${subText}</div>\n ${reconnectAttempts > 5 ? `<button onclick=\"location.reload()\" style=\"margin-top: 16px; padding: 8px 16px; background: ${scheme.accent}; border: none; border-radius: 4px; color: ${scheme.foregroundBright}; cursor: pointer; pointer-events: auto;\">🔄 Refresh Page</button>` : ''}\n `;\n }\n }\n \n function connectWS() {\n connectionState = 'connecting';\n reconnectAttempts++;\n updateConnectionUI();\n \n try {\n ws = new WebSocket('ws://127.0.0.1:7890/ws');\n \n ws.onopen = () => {\n connectionState = 'connected';\n reconnectAttempts = 0;\n lastConnectTime = Date.now();\n updateConnectionUI();\n console.log('🟢 Connected to process server');\n };\n \n ws.onclose = () => {\n connectionState = 'disconnected';\n updateConnectionUI();\n // Exponential backoff: 1s, 2s, 4s, 8s, max 10s\n const delay = Math.min(1000 * Math.pow(2, Math.min(reconnectAttempts - 1, 3)), 10000);\n console.log(`🔴 Disconnected, reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);\n setTimeout(connectWS, delay);\n };\n \n ws.onerror = (err) => {\n console.log('🔴 WebSocket error:', err);\n ws.close();\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('cpus').textContent = data.system.cpus;\n const m = data.system.memory;\n document.getElementById('mem-text').textContent = m.used + ' / ' + m.total;\n \n // Update stats graph with system data\n updateStatsGraph(data.system);\n }\n updateViz(data.processes);\n } catch {}\n };\n } catch (err) {\n console.log('🔴 WebSocket creation error:', err);\n connectionState = 'disconnected';\n updateConnectionUI();\n setTimeout(connectWS, 2000);\n }\n }\n \n // 📊 Stats Graph (CPU/Memory history)\n const GRAPH_POINTS = 60; // 60 data points\n const cpuHistory = new Array(GRAPH_POINTS).fill(0);\n const memHistory = new Array(GRAPH_POINTS).fill(0);\n let statsCanvas = null;\n let statsCtx = null;\n \n function initStatsGraph() {\n statsCanvas = document.getElementById('stats-graph-canvas');\n if (statsCanvas) {\n statsCtx = statsCanvas.getContext('2d');\n // Set actual pixel dimensions for crisp rendering\n const rect = statsCanvas.getBoundingClientRect();\n statsCanvas.width = rect.width * window.devicePixelRatio;\n statsCanvas.height = rect.height * window.devicePixelRatio;\n statsCtx.scale(window.devicePixelRatio, window.devicePixelRatio);\n }\n }\n \n function updateStatsGraph(system) {\n if (!statsCtx) initStatsGraph();\n if (!statsCtx) return;\n \n // Parse memory usage\n const m = system.memory;\n let memPct = 0;\n if (m && m.used && m.total) {\n const usedNum = parseFloat(m.used.replace(/[^\\d.]/g, ''));\n const totalNum = parseFloat(m.total.replace(/[^\\d.]/g, ''));\n if (totalNum > 0) {\n memPct = (usedNum / totalNum) * 100;\n }\n }\n \n // Calculate total CPU usage from all processes\n let totalCpu = 0;\n meshes.forEach((mesh, pid) => {\n if (pid !== 'kernel' && mesh.userData.cpu) {\n totalCpu += mesh.userData.cpu;\n }\n });\n // Normalize to percentage (divide by number of CPUs)\n const numCpus = parseInt(system.cpus) || 1;\n const cpuPct = Math.min(100, totalCpu / numCpus);\n \n // Shift history and add new values\n cpuHistory.shift();\n cpuHistory.push(cpuPct);\n memHistory.shift();\n memHistory.push(memPct);\n \n // Update text labels\n const cpuEl = document.getElementById('cpu-pct');\n const memEl = document.getElementById('mem-pct');\n if (cpuEl) cpuEl.textContent = cpuPct.toFixed(1);\n if (memEl) memEl.textContent = memPct.toFixed(1);\n \n // Draw graph\n drawStatsGraph();\n }\n \n function drawStatsGraph() {\n if (!statsCtx || !statsCanvas) return;\n \n const rect = statsCanvas.getBoundingClientRect();\n const w = rect.width;\n const h = rect.height;\n \n // Clear canvas\n statsCtx.clearRect(0, 0, w, h);\n \n // Colors based on theme\n const cpuColor = currentTheme === 'light' ? '#006400' : '#50fa7b';\n const memColor = currentTheme === 'light' ? '#c71585' : '#ff79c6';\n const gridColor = currentTheme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)';\n \n // Draw grid lines\n statsCtx.strokeStyle = gridColor;\n statsCtx.lineWidth = 0.5;\n for (let i = 0; i <= 4; i++) {\n const y = (h / 4) * i;\n statsCtx.beginPath();\n statsCtx.moveTo(0, y);\n statsCtx.lineTo(w, y);\n statsCtx.stroke();\n }\n \n // Draw CPU line\n statsCtx.strokeStyle = cpuColor;\n statsCtx.lineWidth = 1.5;\n statsCtx.beginPath();\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (cpuHistory[i] / 100) * h;\n if (i === 0) statsCtx.moveTo(x, y);\n else statsCtx.lineTo(x, y);\n }\n statsCtx.stroke();\n \n // Fill CPU area (semi-transparent)\n statsCtx.fillStyle = cpuColor.replace(')', ',0.15)').replace('rgb', 'rgba').replace('#', '');\n if (cpuColor.startsWith('#')) {\n const r = parseInt(cpuColor.slice(1, 3), 16);\n const g = parseInt(cpuColor.slice(3, 5), 16);\n const b = parseInt(cpuColor.slice(5, 7), 16);\n statsCtx.fillStyle = `rgba(${r},${g},${b},0.15)`;\n }\n statsCtx.beginPath();\n statsCtx.moveTo(0, h);\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (cpuHistory[i] / 100) * h;\n statsCtx.lineTo(x, y);\n }\n statsCtx.lineTo(w, h);\n statsCtx.closePath();\n statsCtx.fill();\n \n // Draw Memory line\n statsCtx.strokeStyle = memColor;\n statsCtx.lineWidth = 1.5;\n statsCtx.beginPath();\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (memHistory[i] / 100) * h;\n if (i === 0) statsCtx.moveTo(x, y);\n else statsCtx.lineTo(x, y);\n }\n statsCtx.stroke();\n \n // Fill Memory area (semi-transparent)\n if (memColor.startsWith('#')) {\n const r = parseInt(memColor.slice(1, 3), 16);\n const g = parseInt(memColor.slice(3, 5), 16);\n const b = parseInt(memColor.slice(5, 7), 16);\n statsCtx.fillStyle = `rgba(${r},${g},${b},0.15)`;\n }\n statsCtx.beginPath();\n statsCtx.moveTo(0, h);\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (memHistory[i] / 100) * h;\n statsCtx.lineTo(x, y);\n }\n statsCtx.lineTo(w, h);\n statsCtx.closePath();\n statsCtx.fill();\n }\n \n window.addEventListener('resize', () => {\n width = window.innerWidth;\n height = window.innerHeight;\n camera.aspect = width / height;\n camera.updateProjectionMatrix();\n renderer.setSize(width, height);\n \n // Reinitialize stats graph canvas on resize\n statsCanvas = null;\n statsCtx = null;\n initStatsGraph();\n });\n \n // 🎨 Theme switching function\n function setTheme(themeName) {\n if (themeName !== 'light' && themeName !== 'dark') return;\n currentTheme = themeName;\n scheme = colorSchemes[currentTheme];\n colors = scheme.categories;\n \n // Update scene background\n scene.background.setHex(scheme.three.sceneBackground);\n renderer.setClearColor(scheme.three.sceneBackground);\n \n // Update body styling\n document.body.dataset.theme = themeName;\n document.body.style.background = scheme.background;\n document.body.style.color = scheme.foreground;\n \n // Update kernel mesh colors\n if (kernelMesh) {\n kernelMesh.children[0].material.color.setHex(scheme.three.kernelOuter);\n if (kernelGlow) kernelGlow.material.color.setHex(scheme.three.kernelRing);\n if (kernelCore) kernelCore.material.color.setHex(scheme.three.kernelCore);\n }\n \n // Update all process node colors\n meshes.forEach((mesh, pid) => {\n if (pid === 'kernel') return;\n const category = mesh.userData.category;\n const newColor = colors[category] || 0x666666;\n mesh.material.color.setHex(newColor);\n mesh.userData.baseColor = newColor;\n });\n \n // Update connections\n connections.forEach(conn => {\n conn.line.material.color.setHex(scheme.three.connectionLine);\n });\n \n // Update graveyard\n graveyard.forEach(grave => {\n if (grave.mesh && grave.mesh.material) {\n grave.mesh.material.color.setHex(scheme.three.deadProcess);\n }\n });\n \n // Update CSS styles\n updateThemeStyles();\n }\n \n function toggleTheme() {\n setTheme(currentTheme === 'dark' ? 'light' : 'dark');\n return currentTheme;\n }\n \n function updateThemeStyles() {\n // Update dynamic CSS based on theme\n let styleEl = document.getElementById('theme-dynamic-styles');\n if (!styleEl) {\n styleEl = document.createElement('style');\n styleEl.id = 'theme-dynamic-styles';\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = `\n /* Header styles */\n .title .dot { color: ${scheme.accentBright}; }\n .status-dot { background: ${scheme.accent}; }\n .status-dot.online { background: ${scheme.statusOnline}; }\n .header-center { color: ${scheme.foregroundMuted}; }\n .header-center .val { color: ${scheme.foregroundBright}; }\n .header-btn { \n background: ${currentTheme === 'light' ? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.08)'};\n border-color: ${currentTheme === 'light' ? 'rgba(0,0,0,0.15)' : 'rgba(255,255,255,0.15)'};\n color: ${scheme.foregroundBright};\n }\n .header-btn:hover { border-color: ${scheme.accentBright}; }\n \n /* Center panel styles */\n .center-panel {\n background: ${currentTheme === 'light' ? 'rgba(252,247,197,0.8)' : 'rgba(24,19,24,0.7)'};\n border-color: ${currentTheme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.08)'};\n }\n .process-counter .count { color: ${scheme.foregroundBright}; }\n .process-counter .label { color: ${scheme.foregroundMuted}; }\n \n /* Status bar styles */\n .status-bar { color: ${scheme.foregroundMuted}; }\n .dev-badge { \n background: ${scheme.accentBright}; \n color: ${currentTheme === 'light' ? '#fff' : '#000'}; \n }\n \n /* Label styles - no background, stronger text shadow */\n .proc-label { \n text-shadow: 0 0 6px ${scheme.background}, 0 0 10px ${scheme.background}, 0 0 14px ${scheme.background}; \n background: transparent;\n }\n .proc-label .info { color: ${scheme.foregroundMuted}; }\n \n /* Tour UI styles */\n #tour-ui { background: ${scheme.ui.overlay}; border-color: ${scheme.foregroundMuted}; }\n `;\n }\n \n // Apply initial theme styles\n updateThemeStyles();\n \n // Expose for external use (mock data injection, etc.)\n window.ProcessTreeViz = {\n updateViz,\n scene,\n camera,\n renderer,\n controls,\n meshes,\n connections,\n graveyard,\n // Tour mode\n startTour,\n exitTour,\n tourNext,\n tourPrev,\n toggleAutoPlay,\n isTourMode: () => tourMode,\n // Theme control\n setTheme,\n toggleTheme,\n getTheme: () => currentTheme,\n getScheme: () => scheme,\n colorSchemes\n };\n \n // Add tour button to #header-right (new structure) or .header-right (old structure)\n const headerRight = document.getElementById('header-right') || document.querySelector('.header-right');\n if (headerRight) {\n const tourBtn = document.createElement('button');\n tourBtn.id = 'tour-btn';\n tourBtn.className = 'hdr-btn';\n tourBtn.textContent = '🎬';\n tourBtn.title = 'Tour Mode';\n tourBtn.onclick = () => { \n const isSourceTour = window.ASTTreeViz?.getTab() === 'sources' && window.ASTTreeViz?.isTourMode();\n if (!tourMode && !isSourceTour) startTour(); \n else exitTour(); \n };\n headerRight.insertBefore(tourBtn, headerRight.firstChild);\n }\n \n animate();\n connectWS();\n})();"; 4 + export const PROCESS_TREE_JS = "// 3D Process Tree Visualization\n// Shared between VS Code extension and local dev testing\n\n(function() {\n 'use strict';\n \n // Check if we're in VS Code webview or standalone\n const isVSCode = typeof acquireVsCodeApi === 'function';\n \n // 🎨 Color Schemes (imported from color-schemes.js or embedded)\n const colorSchemes = window.AestheticColorSchemes?.schemes || {\n \"dark\": {\n \"background\": \"#181318\",\n \"backgroundAlt\": \"#141214\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#a87090\",\n \"accentBright\": \"#ff69b4\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1577752,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16738740,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"light\": {\n \"background\": \"#fcf7c5\",\n \"backgroundAlt\": \"#f5f0c0\",\n \"foreground\": \"#281e5a\",\n \"foregroundBright\": \"#281e5a\",\n \"foregroundMuted\": \"#806060\",\n \"accent\": \"#387adf\",\n \"accentBright\": \"#006400\",\n \"statusOnline\": \"#006400\",\n \"categories\": {\n \"editor\": 8405200,\n \"tui\": 13648000,\n \"bridge\": 2129984,\n \"db\": 10518528,\n \"proxy\": 2121920,\n \"ai\": 12607520,\n \"shell\": 32896,\n \"dev\": 2129984,\n \"ide\": 2121920,\n \"lsp\": 6316128,\n \"kernel\": 3701471\n },\n \"three\": {\n \"sceneBackground\": 16578501,\n \"kernelOuter\": 3701471,\n \"kernelRing\": 25600,\n \"kernelCore\": 3701471,\n \"connectionLine\": 11051136,\n \"connectionActive\": 25600,\n \"deadProcess\": 11051136\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.2)\",\n \"overlay\": \"rgba(252, 247, 197, 0.95)\"\n }\n },\n \"red\": {\n \"background\": \"#181010\",\n \"backgroundAlt\": \"#140c0c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#ff5555\",\n \"accentBright\": \"#ff8888\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1576976,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16746632,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"orange\": {\n \"background\": \"#181410\",\n \"backgroundAlt\": \"#14100c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#ffb86c\",\n \"accentBright\": \"#ffd8a8\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1578000,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16767144,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"yellow\": {\n \"background\": \"#181810\",\n \"backgroundAlt\": \"#14140c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#f1fa8c\",\n \"accentBright\": \"#ffffa0\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1579024,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16777120,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"green\": {\n \"background\": \"#101810\",\n \"backgroundAlt\": \"#0c140c\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#50fa7b\",\n \"accentBright\": \"#80ffae\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1054736,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 8454062,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"blue\": {\n \"background\": \"#101418\",\n \"backgroundAlt\": \"#0c1014\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#61afef\",\n \"accentBright\": \"#8cd0ff\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1053720,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 9228543,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"indigo\": {\n \"background\": \"#121018\",\n \"backgroundAlt\": \"#0e0c14\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#6272a4\",\n \"accentBright\": \"#8be9fd\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1183768,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 9169405,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"violet\": {\n \"background\": \"#161016\",\n \"backgroundAlt\": \"#120c12\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#bd93f9\",\n \"accentBright\": \"#ff79c6\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1445910,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16742854,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"pink\": {\n \"background\": \"#181014\",\n \"backgroundAlt\": \"#140c10\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#ff79c6\",\n \"accentBright\": \"#ff9ce6\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1576980,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16751846,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n },\n \"pencil\": {\n \"background\": \"#181818\",\n \"backgroundAlt\": \"#141414\",\n \"foreground\": \"#ffffffcc\",\n \"foregroundBright\": \"#ffffff\",\n \"foregroundMuted\": \"#555555\",\n \"accent\": \"#e0e0e0\",\n \"accentBright\": \"#ffffff\",\n \"statusOnline\": \"#0f0\",\n \"categories\": {\n \"editor\": 11561983,\n \"tui\": 16738740,\n \"bridge\": 7077791,\n \"db\": 16771947,\n \"proxy\": 7053311,\n \"ai\": 16752491,\n \"shell\": 7077887,\n \"dev\": 7077791,\n \"ide\": 7053311,\n \"lsp\": 8947848,\n \"kernel\": 8965375\n },\n \"three\": {\n \"sceneBackground\": 1579032,\n \"kernelOuter\": 4491519,\n \"kernelRing\": 6728447,\n \"kernelCore\": 8965375,\n \"connectionLine\": 4473924,\n \"connectionActive\": 16777215,\n \"deadProcess\": 4473924\n },\n \"ui\": {\n \"shadow\": \"rgba(0, 0, 0, 0.6)\",\n \"overlay\": \"rgba(0, 0, 0, 0.85)\"\n }\n }\n};\n \n // Detect theme from data attribute, URL param, VS Code CSS vars, or OS preference\n function detectTheme() {\n // Check data attribute first (set by the HTML)\n const dataTheme = document.body.dataset.theme;\n if (colorSchemes[dataTheme]) return dataTheme;\n \n // Check URL param\n const urlParams = new URLSearchParams(window.location.search);\n const urlTheme = urlParams.get('theme');\n if (colorSchemes[urlTheme]) return urlTheme;\n \n // Check VS Code CSS variables\n if (typeof getComputedStyle !== 'undefined') {\n const bgColor = getComputedStyle(document.body).getPropertyValue('--vscode-editor-background').trim();\n \n if (bgColor && bgColor.startsWith('#')) {\n // Exact match against known backgrounds\n const bgLower = bgColor.toLowerCase();\n for (const [key, scheme] of Object.entries(colorSchemes)) {\n if (scheme.background.toLowerCase() === bgLower) {\n return key;\n }\n }\n\n // Check for Light vs Dark if no exact match\n const r = parseInt(bgColor.slice(1, 3), 16);\n const g = parseInt(bgColor.slice(3, 5), 16);\n const b = parseInt(bgColor.slice(5, 7), 16);\n const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n return luminance > 0.5 ? 'light' : 'dark';\n }\n }\n \n // Fall back to OS preference (prefers-color-scheme)\n if (typeof window !== 'undefined' && window.matchMedia) {\n if (window.matchMedia('(prefers-color-scheme: light)').matches) {\n return 'light';\n }\n }\n \n return 'dark';\n }\n \n let currentTheme = detectTheme();\n let scheme = colorSchemes[currentTheme];\n let colors = scheme.categories;\n \n // Apply initial body styling based on detected theme\n document.body.style.background = scheme.background;\n document.body.style.color = scheme.foreground;\n document.body.dataset.theme = currentTheme;\n \n // Dev badge is now in the HTML for dev.html - no need to create dynamically\n \n let width = window.innerWidth, height = window.innerHeight;\n let meshes = new Map(), connections = new Map(), ws;\n let graveyard = [];\n const MAX_GRAVEYARD = 30;\n const GRAVEYARD_Y = -200;\n \n // Three.js setup\n const scene = new THREE.Scene();\n scene.background = new THREE.Color(scheme.three.sceneBackground);\n \n const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 5000);\n camera.position.set(0, 150, 400);\n camera.lookAt(0, 0, 0);\n \n const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });\n renderer.setSize(width, height);\n renderer.setPixelRatio(window.devicePixelRatio);\n renderer.setClearColor(scheme.three.sceneBackground);\n \n const controls = new THREE.OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.05;\n controls.minDistance = 20;\n controls.maxDistance = 3000;\n controls.enablePan = true;\n controls.autoRotate = true;\n controls.autoRotateSpeed = 0.3;\n controls.target.set(0, 0, 0);\n \n let focusedPid = null;\n let focusTarget = new THREE.Vector3(0, 0, 0);\n let focusDistance = null;\n let transitioning = false;\n \n // Tour mode state\n let tourMode = false;\n let tourIndex = 0;\n let tourProcessList = [];\n let tourAutoPlay = false;\n let tourAutoPlayInterval = null;\n const TOUR_SPEED = 2500; // ms between auto-advances\n \n const raycaster = new THREE.Raycaster();\n const mouse = new THREE.Vector2();\n \n renderer.domElement.addEventListener('click', (e) => {\n mouse.x = (e.clientX / width) * 2 - 1;\n mouse.y = -(e.clientY / height) * 2 + 1;\n \n raycaster.setFromCamera(mouse, camera);\n const meshArray = Array.from(meshes.values());\n const intersects = raycaster.intersectObjects(meshArray);\n \n if (intersects.length > 0) {\n const clicked = intersects[0].object;\n const pid = clicked.userData.pid;\n \n if (focusedPid === String(pid)) {\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n } else {\n focusedPid = String(pid);\n focusTarget.copy(clicked.position);\n focusDistance = 80 + (clicked.userData.size || 6) * 3;\n }\n transitioning = true;\n controls.autoRotate = true;\n } else if (!e.shiftKey) {\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n transitioning = true;\n }\n });\n \n renderer.domElement.addEventListener('dblclick', () => {\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n transitioning = true;\n camera.position.set(0, 150, 400);\n });\n \n // Tour Mode Functions\n function updateTourUI() {\n let tourUI = document.getElementById('tour-ui');\n if (!tourUI) {\n tourUI = document.createElement('div');\n tourUI.id = 'tour-ui';\n document.body.appendChild(tourUI);\n }\n // Tour UI positioned above center panel, non-overlapping\n tourUI.style.cssText = `\n position: fixed;\n bottom: 180px;\n left: 50%;\n transform: translateX(-50%);\n background: ${scheme.ui.overlay};\n padding: 16px 24px;\n border-radius: 12px;\n color: ${scheme.foregroundBright};\n font-family: monospace;\n font-size: 12px;\n z-index: 1000;\n display: none;\n text-align: center;\n border: 1px solid ${scheme.foregroundMuted}40;\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n min-width: 280px;\n `;\n \n if (tourMode && tourProcessList.length > 0) {\n const current = tourProcessList[tourIndex];\n const mesh = meshes.get(current);\n const name = mesh?.userData?.name || current;\n const icon = mesh?.userData?.icon || '●';\n const category = mesh?.userData?.category || '';\n \n tourUI.style.display = 'block';\n tourUI.innerHTML = `\n <div style=\"margin-bottom:10px;font-size:12px;color:${scheme.accent};text-transform:uppercase;letter-spacing:1px;\">🎬 Tour Mode</div>\n <div style=\"font-size:24px;margin-bottom:4px;color:${scheme.foregroundBright};\">${icon}</div>\n <div style=\"font-size:14px;font-weight:bold;color:${scheme.foregroundBright};margin-bottom:2px;\">${name}</div>\n <div style=\"color:${scheme.foregroundMuted};margin-bottom:14px;font-size:11px;\">${category} • ${tourIndex + 1}/${tourProcessList.length}</div>\n <div style=\"display:flex;gap:8px;justify-content:center;\">\n <button onclick=\"ProcessTreeViz.tourPrev()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">← Prev</button>\n <button onclick=\"ProcessTreeViz.toggleAutoPlay()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">${tourAutoPlay ? '⏸ Stop' : '▶ Auto'}</button>\n <button onclick=\"ProcessTreeViz.tourNext()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">Next →</button>\n <button onclick=\"ProcessTreeViz.exitTour()\" class=\"header-btn\" style=\"pointer-events:auto;padding:8px 14px;\">✕</button>\n </div>\n ${tourAutoPlay ? `<div style=\"color:${scheme.accentBright};margin-top:10px;font-size:10px;\">▶ Auto-playing...</div>` : ''}\n `;\n // Hide the tour button when in tour mode\n const btn = document.getElementById('tour-btn');\n if (btn) btn.style.display = 'none';\n } else {\n tourUI.style.display = 'none';\n // Show the tour button when not in tour mode\n const btn = document.getElementById('tour-btn');\n if (btn) btn.style.display = '';\n }\n }\n \n function buildTourList() {\n // Build ordered list: kernel first, then by category, then by tree depth\n const categoryOrder = ['kernel', 'ide', 'editor', 'tui', 'dev', 'db', 'shell', 'ai', 'lsp', 'proxy', 'bridge'];\n const list = Array.from(meshes.keys());\n \n list.sort((a, b) => {\n const meshA = meshes.get(a);\n const meshB = meshes.get(b);\n const catA = meshA?.userData?.category || 'zzz';\n const catB = meshB?.userData?.category || 'zzz';\n const orderA = categoryOrder.indexOf(catA);\n const orderB = categoryOrder.indexOf(catB);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n \n return list;\n }\n \n function focusOnProcess(pid) {\n const mesh = meshes.get(pid);\n if (!mesh) return;\n \n focusedPid = pid;\n focusTarget.copy(mesh.position);\n focusDistance = 80 + (mesh.userData.size || 6) * 3;\n transitioning = true;\n controls.autoRotate = true;\n }\n \n function startTour() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.startTour();\n // Ensure local state reflects we are \"busy\" or just let AST handle it\n return;\n }\n\n tourMode = true;\n tourProcessList = buildTourList();\n tourIndex = 0;\n if (tourProcessList.length > 0) {\n focusOnProcess(tourProcessList[0]);\n }\n updateTourUI();\n }\n \n function exitTour() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.stopTour();\n return;\n }\n\n tourMode = false;\n tourAutoPlay = false;\n if (tourAutoPlayInterval) {\n clearInterval(tourAutoPlayInterval);\n tourAutoPlayInterval = null;\n }\n focusedPid = null;\n focusTarget.set(0, 0, 0);\n focusDistance = null;\n transitioning = true;\n updateTourUI();\n }\n \n function tourNext() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.tourNext();\n return;\n }\n\n if (!tourMode || tourProcessList.length === 0) return;\n tourIndex = (tourIndex + 1) % tourProcessList.length;\n focusOnProcess(tourProcessList[tourIndex]);\n updateTourUI();\n }\n \n function tourPrev() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.tourPrev();\n return;\n }\n\n if (!tourMode || tourProcessList.length === 0) return;\n tourIndex = (tourIndex - 1 + tourProcessList.length) % tourProcessList.length;\n focusOnProcess(tourProcessList[tourIndex]);\n updateTourUI();\n }\n \n function toggleAutoPlay() {\n if (window.ASTTreeViz?.getTab() === 'sources') {\n window.ASTTreeViz.toggleTourAutoPlay();\n return;\n }\n\n tourAutoPlay = !tourAutoPlay;\n if (tourAutoPlay) {\n tourAutoPlayInterval = setInterval(tourNext, TOUR_SPEED);\n } else {\n if (tourAutoPlayInterval) {\n clearInterval(tourAutoPlayInterval);\n tourAutoPlayInterval = null;\n }\n }\n updateTourUI();\n }\n \n // Keyboard controls\n document.addEventListener('keydown', (e) => {\n const isSourceTour = window.ASTTreeViz?.getTab() === 'sources' && window.ASTTreeViz?.isTourMode();\n const isTourActive = tourMode || isSourceTour;\n\n // T to start tour\n if (e.key === 't' || e.key === 'T') {\n if (!isTourActive) {\n startTour();\n } else {\n exitTour();\n }\n return;\n }\n \n if (isTourActive) {\n switch(e.key) {\n case 'ArrowRight':\n case 'l':\n case 'L':\n tourNext();\n e.preventDefault();\n break;\n case 'ArrowLeft':\n case 'h':\n case 'H':\n tourPrev();\n e.preventDefault();\n break;\n case ' ':\n toggleAutoPlay();\n e.preventDefault();\n break;\n case 'Escape':\n case 'q':\n case 'Q':\n exitTour();\n e.preventDefault();\n break;\n }\n }\n });\n \n let processTree = { roots: [], byPid: new Map() };\n \n let kernelMesh = null, kernelGlow = null, kernelCore = null;\n function createKernelNode() {\n const group = new THREE.Group();\n \n const outerGeo = new THREE.SphereGeometry(35, 32, 32);\n const outerMat = new THREE.MeshBasicMaterial({\n color: scheme.three.kernelOuter, transparent: true, opacity: 0.15, wireframe: true\n });\n group.add(new THREE.Mesh(outerGeo, outerMat));\n \n const ringGeo = new THREE.TorusGeometry(25, 1.5, 8, 48);\n const ringMat = new THREE.MeshBasicMaterial({\n color: scheme.three.kernelRing, transparent: true, opacity: 0.4\n });\n const ring = new THREE.Mesh(ringGeo, ringMat);\n ring.rotation.x = Math.PI / 2;\n group.add(ring);\n kernelGlow = ring;\n \n const coreGeo = new THREE.SphereGeometry(12, 24, 24);\n const coreMat = new THREE.MeshBasicMaterial({\n color: scheme.three.kernelCore, transparent: true, opacity: 0.7\n });\n const core = new THREE.Mesh(coreGeo, coreMat);\n group.add(core);\n kernelCore = core;\n \n group.userData = {\n pid: 'kernel', name: 'Fedora Linux', icon: '🐧', category: 'kernel',\n cpu: 0, rss: 0, size: 35, targetPos: new THREE.Vector3(0, 0, 0), pulsePhase: 0\n };\n return group;\n }\n \n kernelMesh = createKernelNode();\n scene.add(kernelMesh);\n meshes.set('kernel', kernelMesh);\n \n function createNodeMesh(node) {\n const cpu = node.cpu || 0;\n const memMB = (node.rss || 10000) / 1024;\n const baseColor = colors[node.category] || 0x666666;\n const size = Math.max(4, Math.min(12, 3 + memMB * 0.05 + cpu * 0.1));\n \n const geo = new THREE.SphereGeometry(size, 12, 12);\n const mat = new THREE.MeshBasicMaterial({\n color: baseColor, transparent: true, opacity: 0.7 + cpu * 0.003\n });\n \n const mesh = new THREE.Mesh(geo, mat);\n mesh.userData = { \n ...node, size, baseColor, targetPos: new THREE.Vector3(),\n pulsePhase: Math.random() * Math.PI * 2\n };\n return mesh;\n }\n \n function createConnectionLine(color) {\n const geo = new THREE.CylinderGeometry(1.5, 1.5, 1, 8);\n const mat = new THREE.MeshBasicMaterial({\n color: color || scheme.three.connectionLine, transparent: true, opacity: 0.5\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 function layoutTree(processes) {\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 const roots = [];\n processes.forEach(p => {\n const parentPid = String(p.parentInteresting || 0);\n if (parentPid && byPid.has(parentPid)) {\n children.get(parentPid).push(p);\n } else {\n roots.push(p);\n }\n });\n \n const categoryOrder = ['ide', 'editor', 'tui', 'dev', 'db', 'shell', 'ai', 'lsp', 'proxy', 'bridge'];\n roots.sort((a, b) => {\n const ai = categoryOrder.indexOf(a.category);\n const bi = categoryOrder.indexOf(b.category);\n return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);\n });\n \n const levelHeight = 50, baseRadius = 100;\n \n function countDescendants(pid) {\n const nodeChildren = children.get(pid) || [];\n let count = nodeChildren.length;\n nodeChildren.forEach(c => count += countDescendants(String(c.pid)));\n return count;\n }\n \n function positionNode(node, depth, angle, radius, parentX, parentZ) {\n const pid = String(node.pid);\n const nodeChildren = children.get(pid) || [];\n const childCount = nodeChildren.length;\n \n const x = parentX + Math.cos(angle) * radius;\n const z = parentZ + Math.sin(angle) * radius;\n \n node.targetX = x;\n node.targetY = -depth * levelHeight;\n node.targetZ = z;\n \n if (childCount > 0) {\n const arcSpread = Math.min(Math.PI * 0.9, Math.PI * 0.3 * childCount);\n const startAngle = angle - arcSpread / 2;\n const childRadius = 35 + childCount * 10;\n \n nodeChildren.forEach((child, i) => {\n const childAngle = childCount === 1 ? angle : startAngle + (arcSpread / (childCount - 1)) * i;\n positionNode(child, depth + 1, childAngle, childRadius, x, z);\n });\n }\n }\n \n const totalRoots = roots.length;\n if (totalRoots > 0) {\n const weights = roots.map(r => 1 + countDescendants(String(r.pid)) * 0.5);\n const totalWeight = weights.reduce((a, b) => a + b, 0);\n \n let currentAngle = -Math.PI / 2;\n roots.forEach((root, i) => {\n const angleSpan = (weights[i] / totalWeight) * Math.PI * 2;\n const angle = currentAngle + angleSpan / 2;\n currentAngle += angleSpan;\n positionNode(root, 0, angle, baseRadius, 0, 0);\n });\n }\n \n return { roots, byPid, children };\n }\n \n function updateLabels() {\n const container = document.getElementById('labels');\n container.innerHTML = '';\n scene.updateMatrixWorld();\n \n meshes.forEach((mesh, pid) => {\n const pos = new THREE.Vector3();\n mesh.getWorldPosition(pos);\n const labelPos = pos.clone();\n labelPos.y += (mesh.userData.size || 8) + 5;\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 > -100 && x < width + 100 && y > -100 && y < height + 100) {\n const d = mesh.userData;\n const color = '#' + (colors[d.category] || 0x666666).toString(16).padStart(6, '0');\n const distToCamera = camera.position.distanceTo(pos);\n // Larger base scale, less reduction with distance\n const proximityScale = Math.max(0.7, Math.min(3, 200 / distToCamera));\n // Higher minimum opacity - always readable\n const opacity = focusedPid \n ? (pid === focusedPid ? 1 : (d.parentInteresting === parseInt(focusedPid) ? 0.95 : 0.7))\n : Math.max(0.85, Math.min(1, 400 / distToCamera));\n \n const cpuPct = Math.min(100, d.cpu || 0);\n const memMB = ((d.rss || 0) / 1024).toFixed(0);\n \n // Extract short command for display (first 40 chars of cmdShort or cmd)\n const cmdDisplay = d.cmdShort || d.cmd || '';\n const cmdShort = cmdDisplay.length > 50 ? cmdDisplay.slice(0, 47) + '...' : cmdDisplay;\n \n // Calculate rotation based on connection to parent (make label parallel to line)\n let rotation = 0;\n const parentPid = String(d.parentInteresting || 0);\n const parentMesh = meshes.has(parentPid) ? meshes.get(parentPid) : meshes.get('kernel');\n if (parentMesh && pid !== 'kernel') {\n const parentPos = new THREE.Vector3();\n parentMesh.getWorldPosition(parentPos);\n // Project both positions to 2D screen space\n const childScreen = pos.clone().project(camera);\n const parentScreen = parentPos.clone().project(camera);\n // Calculate angle in screen space\n const dx = (parentScreen.x - childScreen.x);\n const dy = (parentScreen.y - childScreen.y);\n rotation = Math.atan2(-dy, dx) * (180 / Math.PI);\n // Clamp rotation to reasonable range (-45 to 45 degrees)\n rotation = Math.max(-45, Math.min(45, rotation));\n }\n \n const label = document.createElement('div');\n label.className = 'proc-label';\n label.style.left = x + 'px';\n label.style.top = y + 'px';\n label.style.opacity = opacity;\n label.style.transform = 'translate(-50%, -100%) scale(' + proximityScale + ') rotate(' + rotation + 'deg)';\n // Show name, then command on second line, then stats (no background)\n label.innerHTML = '<div class=\"icon\">' + (d.icon || '●') + '</div>' +\n '<div class=\"name\" style=\"color:' + color + '\">' + (d.name || pid) + '</div>' +\n (cmdShort ? '<div class=\"cmd\" style=\"color:' + scheme.foregroundMuted + ';font-size:8px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\">' + cmdShort + '</div>' : '') +\n '<div class=\"info\" style=\"color:' + scheme.foregroundMuted + ';\">' + memMB + 'MB · ' + cpuPct.toFixed(0) + '%</div>';\n container.appendChild(label);\n }\n });\n \n // Add labels for graveyard (dead) processes\n graveyard.forEach((grave) => {\n const mesh = grave.mesh;\n if (!mesh) return;\n \n const pos = new THREE.Vector3();\n mesh.getWorldPosition(pos);\n const labelPos = pos.clone();\n labelPos.y += 8;\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 > -100 && x < width + 100 && y > -100 && y < height + 100) {\n const distToCamera = camera.position.distanceTo(pos);\n const proximityScale = Math.max(0.5, Math.min(2, 150 / distToCamera));\n const age = (Date.now() - grave.deathTime) / 1000;\n const opacity = Math.max(0.3, 0.7 - age * 0.01);\n \n const cmdShort = grave.cmd ? (grave.cmd.length > 30 ? grave.cmd.slice(0, 27) + '...' : grave.cmd) : '';\n const timeAgo = age < 60 ? Math.floor(age) + 's ago' : Math.floor(age / 60) + 'm ago';\n \n const label = document.createElement('div');\n label.className = 'proc-label graveyard';\n label.style.left = x + 'px';\n label.style.top = y + 'px';\n label.style.opacity = opacity;\n label.style.transform = 'translate(-50%, -100%) scale(' + proximityScale + ')';\n label.innerHTML = '<div class=\"icon\">💀</div>' +\n '<div class=\"name\" style=\"color:' + scheme.foregroundMuted + ';text-decoration:line-through;\">' + (grave.name || grave.pid) + '</div>' +\n (cmdShort ? '<div class=\"cmd\" style=\"color:' + scheme.foregroundMuted + ';font-size:7px;opacity:0.7;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\">' + cmdShort + '</div>' : '') +\n '<div class=\"info\" style=\"color:' + scheme.foregroundMuted + ';opacity:0.6;\">' + timeAgo + '</div>';\n container.appendChild(label);\n }\n });\n }\n \n function updateViz(processData) {\n if (!processData?.interesting) return;\n \n const processes = processData.interesting;\n document.getElementById('process-count').textContent = processes.length;\n \n processTree = layoutTree(processes);\n const currentPids = new Set(processes.map(p => String(p.pid)));\n \n processes.forEach(p => {\n const pid = String(p.pid);\n \n if (!meshes.has(pid)) {\n const mesh = createNodeMesh(p);\n mesh.position.set(p.targetX || 0, p.targetY || 0, p.targetZ || 0);\n mesh.userData.targetPos.set(p.targetX || 0, p.targetY || 0, p.targetZ || 0);\n scene.add(mesh);\n meshes.set(pid, mesh);\n \n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n mesh.visible = !isSourcesTab;\n } else {\n const mesh = meshes.get(pid);\n const d = mesh.userData;\n d.cpu = p.cpu; d.mem = p.mem; d.rss = p.rss; d.name = p.name;\n d.targetPos.set(p.targetX || d.targetPos.x, p.targetY || d.targetPos.y, p.targetZ || d.targetPos.z);\n \n const memMB = (p.rss || 10000) / 1024;\n d.size = Math.max(4, Math.min(12, 3 + memMB * 0.05 + p.cpu * 0.1));\n mesh.scale.setScalar(d.size / 6);\n \n const baseColor = colors[p.category] || 0x666666;\n const brighten = Math.min(1.8, 1 + p.cpu * 0.02);\n const r = ((baseColor >> 16) & 255) * brighten;\n const g = ((baseColor >> 8) & 255) * brighten;\n const b = (baseColor & 255) * brighten;\n mesh.material.color.setRGB(Math.min(255, r) / 255, Math.min(255, g) / 255, Math.min(255, b) / 255);\n mesh.material.opacity = 0.7 + p.cpu * 0.003;\n }\n \n const parentPid = String(p.parentInteresting || 0);\n const childColor = colors[p.category] || 0x666666;\n if (parentPid && meshes.has(parentPid)) {\n const connKey = pid + '->' + parentPid;\n if (!connections.has(connKey)) {\n const line = createConnectionLine(childColor);\n scene.add(line);\n connections.set(connKey, { line, childPid: pid, parentPid, childColor });\n \n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n line.visible = !isSourcesTab;\n }\n } else {\n const connKey = pid + '->kernel';\n if (!connections.has(connKey)) {\n const line = createConnectionLine(childColor);\n scene.add(line);\n connections.set(connKey, { line, childPid: pid, parentPid: 'kernel', childColor });\n \n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n line.visible = !isSourcesTab;\n }\n }\n });\n \n meshes.forEach((mesh, pid) => {\n if (pid === 'kernel') return;\n if (!currentPids.has(pid) && !mesh.userData.isDead) {\n mesh.userData.isDead = true;\n mesh.userData.deathTime = Date.now();\n \n const graveyardIndex = graveyard.length;\n const col = graveyardIndex % 10;\n const row = Math.floor(graveyardIndex / 10);\n mesh.userData.targetPos.set((col - 4.5) * 25, GRAVEYARD_Y - row * 20, 0);\n \n mesh.material.opacity = 0.25;\n mesh.material.color.setHex(scheme.three.deadProcess);\n \n // Store full process info for graveyard labels\n const d = mesh.userData;\n const graveItem = { \n pid, \n mesh, \n name: d.name,\n icon: d.icon || '💀',\n cmd: d.cmdShort || d.cmd || '',\n category: d.category,\n deathTime: Date.now() \n };\n graveyard.push(graveItem);\n meshes.delete(pid);\n\n // Respect current visibility\n const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources';\n mesh.visible = !isSourcesTab;\n \n while (graveyard.length > MAX_GRAVEYARD) {\n const oldest = graveyard.shift();\n scene.remove(oldest.mesh);\n if (oldest.mesh.geometry) oldest.mesh.geometry.dispose();\n if (oldest.mesh.material) oldest.mesh.material.dispose();\n }\n }\n });\n \n const graveyardPids = new Set(graveyard.map(g => g.pid));\n connections.forEach((conn, key) => {\n const childExists = meshes.has(conn.childPid) || graveyardPids.has(conn.childPid);\n const parentExists = meshes.has(conn.parentPid) || graveyardPids.has(conn.parentPid);\n if (!childExists || !parentExists) {\n scene.remove(conn.line);\n conn.line.geometry.dispose();\n conn.line.material.dispose();\n connections.delete(key);\n }\n });\n \n // Refresh tour list if in tour mode (processes may have changed)\n if (tourMode) {\n const oldPid = tourProcessList[tourIndex];\n tourProcessList = buildTourList();\n // Try to stay on the same process if it still exists\n const newIndex = tourProcessList.indexOf(oldPid);\n if (newIndex !== -1) {\n tourIndex = newIndex;\n } else if (tourIndex >= tourProcessList.length) {\n tourIndex = Math.max(0, tourProcessList.length - 1);\n }\n updateTourUI();\n }\n }\n \n let time = 0;\n function animate() {\n requestAnimationFrame(animate);\n time += 0.016;\n \n if (focusedPid && meshes.has(focusedPid)) {\n focusTarget.lerp(meshes.get(focusedPid).position, 0.08);\n }\n \n controls.target.lerp(focusTarget, transitioning ? 0.06 : 0.02);\n \n if (focusDistance !== null) {\n const currentDist = camera.position.distanceTo(controls.target);\n if (Math.abs(currentDist - focusDistance) > 5) {\n const dir = camera.position.clone().sub(controls.target).normalize();\n const targetPos = controls.target.clone().add(dir.multiplyScalar(focusDistance));\n camera.position.lerp(targetPos, 0.04);\n } else {\n transitioning = false;\n }\n } else {\n transitioning = false;\n }\n \n controls.update();\n \n if (kernelGlow) {\n kernelGlow.rotation.z = time * 0.3;\n kernelGlow.rotation.x = Math.PI / 2 + Math.sin(time * 0.5) * 0.1;\n }\n if (kernelCore) {\n const pulse = 1 + Math.sin(time * 0.8) * 0.1;\n kernelCore.scale.setScalar(pulse);\n }\n if (kernelMesh) {\n kernelMesh.rotation.y = time * 0.1;\n }\n \n // Graveyard animation with null checks\n graveyard.forEach((grave, i) => {\n const mesh = grave.mesh;\n if (mesh && mesh.userData && mesh.material) {\n const d = mesh.userData;\n mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.02;\n mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.015;\n mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.02;\n mesh.position.x += Math.sin(time * 0.3 + i) * 0.05;\n const age = (Date.now() - grave.deathTime) / 1000;\n mesh.material.opacity = Math.max(0.1, 0.3 - age * 0.005);\n }\n });\n \n // Active meshes animation with null checks\n meshes.forEach((mesh, pid) => {\n if (!mesh || !mesh.userData || !mesh.material) return;\n const d = mesh.userData;\n const cpu = d.cpu || 0;\n const isFocused = focusedPid === pid;\n const isRelated = focusedPid && (d.parentInteresting === parseInt(focusedPid) || String(d.parentInteresting) === focusedPid);\n \n mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.03;\n mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.03;\n mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.03;\n \n const float = Math.sin(time * 0.5 + d.pulsePhase) * 2;\n mesh.position.y += float * 0.02;\n \n const pulseAmp = isFocused ? 0.2 : (0.1 + cpu * 0.005);\n const pulse = 1 + Math.sin(time * (1 + cpu * 0.05) + d.pulsePhase) * pulseAmp;\n const sizeMultiplier = isFocused ? 1.5 : (isRelated ? 1.2 : 1);\n mesh.scale.setScalar((d.size / 6) * pulse * sizeMultiplier);\n \n if (focusedPid) {\n mesh.material.opacity = isFocused ? 1 : (isRelated ? 0.8 : 0.3);\n } else {\n mesh.material.opacity = 0.7 + cpu * 0.003;\n }\n });\n \n connections.forEach(conn => {\n const childMesh = meshes.get(conn.childPid);\n const parentMesh = meshes.get(conn.parentPid);\n if (childMesh && parentMesh) {\n updateConnectionMesh(conn, childMesh.position, parentMesh.position);\n const involvesFocus = focusedPid && (conn.childPid === focusedPid || conn.parentPid === focusedPid);\n conn.line.material.opacity = focusedPid ? (involvesFocus ? 0.9 : 0.15) : 0.6;\n // Use child's category color for the line (color-coded connections)\n const childCategory = childMesh.userData?.category;\n const lineColor = involvesFocus ? scheme.three.connectionActive : (colors[childCategory] || conn.childColor || scheme.three.connectionLine);\n conn.line.material.color.setHex(lineColor);\n const thickness = involvesFocus ? 2.5 : 1.5;\n conn.line.scale.x = thickness / 1.5;\n conn.line.scale.z = thickness / 1.5;\n }\n });\n \n // 🌳 AST Tree Animation (if loaded)\n if (window.ASTTreeViz?.animateAST) {\n window.ASTTreeViz.animateAST();\n }\n \n renderer.render(scene, camera);\n updateLabels();\n }\n \n // Connection state tracking\n let connectionState = 'disconnected'; // disconnected, connecting, connected\n let reconnectAttempts = 0;\n let lastConnectTime = 0;\n \n // Connection log messages for the corner indicator\n const connectionLog = [];\n const MAX_LOG_LINES = 6;\n \n function addConnectionLog(msg) {\n const now = new Date();\n const ts = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;\n connectionLog.push({ ts, msg });\n if (connectionLog.length > MAX_LOG_LINES) connectionLog.shift();\n }\n \n function updateConnectionUI() {\n const dot = document.getElementById('status-dot');\n let indicator = document.getElementById('connection-indicator');\n \n if (!indicator) {\n indicator = document.createElement('div');\n indicator.id = 'connection-indicator';\n indicator.style.cssText = `\n position: fixed; top: 50px; left: 12px;\n max-width: 280px;\n padding: 8px 10px;\n background: ${scheme.ui.shadow};\n border-radius: 6px;\n border: 1px solid ${scheme.foregroundMuted}30;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n z-index: 500; pointer-events: none;\n transition: opacity 0.5s ease;\n font-family: monospace;\n font-size: 10px;\n line-height: 1.5;\n `;\n document.body.appendChild(indicator);\n }\n \n if (connectionState === 'connected') {\n dot?.classList.add('online');\n addConnectionLog('connected ✓');\n // Show briefly then fade out\n indicator.style.opacity = '1';\n renderConnectionIndicator(indicator);\n setTimeout(() => { indicator.style.opacity = '0'; }, 2000);\n setTimeout(() => { if (connectionState === 'connected') indicator.style.display = 'none'; }, 2500);\n } else {\n dot?.classList.remove('online');\n indicator.style.display = 'block';\n indicator.style.opacity = '1';\n \n if (connectionState === 'connecting') {\n addConnectionLog(`connecting... (attempt ${reconnectAttempts})`);\n } else {\n addConnectionLog(`waiting to reconnect (attempt ${reconnectAttempts})`);\n }\n \n renderConnectionIndicator(indicator);\n }\n }\n \n function renderConnectionIndicator(indicator) {\n const stateColor = connectionState === 'connected' ? (scheme.statusOnline || '#0f0')\n : connectionState === 'connecting' ? (scheme.accent || '#ff69b4')\n : (scheme.foregroundMuted || '#555');\n const stateIcon = connectionState === 'connected' ? '●'\n : connectionState === 'connecting' ? '◌'\n : '○';\n \n const logHtml = connectionLog.map(l => \n `<div style=\"color: ${scheme.foregroundMuted}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\"><span style=\"color: ${scheme.foregroundMuted}80;\">${l.ts}</span> ${l.msg}</div>`\n ).join('');\n \n indicator.innerHTML = `\n <div style=\"display: flex; align-items: center; gap: 6px; margin-bottom: 4px;\">\n <span style=\"color: ${stateColor}; font-size: 8px;\">${stateIcon}</span>\n <span style=\"color: ${scheme.foreground || '#fff'}; font-size: 10px; font-weight: bold;\">process server</span>\n </div>\n ${logHtml}\n ${reconnectAttempts > 5 ? `<button onclick=\"location.reload()\" style=\"margin-top: 6px; padding: 3px 8px; background: ${scheme.accent}; border: none; border-radius: 3px; color: ${scheme.foregroundBright}; cursor: pointer; pointer-events: auto; font-size: 9px; font-family: monospace;\">↻ refresh</button>` : ''}\n `;\n }\n \n function connectWS() {\n connectionState = 'connecting';\n reconnectAttempts++;\n updateConnectionUI();\n \n try {\n ws = new WebSocket('ws://127.0.0.1:7890/ws');\n \n ws.onopen = () => {\n connectionState = 'connected';\n reconnectAttempts = 0;\n lastConnectTime = Date.now();\n updateConnectionUI();\n console.log('🟢 Connected to process server');\n };\n \n ws.onclose = () => {\n connectionState = 'disconnected';\n updateConnectionUI();\n // Exponential backoff: 1s, 2s, 4s, 8s, max 10s\n const delay = Math.min(1000 * Math.pow(2, Math.min(reconnectAttempts - 1, 3)), 10000);\n console.log(`🔴 Disconnected, reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);\n setTimeout(connectWS, delay);\n };\n \n ws.onerror = (err) => {\n console.log('🔴 WebSocket error:', err);\n ws.close();\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('cpus').textContent = data.system.cpus;\n const m = data.system.memory;\n document.getElementById('mem-text').textContent = m.used + ' / ' + m.total;\n \n // Update stats graph with system data\n updateStatsGraph(data.system);\n }\n updateViz(data.processes);\n } catch {}\n };\n } catch (err) {\n console.log('🔴 WebSocket creation error:', err);\n connectionState = 'disconnected';\n updateConnectionUI();\n setTimeout(connectWS, 2000);\n }\n }\n \n // 📊 Stats Graph (CPU/Memory history)\n const GRAPH_POINTS = 60; // 60 data points\n const cpuHistory = new Array(GRAPH_POINTS).fill(0);\n const memHistory = new Array(GRAPH_POINTS).fill(0);\n let statsCanvas = null;\n let statsCtx = null;\n \n function initStatsGraph() {\n statsCanvas = document.getElementById('stats-graph-canvas');\n if (statsCanvas) {\n statsCtx = statsCanvas.getContext('2d');\n // Set actual pixel dimensions for crisp rendering\n const rect = statsCanvas.getBoundingClientRect();\n statsCanvas.width = rect.width * window.devicePixelRatio;\n statsCanvas.height = rect.height * window.devicePixelRatio;\n statsCtx.scale(window.devicePixelRatio, window.devicePixelRatio);\n }\n }\n \n function updateStatsGraph(system) {\n if (!statsCtx) initStatsGraph();\n if (!statsCtx) return;\n \n // Parse memory usage\n const m = system.memory;\n let memPct = 0;\n if (m && m.used && m.total) {\n const usedNum = parseFloat(m.used.replace(/[^\\d.]/g, ''));\n const totalNum = parseFloat(m.total.replace(/[^\\d.]/g, ''));\n if (totalNum > 0) {\n memPct = (usedNum / totalNum) * 100;\n }\n }\n \n // Calculate total CPU usage from all processes\n let totalCpu = 0;\n meshes.forEach((mesh, pid) => {\n if (pid !== 'kernel' && mesh.userData.cpu) {\n totalCpu += mesh.userData.cpu;\n }\n });\n // Normalize to percentage (divide by number of CPUs)\n const numCpus = parseInt(system.cpus) || 1;\n const cpuPct = Math.min(100, totalCpu / numCpus);\n \n // Shift history and add new values\n cpuHistory.shift();\n cpuHistory.push(cpuPct);\n memHistory.shift();\n memHistory.push(memPct);\n \n // Update text labels\n const cpuEl = document.getElementById('cpu-pct');\n const memEl = document.getElementById('mem-pct');\n if (cpuEl) cpuEl.textContent = cpuPct.toFixed(1);\n if (memEl) memEl.textContent = memPct.toFixed(1);\n \n // Draw graph\n drawStatsGraph();\n }\n \n function drawStatsGraph() {\n if (!statsCtx || !statsCanvas) return;\n \n const rect = statsCanvas.getBoundingClientRect();\n const w = rect.width;\n const h = rect.height;\n \n // Clear canvas\n statsCtx.clearRect(0, 0, w, h);\n \n // Colors based on theme\n const cpuColor = currentTheme === 'light' ? '#006400' : '#50fa7b';\n const memColor = currentTheme === 'light' ? '#c71585' : '#ff79c6';\n const gridColor = currentTheme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)';\n \n // Draw grid lines\n statsCtx.strokeStyle = gridColor;\n statsCtx.lineWidth = 0.5;\n for (let i = 0; i <= 4; i++) {\n const y = (h / 4) * i;\n statsCtx.beginPath();\n statsCtx.moveTo(0, y);\n statsCtx.lineTo(w, y);\n statsCtx.stroke();\n }\n \n // Draw CPU line\n statsCtx.strokeStyle = cpuColor;\n statsCtx.lineWidth = 1.5;\n statsCtx.beginPath();\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (cpuHistory[i] / 100) * h;\n if (i === 0) statsCtx.moveTo(x, y);\n else statsCtx.lineTo(x, y);\n }\n statsCtx.stroke();\n \n // Fill CPU area (semi-transparent)\n statsCtx.fillStyle = cpuColor.replace(')', ',0.15)').replace('rgb', 'rgba').replace('#', '');\n if (cpuColor.startsWith('#')) {\n const r = parseInt(cpuColor.slice(1, 3), 16);\n const g = parseInt(cpuColor.slice(3, 5), 16);\n const b = parseInt(cpuColor.slice(5, 7), 16);\n statsCtx.fillStyle = `rgba(${r},${g},${b},0.15)`;\n }\n statsCtx.beginPath();\n statsCtx.moveTo(0, h);\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (cpuHistory[i] / 100) * h;\n statsCtx.lineTo(x, y);\n }\n statsCtx.lineTo(w, h);\n statsCtx.closePath();\n statsCtx.fill();\n \n // Draw Memory line\n statsCtx.strokeStyle = memColor;\n statsCtx.lineWidth = 1.5;\n statsCtx.beginPath();\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (memHistory[i] / 100) * h;\n if (i === 0) statsCtx.moveTo(x, y);\n else statsCtx.lineTo(x, y);\n }\n statsCtx.stroke();\n \n // Fill Memory area (semi-transparent)\n if (memColor.startsWith('#')) {\n const r = parseInt(memColor.slice(1, 3), 16);\n const g = parseInt(memColor.slice(3, 5), 16);\n const b = parseInt(memColor.slice(5, 7), 16);\n statsCtx.fillStyle = `rgba(${r},${g},${b},0.15)`;\n }\n statsCtx.beginPath();\n statsCtx.moveTo(0, h);\n for (let i = 0; i < GRAPH_POINTS; i++) {\n const x = (w / (GRAPH_POINTS - 1)) * i;\n const y = h - (memHistory[i] / 100) * h;\n statsCtx.lineTo(x, y);\n }\n statsCtx.lineTo(w, h);\n statsCtx.closePath();\n statsCtx.fill();\n }\n \n window.addEventListener('resize', () => {\n width = window.innerWidth;\n height = window.innerHeight;\n camera.aspect = width / height;\n camera.updateProjectionMatrix();\n renderer.setSize(width, height);\n \n // Reinitialize stats graph canvas on resize\n statsCanvas = null;\n statsCtx = null;\n initStatsGraph();\n });\n \n // 🎨 Theme switching function\n function setTheme(themeName) {\n if (themeName !== 'light' && themeName !== 'dark') return;\n currentTheme = themeName;\n scheme = colorSchemes[currentTheme];\n colors = scheme.categories;\n \n // Update scene background\n scene.background.setHex(scheme.three.sceneBackground);\n renderer.setClearColor(scheme.three.sceneBackground);\n \n // Update body styling\n document.body.dataset.theme = themeName;\n document.body.style.background = scheme.background;\n document.body.style.color = scheme.foreground;\n \n // Update kernel mesh colors\n if (kernelMesh) {\n kernelMesh.children[0].material.color.setHex(scheme.three.kernelOuter);\n if (kernelGlow) kernelGlow.material.color.setHex(scheme.three.kernelRing);\n if (kernelCore) kernelCore.material.color.setHex(scheme.three.kernelCore);\n }\n \n // Update all process node colors\n meshes.forEach((mesh, pid) => {\n if (pid === 'kernel') return;\n const category = mesh.userData.category;\n const newColor = colors[category] || 0x666666;\n mesh.material.color.setHex(newColor);\n mesh.userData.baseColor = newColor;\n });\n \n // Update connections\n connections.forEach(conn => {\n conn.line.material.color.setHex(scheme.three.connectionLine);\n });\n \n // Update graveyard\n graveyard.forEach(grave => {\n if (grave.mesh && grave.mesh.material) {\n grave.mesh.material.color.setHex(scheme.three.deadProcess);\n }\n });\n \n // Update CSS styles\n updateThemeStyles();\n }\n \n function toggleTheme() {\n setTheme(currentTheme === 'dark' ? 'light' : 'dark');\n return currentTheme;\n }\n \n function updateThemeStyles() {\n // Update dynamic CSS based on theme\n let styleEl = document.getElementById('theme-dynamic-styles');\n if (!styleEl) {\n styleEl = document.createElement('style');\n styleEl.id = 'theme-dynamic-styles';\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = `\n /* Header styles */\n .title .dot { color: ${scheme.accentBright}; }\n .status-dot { background: ${scheme.accent}; }\n .status-dot.online { background: ${scheme.statusOnline}; }\n .header-center { color: ${scheme.foregroundMuted}; }\n .header-center .val { color: ${scheme.foregroundBright}; }\n .header-btn { \n background: ${currentTheme === 'light' ? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.08)'};\n border-color: ${currentTheme === 'light' ? 'rgba(0,0,0,0.15)' : 'rgba(255,255,255,0.15)'};\n color: ${scheme.foregroundBright};\n }\n .header-btn:hover { border-color: ${scheme.accentBright}; }\n \n /* Center panel styles */\n .center-panel {\n background: ${currentTheme === 'light' ? 'rgba(252,247,197,0.8)' : 'rgba(24,19,24,0.7)'};\n border-color: ${currentTheme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.08)'};\n }\n .process-counter .count { color: ${scheme.foregroundBright}; }\n .process-counter .label { color: ${scheme.foregroundMuted}; }\n \n /* Status bar styles */\n .status-bar { color: ${scheme.foregroundMuted}; }\n .dev-badge { \n background: ${scheme.accentBright}; \n color: ${currentTheme === 'light' ? '#fff' : '#000'}; \n }\n \n /* Label styles - no background, stronger text shadow */\n .proc-label { \n text-shadow: 0 0 6px ${scheme.background}, 0 0 10px ${scheme.background}, 0 0 14px ${scheme.background}; \n background: transparent;\n }\n .proc-label .info { color: ${scheme.foregroundMuted}; }\n \n /* Tour UI styles */\n #tour-ui { background: ${scheme.ui.overlay}; border-color: ${scheme.foregroundMuted}; }\n `;\n }\n \n // Apply initial theme styles\n updateThemeStyles();\n \n // Expose for external use (mock data injection, etc.)\n window.ProcessTreeViz = {\n updateViz,\n scene,\n camera,\n renderer,\n controls,\n meshes,\n connections,\n graveyard,\n // Tour mode\n startTour,\n exitTour,\n tourNext,\n tourPrev,\n toggleAutoPlay,\n isTourMode: () => tourMode,\n // Theme control\n setTheme,\n toggleTheme,\n getTheme: () => currentTheme,\n getScheme: () => scheme,\n colorSchemes\n };\n \n // Add tour button to #header-right (new structure) or .header-right (old structure)\n const headerRight = document.getElementById('header-right') || document.querySelector('.header-right');\n if (headerRight) {\n const tourBtn = document.createElement('button');\n tourBtn.id = 'tour-btn';\n tourBtn.className = 'hdr-btn';\n tourBtn.textContent = '🎬';\n tourBtn.title = 'Tour Mode';\n tourBtn.onclick = () => { \n const isSourceTour = window.ASTTreeViz?.getTab() === 'sources' && window.ASTTreeViz?.isTourMode();\n if (!tourMode && !isSourceTour) startTour(); \n else exitTour(); \n };\n headerRight.insertBefore(tourBtn, headerRight.firstChild);\n }\n \n animate();\n connectWS();\n})();"; 5 5 6 6 export 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";
+2 -2
vscode-extension/package-lock.json
··· 1 1 { 2 2 "name": "aesthetic-computer-code", 3 - "version": "1.258.0", 3 + "version": "1.259.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "aesthetic-computer-code", 9 - "version": "1.258.0", 9 + "version": "1.259.0", 10 10 "license": "None", 11 11 "dependencies": { 12 12 "acorn": "^8.15.0",
+1 -1
vscode-extension/package.json
··· 4 4 "displayName": "Aesthetic Computer", 5 5 "icon": "resources/icon.png", 6 6 "author": "Jeffrey Alan Scudder", 7 - "version": "1.258.0", 7 + "version": "1.259.0", 8 8 "description": "Code, run, and publish your pieces. Includes Aesthetic Computer themes and KidLisp syntax highlighting.", 9 9 "engines": { 10 10 "vscode": "^1.105.0"
+65 -25
vscode-extension/views/process-tree.js
··· 1258 1258 let reconnectAttempts = 0; 1259 1259 let lastConnectTime = 0; 1260 1260 1261 + // Connection log messages for the corner indicator 1262 + const connectionLog = []; 1263 + const MAX_LOG_LINES = 6; 1264 + 1265 + function addConnectionLog(msg) { 1266 + const now = new Date(); 1267 + const ts = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`; 1268 + connectionLog.push({ ts, msg }); 1269 + if (connectionLog.length > MAX_LOG_LINES) connectionLog.shift(); 1270 + } 1271 + 1261 1272 function updateConnectionUI() { 1262 1273 const dot = document.getElementById('status-dot'); 1263 - let overlay = document.getElementById('connection-overlay'); 1274 + let indicator = document.getElementById('connection-indicator'); 1264 1275 1265 - if (!overlay) { 1266 - overlay = document.createElement('div'); 1267 - overlay.id = 'connection-overlay'; 1268 - overlay.style.cssText = ` 1269 - position: fixed; top: 0; left: 0; right: 0; bottom: 0; 1270 - display: flex; flex-direction: column; align-items: center; justify-content: center; 1271 - background: ${scheme.ui.overlay}; z-index: 500; pointer-events: none; 1276 + if (!indicator) { 1277 + indicator = document.createElement('div'); 1278 + indicator.id = 'connection-indicator'; 1279 + indicator.style.cssText = ` 1280 + position: fixed; top: 50px; left: 12px; 1281 + max-width: 280px; 1282 + padding: 8px 10px; 1283 + background: ${scheme.ui.shadow}; 1284 + border-radius: 6px; 1285 + border: 1px solid ${scheme.foregroundMuted}30; 1286 + backdrop-filter: blur(6px); 1287 + -webkit-backdrop-filter: blur(6px); 1288 + z-index: 500; pointer-events: none; 1272 1289 transition: opacity 0.5s ease; 1290 + font-family: monospace; 1291 + font-size: 10px; 1292 + line-height: 1.5; 1273 1293 `; 1274 - document.body.appendChild(overlay); 1294 + document.body.appendChild(indicator); 1275 1295 } 1276 1296 1277 1297 if (connectionState === 'connected') { 1278 1298 dot?.classList.add('online'); 1279 - overlay.style.opacity = '0'; 1280 - setTimeout(() => { if (connectionState === 'connected') overlay.style.display = 'none'; }, 500); 1299 + addConnectionLog('connected ✓'); 1300 + // Show briefly then fade out 1301 + indicator.style.opacity = '1'; 1302 + renderConnectionIndicator(indicator); 1303 + setTimeout(() => { indicator.style.opacity = '0'; }, 2000); 1304 + setTimeout(() => { if (connectionState === 'connected') indicator.style.display = 'none'; }, 2500); 1281 1305 } else { 1282 1306 dot?.classList.remove('online'); 1283 - overlay.style.display = 'flex'; 1284 - overlay.style.opacity = '1'; 1307 + indicator.style.display = 'block'; 1308 + indicator.style.opacity = '1'; 1285 1309 1286 - const statusText = connectionState === 'connecting' 1287 - ? `Connecting to process server...` 1288 - : `Waiting for process server (attempt ${reconnectAttempts})`; 1289 - const subText = reconnectAttempts > 3 1290 - ? 'Server may still be starting up...' 1291 - : 'ws://127.0.0.1:7890'; 1310 + if (connectionState === 'connecting') { 1311 + addConnectionLog(`connecting... (attempt ${reconnectAttempts})`); 1312 + } else { 1313 + addConnectionLog(`waiting to reconnect (attempt ${reconnectAttempts})`); 1314 + } 1292 1315 1293 - overlay.innerHTML = ` 1294 - <div style="font-size: 48px; margin-bottom: 16px;">${connectionState === 'connecting' ? '🔄' : '⏳'}</div> 1295 - <div style="font-size: 16px; color: ${scheme.foregroundBright}; margin-bottom: 8px;">${statusText}</div> 1296 - <div style="font-size: 12px; color: ${scheme.foregroundMuted};">${subText}</div> 1297 - ${reconnectAttempts > 5 ? `<button onclick="location.reload()" style="margin-top: 16px; padding: 8px 16px; background: ${scheme.accent}; border: none; border-radius: 4px; color: ${scheme.foregroundBright}; cursor: pointer; pointer-events: auto;">🔄 Refresh Page</button>` : ''} 1298 - `; 1316 + renderConnectionIndicator(indicator); 1299 1317 } 1318 + } 1319 + 1320 + function renderConnectionIndicator(indicator) { 1321 + const stateColor = connectionState === 'connected' ? (scheme.statusOnline || '#0f0') 1322 + : connectionState === 'connecting' ? (scheme.accent || '#ff69b4') 1323 + : (scheme.foregroundMuted || '#555'); 1324 + const stateIcon = connectionState === 'connected' ? '●' 1325 + : connectionState === 'connecting' ? '◌' 1326 + : '○'; 1327 + 1328 + const logHtml = connectionLog.map(l => 1329 + `<div style="color: ${scheme.foregroundMuted}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><span style="color: ${scheme.foregroundMuted}80;">${l.ts}</span> ${l.msg}</div>` 1330 + ).join(''); 1331 + 1332 + indicator.innerHTML = ` 1333 + <div style="display: flex; align-items: center; gap: 6px; margin-bottom: 4px;"> 1334 + <span style="color: ${stateColor}; font-size: 8px;">${stateIcon}</span> 1335 + <span style="color: ${scheme.foreground || '#fff'}; font-size: 10px; font-weight: bold;">process server</span> 1336 + </div> 1337 + ${logHtml} 1338 + ${reconnectAttempts > 5 ? `<button onclick="location.reload()" style="margin-top: 6px; padding: 3px 8px; background: ${scheme.accent}; border: none; border-radius: 3px; color: ${scheme.foregroundBright}; cursor: pointer; pointer-events: auto; font-size: 9px; font-family: monospace;">↻ refresh</button>` : ''} 1339 + `; 1300 1340 } 1301 1341 1302 1342 function connectWS() {