Monorepo for Aesthetic.Computer aesthetic.computer
at main 1705 lines 57 kB view raw
1// 3D Process Tree Visualization 2// Shared between VS Code extension and local dev testing 3 4(function() { 5 'use strict'; 6 7 // Check if we're in VS Code webview or standalone 8 const isVSCode = typeof acquireVsCodeApi === 'function'; 9 10 // 🎨 Color Schemes (imported from color-schemes.js or embedded) 11 const colorSchemes = window.AestheticColorSchemes?.schemes || { 12 "dark": { 13 "background": "#181318", 14 "backgroundAlt": "#141214", 15 "foreground": "#ffffffcc", 16 "foregroundBright": "#ffffff", 17 "foregroundMuted": "#555555", 18 "accent": "#a87090", 19 "accentBright": "#ff69b4", 20 "statusOnline": "#0f0", 21 "categories": { 22 "editor": 11561983, 23 "tui": 16738740, 24 "bridge": 7077791, 25 "db": 16771947, 26 "proxy": 7053311, 27 "ai": 16752491, 28 "shell": 7077887, 29 "dev": 7077791, 30 "ide": 7053311, 31 "lsp": 8947848, 32 "kernel": 8965375 33 }, 34 "three": { 35 "sceneBackground": 1577752, 36 "kernelOuter": 4491519, 37 "kernelRing": 6728447, 38 "kernelCore": 8965375, 39 "connectionLine": 4473924, 40 "connectionActive": 16738740, 41 "deadProcess": 4473924 42 }, 43 "ui": { 44 "shadow": "rgba(0, 0, 0, 0.6)", 45 "overlay": "rgba(0, 0, 0, 0.85)" 46 } 47 }, 48 "light": { 49 "background": "#fcf7c5", 50 "backgroundAlt": "#f5f0c0", 51 "foreground": "#281e5a", 52 "foregroundBright": "#281e5a", 53 "foregroundMuted": "#806060", 54 "accent": "#387adf", 55 "accentBright": "#006400", 56 "statusOnline": "#006400", 57 "categories": { 58 "editor": 8405200, 59 "tui": 13648000, 60 "bridge": 2129984, 61 "db": 10518528, 62 "proxy": 2121920, 63 "ai": 12607520, 64 "shell": 32896, 65 "dev": 2129984, 66 "ide": 2121920, 67 "lsp": 6316128, 68 "kernel": 3701471 69 }, 70 "three": { 71 "sceneBackground": 16578501, 72 "kernelOuter": 3701471, 73 "kernelRing": 25600, 74 "kernelCore": 3701471, 75 "connectionLine": 11051136, 76 "connectionActive": 25600, 77 "deadProcess": 11051136 78 }, 79 "ui": { 80 "shadow": "rgba(0, 0, 0, 0.2)", 81 "overlay": "rgba(252, 247, 197, 0.95)" 82 } 83 }, 84 "red": { 85 "background": "#181010", 86 "backgroundAlt": "#140c0c", 87 "foreground": "#ffffffcc", 88 "foregroundBright": "#ffffff", 89 "foregroundMuted": "#555555", 90 "accent": "#ff5555", 91 "accentBright": "#ff8888", 92 "statusOnline": "#0f0", 93 "categories": { 94 "editor": 11561983, 95 "tui": 16738740, 96 "bridge": 7077791, 97 "db": 16771947, 98 "proxy": 7053311, 99 "ai": 16752491, 100 "shell": 7077887, 101 "dev": 7077791, 102 "ide": 7053311, 103 "lsp": 8947848, 104 "kernel": 8965375 105 }, 106 "three": { 107 "sceneBackground": 1576976, 108 "kernelOuter": 4491519, 109 "kernelRing": 6728447, 110 "kernelCore": 8965375, 111 "connectionLine": 4473924, 112 "connectionActive": 16746632, 113 "deadProcess": 4473924 114 }, 115 "ui": { 116 "shadow": "rgba(0, 0, 0, 0.6)", 117 "overlay": "rgba(0, 0, 0, 0.85)" 118 } 119 }, 120 "orange": { 121 "background": "#181410", 122 "backgroundAlt": "#14100c", 123 "foreground": "#ffffffcc", 124 "foregroundBright": "#ffffff", 125 "foregroundMuted": "#555555", 126 "accent": "#ffb86c", 127 "accentBright": "#ffd8a8", 128 "statusOnline": "#0f0", 129 "categories": { 130 "editor": 11561983, 131 "tui": 16738740, 132 "bridge": 7077791, 133 "db": 16771947, 134 "proxy": 7053311, 135 "ai": 16752491, 136 "shell": 7077887, 137 "dev": 7077791, 138 "ide": 7053311, 139 "lsp": 8947848, 140 "kernel": 8965375 141 }, 142 "three": { 143 "sceneBackground": 1578000, 144 "kernelOuter": 4491519, 145 "kernelRing": 6728447, 146 "kernelCore": 8965375, 147 "connectionLine": 4473924, 148 "connectionActive": 16767144, 149 "deadProcess": 4473924 150 }, 151 "ui": { 152 "shadow": "rgba(0, 0, 0, 0.6)", 153 "overlay": "rgba(0, 0, 0, 0.85)" 154 } 155 }, 156 "yellow": { 157 "background": "#181810", 158 "backgroundAlt": "#14140c", 159 "foreground": "#ffffffcc", 160 "foregroundBright": "#ffffff", 161 "foregroundMuted": "#555555", 162 "accent": "#f1fa8c", 163 "accentBright": "#ffffa0", 164 "statusOnline": "#0f0", 165 "categories": { 166 "editor": 11561983, 167 "tui": 16738740, 168 "bridge": 7077791, 169 "db": 16771947, 170 "proxy": 7053311, 171 "ai": 16752491, 172 "shell": 7077887, 173 "dev": 7077791, 174 "ide": 7053311, 175 "lsp": 8947848, 176 "kernel": 8965375 177 }, 178 "three": { 179 "sceneBackground": 1579024, 180 "kernelOuter": 4491519, 181 "kernelRing": 6728447, 182 "kernelCore": 8965375, 183 "connectionLine": 4473924, 184 "connectionActive": 16777120, 185 "deadProcess": 4473924 186 }, 187 "ui": { 188 "shadow": "rgba(0, 0, 0, 0.6)", 189 "overlay": "rgba(0, 0, 0, 0.85)" 190 } 191 }, 192 "green": { 193 "background": "#101810", 194 "backgroundAlt": "#0c140c", 195 "foreground": "#ffffffcc", 196 "foregroundBright": "#ffffff", 197 "foregroundMuted": "#555555", 198 "accent": "#50fa7b", 199 "accentBright": "#80ffae", 200 "statusOnline": "#0f0", 201 "categories": { 202 "editor": 11561983, 203 "tui": 16738740, 204 "bridge": 7077791, 205 "db": 16771947, 206 "proxy": 7053311, 207 "ai": 16752491, 208 "shell": 7077887, 209 "dev": 7077791, 210 "ide": 7053311, 211 "lsp": 8947848, 212 "kernel": 8965375 213 }, 214 "three": { 215 "sceneBackground": 1054736, 216 "kernelOuter": 4491519, 217 "kernelRing": 6728447, 218 "kernelCore": 8965375, 219 "connectionLine": 4473924, 220 "connectionActive": 8454062, 221 "deadProcess": 4473924 222 }, 223 "ui": { 224 "shadow": "rgba(0, 0, 0, 0.6)", 225 "overlay": "rgba(0, 0, 0, 0.85)" 226 } 227 }, 228 "blue": { 229 "background": "#101418", 230 "backgroundAlt": "#0c1014", 231 "foreground": "#ffffffcc", 232 "foregroundBright": "#ffffff", 233 "foregroundMuted": "#555555", 234 "accent": "#61afef", 235 "accentBright": "#8cd0ff", 236 "statusOnline": "#0f0", 237 "categories": { 238 "editor": 11561983, 239 "tui": 16738740, 240 "bridge": 7077791, 241 "db": 16771947, 242 "proxy": 7053311, 243 "ai": 16752491, 244 "shell": 7077887, 245 "dev": 7077791, 246 "ide": 7053311, 247 "lsp": 8947848, 248 "kernel": 8965375 249 }, 250 "three": { 251 "sceneBackground": 1053720, 252 "kernelOuter": 4491519, 253 "kernelRing": 6728447, 254 "kernelCore": 8965375, 255 "connectionLine": 4473924, 256 "connectionActive": 9228543, 257 "deadProcess": 4473924 258 }, 259 "ui": { 260 "shadow": "rgba(0, 0, 0, 0.6)", 261 "overlay": "rgba(0, 0, 0, 0.85)" 262 } 263 }, 264 "indigo": { 265 "background": "#121018", 266 "backgroundAlt": "#0e0c14", 267 "foreground": "#ffffffcc", 268 "foregroundBright": "#ffffff", 269 "foregroundMuted": "#555555", 270 "accent": "#6272a4", 271 "accentBright": "#8be9fd", 272 "statusOnline": "#0f0", 273 "categories": { 274 "editor": 11561983, 275 "tui": 16738740, 276 "bridge": 7077791, 277 "db": 16771947, 278 "proxy": 7053311, 279 "ai": 16752491, 280 "shell": 7077887, 281 "dev": 7077791, 282 "ide": 7053311, 283 "lsp": 8947848, 284 "kernel": 8965375 285 }, 286 "three": { 287 "sceneBackground": 1183768, 288 "kernelOuter": 4491519, 289 "kernelRing": 6728447, 290 "kernelCore": 8965375, 291 "connectionLine": 4473924, 292 "connectionActive": 9169405, 293 "deadProcess": 4473924 294 }, 295 "ui": { 296 "shadow": "rgba(0, 0, 0, 0.6)", 297 "overlay": "rgba(0, 0, 0, 0.85)" 298 } 299 }, 300 "violet": { 301 "background": "#161016", 302 "backgroundAlt": "#120c12", 303 "foreground": "#ffffffcc", 304 "foregroundBright": "#ffffff", 305 "foregroundMuted": "#555555", 306 "accent": "#bd93f9", 307 "accentBright": "#ff79c6", 308 "statusOnline": "#0f0", 309 "categories": { 310 "editor": 11561983, 311 "tui": 16738740, 312 "bridge": 7077791, 313 "db": 16771947, 314 "proxy": 7053311, 315 "ai": 16752491, 316 "shell": 7077887, 317 "dev": 7077791, 318 "ide": 7053311, 319 "lsp": 8947848, 320 "kernel": 8965375 321 }, 322 "three": { 323 "sceneBackground": 1445910, 324 "kernelOuter": 4491519, 325 "kernelRing": 6728447, 326 "kernelCore": 8965375, 327 "connectionLine": 4473924, 328 "connectionActive": 16742854, 329 "deadProcess": 4473924 330 }, 331 "ui": { 332 "shadow": "rgba(0, 0, 0, 0.6)", 333 "overlay": "rgba(0, 0, 0, 0.85)" 334 } 335 }, 336 "pink": { 337 "background": "#181014", 338 "backgroundAlt": "#140c10", 339 "foreground": "#ffffffcc", 340 "foregroundBright": "#ffffff", 341 "foregroundMuted": "#555555", 342 "accent": "#ff79c6", 343 "accentBright": "#ff9ce6", 344 "statusOnline": "#0f0", 345 "categories": { 346 "editor": 11561983, 347 "tui": 16738740, 348 "bridge": 7077791, 349 "db": 16771947, 350 "proxy": 7053311, 351 "ai": 16752491, 352 "shell": 7077887, 353 "dev": 7077791, 354 "ide": 7053311, 355 "lsp": 8947848, 356 "kernel": 8965375 357 }, 358 "three": { 359 "sceneBackground": 1576980, 360 "kernelOuter": 4491519, 361 "kernelRing": 6728447, 362 "kernelCore": 8965375, 363 "connectionLine": 4473924, 364 "connectionActive": 16751846, 365 "deadProcess": 4473924 366 }, 367 "ui": { 368 "shadow": "rgba(0, 0, 0, 0.6)", 369 "overlay": "rgba(0, 0, 0, 0.85)" 370 } 371 }, 372 "pencil": { 373 "background": "#181818", 374 "backgroundAlt": "#141414", 375 "foreground": "#ffffffcc", 376 "foregroundBright": "#ffffff", 377 "foregroundMuted": "#555555", 378 "accent": "#e0e0e0", 379 "accentBright": "#ffffff", 380 "statusOnline": "#0f0", 381 "categories": { 382 "editor": 11561983, 383 "tui": 16738740, 384 "bridge": 7077791, 385 "db": 16771947, 386 "proxy": 7053311, 387 "ai": 16752491, 388 "shell": 7077887, 389 "dev": 7077791, 390 "ide": 7053311, 391 "lsp": 8947848, 392 "kernel": 8965375 393 }, 394 "three": { 395 "sceneBackground": 1579032, 396 "kernelOuter": 4491519, 397 "kernelRing": 6728447, 398 "kernelCore": 8965375, 399 "connectionLine": 4473924, 400 "connectionActive": 16777215, 401 "deadProcess": 4473924 402 }, 403 "ui": { 404 "shadow": "rgba(0, 0, 0, 0.6)", 405 "overlay": "rgba(0, 0, 0, 0.85)" 406 } 407 } 408}; 409 410 // Detect theme from data attribute, URL param, VS Code CSS vars, or OS preference 411 function detectTheme() { 412 // Check data attribute first (set by the HTML) 413 const dataTheme = document.body.dataset.theme; 414 if (colorSchemes[dataTheme]) return dataTheme; 415 416 // Check URL param 417 const urlParams = new URLSearchParams(window.location.search); 418 const urlTheme = urlParams.get('theme'); 419 if (colorSchemes[urlTheme]) return urlTheme; 420 421 // Check VS Code CSS variables 422 if (typeof getComputedStyle !== 'undefined') { 423 const bgColor = getComputedStyle(document.body).getPropertyValue('--vscode-editor-background').trim(); 424 425 if (bgColor && bgColor.startsWith('#')) { 426 // Exact match against known backgrounds 427 const bgLower = bgColor.toLowerCase(); 428 for (const [key, scheme] of Object.entries(colorSchemes)) { 429 if (scheme.background.toLowerCase() === bgLower) { 430 return key; 431 } 432 } 433 434 // Check for Light vs Dark if no exact match 435 const r = parseInt(bgColor.slice(1, 3), 16); 436 const g = parseInt(bgColor.slice(3, 5), 16); 437 const b = parseInt(bgColor.slice(5, 7), 16); 438 const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 439 return luminance > 0.5 ? 'light' : 'dark'; 440 } 441 } 442 443 // Fall back to OS preference (prefers-color-scheme) 444 if (typeof window !== 'undefined' && window.matchMedia) { 445 if (window.matchMedia('(prefers-color-scheme: light)').matches) { 446 return 'light'; 447 } 448 } 449 450 return 'dark'; 451 } 452 453 let currentTheme = detectTheme(); 454 let scheme = colorSchemes[currentTheme]; 455 let colors = scheme.categories; 456 457 // Apply initial body styling based on detected theme 458 document.body.style.background = scheme.background; 459 document.body.style.color = scheme.foreground; 460 document.body.dataset.theme = currentTheme; 461 462 // Dev badge is now in the HTML for dev.html - no need to create dynamically 463 464 let width = window.innerWidth, height = window.innerHeight; 465 let meshes = new Map(), connections = new Map(), ws; 466 let graveyard = []; 467 const MAX_GRAVEYARD = 30; 468 const GRAVEYARD_Y = -200; 469 470 // Three.js setup 471 const scene = new THREE.Scene(); 472 scene.background = new THREE.Color(scheme.three.sceneBackground); 473 474 const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 5000); 475 camera.position.set(0, 150, 400); 476 camera.lookAt(0, 0, 0); 477 478 const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true }); 479 renderer.setSize(width, height); 480 renderer.setPixelRatio(window.devicePixelRatio); 481 renderer.setClearColor(scheme.three.sceneBackground); 482 483 const controls = new THREE.OrbitControls(camera, renderer.domElement); 484 controls.enableDamping = true; 485 controls.dampingFactor = 0.05; 486 controls.minDistance = 20; 487 controls.maxDistance = 3000; 488 controls.enablePan = true; 489 controls.autoRotate = true; 490 controls.autoRotateSpeed = 0.3; 491 controls.target.set(0, 0, 0); 492 493 let focusedPid = null; 494 let focusTarget = new THREE.Vector3(0, 0, 0); 495 let focusDistance = null; 496 let transitioning = false; 497 498 // Tour mode state 499 let tourMode = false; 500 let tourIndex = 0; 501 let tourProcessList = []; 502 let tourAutoPlay = false; 503 let tourAutoPlayInterval = null; 504 const TOUR_SPEED = 2500; // ms between auto-advances 505 506 const raycaster = new THREE.Raycaster(); 507 const mouse = new THREE.Vector2(); 508 509 renderer.domElement.addEventListener('click', (e) => { 510 mouse.x = (e.clientX / width) * 2 - 1; 511 mouse.y = -(e.clientY / height) * 2 + 1; 512 513 raycaster.setFromCamera(mouse, camera); 514 const meshArray = Array.from(meshes.values()); 515 const intersects = raycaster.intersectObjects(meshArray); 516 517 if (intersects.length > 0) { 518 const clicked = intersects[0].object; 519 const pid = clicked.userData.pid; 520 521 if (focusedPid === String(pid)) { 522 focusedPid = null; 523 focusTarget.set(0, 0, 0); 524 focusDistance = null; 525 } else { 526 focusedPid = String(pid); 527 focusTarget.copy(clicked.position); 528 focusDistance = 80 + (clicked.userData.size || 6) * 3; 529 } 530 transitioning = true; 531 controls.autoRotate = true; 532 } else if (!e.shiftKey) { 533 focusedPid = null; 534 focusTarget.set(0, 0, 0); 535 focusDistance = null; 536 transitioning = true; 537 } 538 }); 539 540 renderer.domElement.addEventListener('dblclick', () => { 541 focusedPid = null; 542 focusTarget.set(0, 0, 0); 543 focusDistance = null; 544 transitioning = true; 545 camera.position.set(0, 150, 400); 546 }); 547 548 // Tour Mode Functions 549 function updateTourUI() { 550 let tourUI = document.getElementById('tour-ui'); 551 if (!tourUI) { 552 tourUI = document.createElement('div'); 553 tourUI.id = 'tour-ui'; 554 document.body.appendChild(tourUI); 555 } 556 // Tour UI positioned above center panel, non-overlapping 557 tourUI.style.cssText = ` 558 position: fixed; 559 bottom: 180px; 560 left: 50%; 561 transform: translateX(-50%); 562 background: ${scheme.ui.overlay}; 563 padding: 16px 24px; 564 border-radius: 12px; 565 color: ${scheme.foregroundBright}; 566 font-family: monospace; 567 font-size: 12px; 568 z-index: 1000; 569 display: none; 570 text-align: center; 571 border: 1px solid ${scheme.foregroundMuted}40; 572 backdrop-filter: blur(8px); 573 -webkit-backdrop-filter: blur(8px); 574 min-width: 280px; 575 `; 576 577 if (tourMode && tourProcessList.length > 0) { 578 const current = tourProcessList[tourIndex]; 579 const mesh = meshes.get(current); 580 const name = mesh?.userData?.name || current; 581 const icon = mesh?.userData?.icon || '●'; 582 const category = mesh?.userData?.category || ''; 583 584 tourUI.style.display = 'block'; 585 tourUI.innerHTML = ` 586 <div style="margin-bottom:10px;font-size:12px;color:${scheme.accent};text-transform:uppercase;letter-spacing:1px;">🎬 Tour Mode</div> 587 <div style="font-size:24px;margin-bottom:4px;color:${scheme.foregroundBright};">${icon}</div> 588 <div style="font-size:14px;font-weight:bold;color:${scheme.foregroundBright};margin-bottom:2px;">${name}</div> 589 <div style="color:${scheme.foregroundMuted};margin-bottom:14px;font-size:11px;">${category}${tourIndex + 1}/${tourProcessList.length}</div> 590 <div style="display:flex;gap:8px;justify-content:center;"> 591 <button onclick="ProcessTreeViz.tourPrev()" class="header-btn" style="pointer-events:auto;padding:8px 14px;">← Prev</button> 592 <button onclick="ProcessTreeViz.toggleAutoPlay()" class="header-btn" style="pointer-events:auto;padding:8px 14px;">${tourAutoPlay ? '⏸ Stop' : '▶ Auto'}</button> 593 <button onclick="ProcessTreeViz.tourNext()" class="header-btn" style="pointer-events:auto;padding:8px 14px;">Next →</button> 594 <button onclick="ProcessTreeViz.exitTour()" class="header-btn" style="pointer-events:auto;padding:8px 14px;">✕</button> 595 </div> 596 ${tourAutoPlay ? `<div style="color:${scheme.accentBright};margin-top:10px;font-size:10px;">▶ Auto-playing...</div>` : ''} 597 `; 598 // Hide the tour button when in tour mode 599 const btn = document.getElementById('tour-btn'); 600 if (btn) btn.style.display = 'none'; 601 } else { 602 tourUI.style.display = 'none'; 603 // Show the tour button when not in tour mode 604 const btn = document.getElementById('tour-btn'); 605 if (btn) btn.style.display = ''; 606 } 607 } 608 609 function buildTourList() { 610 // Build ordered list: kernel first, then by category, then by tree depth 611 const categoryOrder = ['kernel', 'ide', 'editor', 'tui', 'dev', 'db', 'shell', 'ai', 'lsp', 'proxy', 'bridge']; 612 const list = Array.from(meshes.keys()); 613 614 list.sort((a, b) => { 615 const meshA = meshes.get(a); 616 const meshB = meshes.get(b); 617 const catA = meshA?.userData?.category || 'zzz'; 618 const catB = meshB?.userData?.category || 'zzz'; 619 const orderA = categoryOrder.indexOf(catA); 620 const orderB = categoryOrder.indexOf(catB); 621 return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB); 622 }); 623 624 return list; 625 } 626 627 function focusOnProcess(pid) { 628 const mesh = meshes.get(pid); 629 if (!mesh) return; 630 631 focusedPid = pid; 632 focusTarget.copy(mesh.position); 633 focusDistance = 80 + (mesh.userData.size || 6) * 3; 634 transitioning = true; 635 controls.autoRotate = true; 636 } 637 638 function startTour() { 639 if (window.ASTTreeViz?.getTab() === 'sources') { 640 window.ASTTreeViz.startTour(); 641 // Ensure local state reflects we are "busy" or just let AST handle it 642 return; 643 } 644 645 tourMode = true; 646 tourProcessList = buildTourList(); 647 tourIndex = 0; 648 if (tourProcessList.length > 0) { 649 focusOnProcess(tourProcessList[0]); 650 } 651 updateTourUI(); 652 } 653 654 function exitTour() { 655 if (window.ASTTreeViz?.getTab() === 'sources') { 656 window.ASTTreeViz.stopTour(); 657 return; 658 } 659 660 tourMode = false; 661 tourAutoPlay = false; 662 if (tourAutoPlayInterval) { 663 clearInterval(tourAutoPlayInterval); 664 tourAutoPlayInterval = null; 665 } 666 focusedPid = null; 667 focusTarget.set(0, 0, 0); 668 focusDistance = null; 669 transitioning = true; 670 updateTourUI(); 671 } 672 673 function tourNext() { 674 if (window.ASTTreeViz?.getTab() === 'sources') { 675 window.ASTTreeViz.tourNext(); 676 return; 677 } 678 679 if (!tourMode || tourProcessList.length === 0) return; 680 tourIndex = (tourIndex + 1) % tourProcessList.length; 681 focusOnProcess(tourProcessList[tourIndex]); 682 updateTourUI(); 683 } 684 685 function tourPrev() { 686 if (window.ASTTreeViz?.getTab() === 'sources') { 687 window.ASTTreeViz.tourPrev(); 688 return; 689 } 690 691 if (!tourMode || tourProcessList.length === 0) return; 692 tourIndex = (tourIndex - 1 + tourProcessList.length) % tourProcessList.length; 693 focusOnProcess(tourProcessList[tourIndex]); 694 updateTourUI(); 695 } 696 697 function toggleAutoPlay() { 698 if (window.ASTTreeViz?.getTab() === 'sources') { 699 window.ASTTreeViz.toggleTourAutoPlay(); 700 return; 701 } 702 703 tourAutoPlay = !tourAutoPlay; 704 if (tourAutoPlay) { 705 tourAutoPlayInterval = setInterval(tourNext, TOUR_SPEED); 706 } else { 707 if (tourAutoPlayInterval) { 708 clearInterval(tourAutoPlayInterval); 709 tourAutoPlayInterval = null; 710 } 711 } 712 updateTourUI(); 713 } 714 715 // Keyboard controls 716 document.addEventListener('keydown', (e) => { 717 const isSourceTour = window.ASTTreeViz?.getTab() === 'sources' && window.ASTTreeViz?.isTourMode(); 718 const isTourActive = tourMode || isSourceTour; 719 720 // T to start tour 721 if (e.key === 't' || e.key === 'T') { 722 if (!isTourActive) { 723 startTour(); 724 } else { 725 exitTour(); 726 } 727 return; 728 } 729 730 if (isTourActive) { 731 switch(e.key) { 732 case 'ArrowRight': 733 case 'l': 734 case 'L': 735 tourNext(); 736 e.preventDefault(); 737 break; 738 case 'ArrowLeft': 739 case 'h': 740 case 'H': 741 tourPrev(); 742 e.preventDefault(); 743 break; 744 case ' ': 745 toggleAutoPlay(); 746 e.preventDefault(); 747 break; 748 case 'Escape': 749 case 'q': 750 case 'Q': 751 exitTour(); 752 e.preventDefault(); 753 break; 754 } 755 } 756 }); 757 758 let processTree = { roots: [], byPid: new Map() }; 759 760 let kernelMesh = null, kernelGlow = null, kernelCore = null; 761 function createKernelNode() { 762 const group = new THREE.Group(); 763 764 const outerGeo = new THREE.SphereGeometry(35, 32, 32); 765 const outerMat = new THREE.MeshBasicMaterial({ 766 color: scheme.three.kernelOuter, transparent: true, opacity: 0.15, wireframe: true 767 }); 768 group.add(new THREE.Mesh(outerGeo, outerMat)); 769 770 const ringGeo = new THREE.TorusGeometry(25, 1.5, 8, 48); 771 const ringMat = new THREE.MeshBasicMaterial({ 772 color: scheme.three.kernelRing, transparent: true, opacity: 0.4 773 }); 774 const ring = new THREE.Mesh(ringGeo, ringMat); 775 ring.rotation.x = Math.PI / 2; 776 group.add(ring); 777 kernelGlow = ring; 778 779 const coreGeo = new THREE.SphereGeometry(12, 24, 24); 780 const coreMat = new THREE.MeshBasicMaterial({ 781 color: scheme.three.kernelCore, transparent: true, opacity: 0.7 782 }); 783 const core = new THREE.Mesh(coreGeo, coreMat); 784 group.add(core); 785 kernelCore = core; 786 787 group.userData = { 788 pid: 'kernel', name: 'Fedora Linux', icon: '🐧', category: 'kernel', 789 cpu: 0, rss: 0, size: 35, targetPos: new THREE.Vector3(0, 0, 0), pulsePhase: 0 790 }; 791 return group; 792 } 793 794 kernelMesh = createKernelNode(); 795 scene.add(kernelMesh); 796 meshes.set('kernel', kernelMesh); 797 798 function createNodeMesh(node) { 799 const cpu = node.cpu || 0; 800 const memMB = (node.rss || 10000) / 1024; 801 const baseColor = colors[node.category] || 0x666666; 802 const size = Math.max(4, Math.min(12, 3 + memMB * 0.05 + cpu * 0.1)); 803 804 const geo = new THREE.SphereGeometry(size, 12, 12); 805 const mat = new THREE.MeshBasicMaterial({ 806 color: baseColor, transparent: true, opacity: 0.7 + cpu * 0.003 807 }); 808 809 const mesh = new THREE.Mesh(geo, mat); 810 mesh.userData = { 811 ...node, size, baseColor, targetPos: new THREE.Vector3(), 812 pulsePhase: Math.random() * Math.PI * 2 813 }; 814 return mesh; 815 } 816 817 function createConnectionLine(color) { 818 const geo = new THREE.CylinderGeometry(1.5, 1.5, 1, 8); 819 const mat = new THREE.MeshBasicMaterial({ 820 color: color || scheme.three.connectionLine, transparent: true, opacity: 0.5 821 }); 822 return new THREE.Mesh(geo, mat); 823 } 824 825 function updateConnectionMesh(conn, childPos, parentPos) { 826 const mesh = conn.line; 827 const mid = new THREE.Vector3().addVectors(childPos, parentPos).multiplyScalar(0.5); 828 mesh.position.copy(mid); 829 const dir = new THREE.Vector3().subVectors(parentPos, childPos); 830 const length = dir.length(); 831 mesh.scale.set(1, length, 1); 832 mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize()); 833 } 834 835 function layoutTree(processes) { 836 const byPid = new Map(); 837 const children = new Map(); 838 839 processes.forEach(p => { 840 byPid.set(String(p.pid), p); 841 children.set(String(p.pid), []); 842 }); 843 844 const roots = []; 845 processes.forEach(p => { 846 const parentPid = String(p.parentInteresting || 0); 847 if (parentPid && byPid.has(parentPid)) { 848 children.get(parentPid).push(p); 849 } else { 850 roots.push(p); 851 } 852 }); 853 854 const categoryOrder = ['ide', 'editor', 'tui', 'dev', 'db', 'shell', 'ai', 'lsp', 'proxy', 'bridge']; 855 roots.sort((a, b) => { 856 const ai = categoryOrder.indexOf(a.category); 857 const bi = categoryOrder.indexOf(b.category); 858 return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi); 859 }); 860 861 const levelHeight = 50, baseRadius = 100; 862 863 function countDescendants(pid) { 864 const nodeChildren = children.get(pid) || []; 865 let count = nodeChildren.length; 866 nodeChildren.forEach(c => count += countDescendants(String(c.pid))); 867 return count; 868 } 869 870 function positionNode(node, depth, angle, radius, parentX, parentZ) { 871 const pid = String(node.pid); 872 const nodeChildren = children.get(pid) || []; 873 const childCount = nodeChildren.length; 874 875 const x = parentX + Math.cos(angle) * radius; 876 const z = parentZ + Math.sin(angle) * radius; 877 878 node.targetX = x; 879 node.targetY = -depth * levelHeight; 880 node.targetZ = z; 881 882 if (childCount > 0) { 883 const arcSpread = Math.min(Math.PI * 0.9, Math.PI * 0.3 * childCount); 884 const startAngle = angle - arcSpread / 2; 885 const childRadius = 35 + childCount * 10; 886 887 nodeChildren.forEach((child, i) => { 888 const childAngle = childCount === 1 ? angle : startAngle + (arcSpread / (childCount - 1)) * i; 889 positionNode(child, depth + 1, childAngle, childRadius, x, z); 890 }); 891 } 892 } 893 894 const totalRoots = roots.length; 895 if (totalRoots > 0) { 896 const weights = roots.map(r => 1 + countDescendants(String(r.pid)) * 0.5); 897 const totalWeight = weights.reduce((a, b) => a + b, 0); 898 899 let currentAngle = -Math.PI / 2; 900 roots.forEach((root, i) => { 901 const angleSpan = (weights[i] / totalWeight) * Math.PI * 2; 902 const angle = currentAngle + angleSpan / 2; 903 currentAngle += angleSpan; 904 positionNode(root, 0, angle, baseRadius, 0, 0); 905 }); 906 } 907 908 return { roots, byPid, children }; 909 } 910 911 function updateLabels() { 912 const container = document.getElementById('labels'); 913 container.innerHTML = ''; 914 scene.updateMatrixWorld(); 915 916 meshes.forEach((mesh, pid) => { 917 const pos = new THREE.Vector3(); 918 mesh.getWorldPosition(pos); 919 const labelPos = pos.clone(); 920 labelPos.y += (mesh.userData.size || 8) + 5; 921 labelPos.project(camera); 922 923 const x = (labelPos.x * 0.5 + 0.5) * width; 924 const y = (-labelPos.y * 0.5 + 0.5) * height; 925 926 if (labelPos.z < 1 && x > -100 && x < width + 100 && y > -100 && y < height + 100) { 927 const d = mesh.userData; 928 const color = '#' + (colors[d.category] || 0x666666).toString(16).padStart(6, '0'); 929 const distToCamera = camera.position.distanceTo(pos); 930 // Larger base scale, less reduction with distance 931 const proximityScale = Math.max(0.7, Math.min(3, 200 / distToCamera)); 932 // Higher minimum opacity - always readable 933 const opacity = focusedPid 934 ? (pid === focusedPid ? 1 : (d.parentInteresting === parseInt(focusedPid) ? 0.95 : 0.7)) 935 : Math.max(0.85, Math.min(1, 400 / distToCamera)); 936 937 const cpuPct = Math.min(100, d.cpu || 0); 938 const memMB = ((d.rss || 0) / 1024).toFixed(0); 939 940 // Extract short command for display (first 40 chars of cmdShort or cmd) 941 const cmdDisplay = d.cmdShort || d.cmd || ''; 942 const cmdShort = cmdDisplay.length > 50 ? cmdDisplay.slice(0, 47) + '...' : cmdDisplay; 943 944 // Calculate rotation based on connection to parent (make label parallel to line) 945 let rotation = 0; 946 const parentPid = String(d.parentInteresting || 0); 947 const parentMesh = meshes.has(parentPid) ? meshes.get(parentPid) : meshes.get('kernel'); 948 if (parentMesh && pid !== 'kernel') { 949 const parentPos = new THREE.Vector3(); 950 parentMesh.getWorldPosition(parentPos); 951 // Project both positions to 2D screen space 952 const childScreen = pos.clone().project(camera); 953 const parentScreen = parentPos.clone().project(camera); 954 // Calculate angle in screen space 955 const dx = (parentScreen.x - childScreen.x); 956 const dy = (parentScreen.y - childScreen.y); 957 rotation = Math.atan2(-dy, dx) * (180 / Math.PI); 958 // Clamp rotation to reasonable range (-45 to 45 degrees) 959 rotation = Math.max(-45, Math.min(45, rotation)); 960 } 961 962 const label = document.createElement('div'); 963 label.className = 'proc-label'; 964 label.style.left = x + 'px'; 965 label.style.top = y + 'px'; 966 label.style.opacity = opacity; 967 label.style.transform = 'translate(-50%, -100%) scale(' + proximityScale + ') rotate(' + rotation + 'deg)'; 968 // Show name, then command on second line, then stats (no background) 969 label.innerHTML = '<div class="icon">' + (d.icon || '●') + '</div>' + 970 '<div class="name" style="color:' + color + '">' + (d.name || pid) + '</div>' + 971 (cmdShort ? '<div class="cmd" style="color:' + scheme.foregroundMuted + ';font-size:8px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + cmdShort + '</div>' : '') + 972 '<div class="info" style="color:' + scheme.foregroundMuted + ';">' + memMB + 'MB · ' + cpuPct.toFixed(0) + '%</div>'; 973 container.appendChild(label); 974 } 975 }); 976 977 // Add labels for graveyard (dead) processes 978 graveyard.forEach((grave) => { 979 const mesh = grave.mesh; 980 if (!mesh) return; 981 982 const pos = new THREE.Vector3(); 983 mesh.getWorldPosition(pos); 984 const labelPos = pos.clone(); 985 labelPos.y += 8; 986 labelPos.project(camera); 987 988 const x = (labelPos.x * 0.5 + 0.5) * width; 989 const y = (-labelPos.y * 0.5 + 0.5) * height; 990 991 if (labelPos.z < 1 && x > -100 && x < width + 100 && y > -100 && y < height + 100) { 992 const distToCamera = camera.position.distanceTo(pos); 993 const proximityScale = Math.max(0.5, Math.min(2, 150 / distToCamera)); 994 const age = (Date.now() - grave.deathTime) / 1000; 995 const opacity = Math.max(0.3, 0.7 - age * 0.01); 996 997 const cmdShort = grave.cmd ? (grave.cmd.length > 30 ? grave.cmd.slice(0, 27) + '...' : grave.cmd) : ''; 998 const timeAgo = age < 60 ? Math.floor(age) + 's ago' : Math.floor(age / 60) + 'm ago'; 999 1000 const label = document.createElement('div'); 1001 label.className = 'proc-label graveyard'; 1002 label.style.left = x + 'px'; 1003 label.style.top = y + 'px'; 1004 label.style.opacity = opacity; 1005 label.style.transform = 'translate(-50%, -100%) scale(' + proximityScale + ')'; 1006 label.innerHTML = '<div class="icon">💀</div>' + 1007 '<div class="name" style="color:' + scheme.foregroundMuted + ';text-decoration:line-through;">' + (grave.name || grave.pid) + '</div>' + 1008 (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>' : '') + 1009 '<div class="info" style="color:' + scheme.foregroundMuted + ';opacity:0.6;">' + timeAgo + '</div>'; 1010 container.appendChild(label); 1011 } 1012 }); 1013 } 1014 1015 function updateViz(processData) { 1016 if (!processData?.interesting) return; 1017 1018 const processes = processData.interesting; 1019 document.getElementById('process-count').textContent = processes.length; 1020 1021 processTree = layoutTree(processes); 1022 const currentPids = new Set(processes.map(p => String(p.pid))); 1023 1024 processes.forEach(p => { 1025 const pid = String(p.pid); 1026 1027 if (!meshes.has(pid)) { 1028 const mesh = createNodeMesh(p); 1029 mesh.position.set(p.targetX || 0, p.targetY || 0, p.targetZ || 0); 1030 mesh.userData.targetPos.set(p.targetX || 0, p.targetY || 0, p.targetZ || 0); 1031 scene.add(mesh); 1032 meshes.set(pid, mesh); 1033 1034 // Respect current visibility 1035 const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources'; 1036 mesh.visible = !isSourcesTab; 1037 } else { 1038 const mesh = meshes.get(pid); 1039 const d = mesh.userData; 1040 d.cpu = p.cpu; d.mem = p.mem; d.rss = p.rss; d.name = p.name; 1041 d.targetPos.set(p.targetX || d.targetPos.x, p.targetY || d.targetPos.y, p.targetZ || d.targetPos.z); 1042 1043 const memMB = (p.rss || 10000) / 1024; 1044 d.size = Math.max(4, Math.min(12, 3 + memMB * 0.05 + p.cpu * 0.1)); 1045 mesh.scale.setScalar(d.size / 6); 1046 1047 const baseColor = colors[p.category] || 0x666666; 1048 const brighten = Math.min(1.8, 1 + p.cpu * 0.02); 1049 const r = ((baseColor >> 16) & 255) * brighten; 1050 const g = ((baseColor >> 8) & 255) * brighten; 1051 const b = (baseColor & 255) * brighten; 1052 mesh.material.color.setRGB(Math.min(255, r) / 255, Math.min(255, g) / 255, Math.min(255, b) / 255); 1053 mesh.material.opacity = 0.7 + p.cpu * 0.003; 1054 } 1055 1056 const parentPid = String(p.parentInteresting || 0); 1057 const childColor = colors[p.category] || 0x666666; 1058 if (parentPid && meshes.has(parentPid)) { 1059 const connKey = pid + '->' + parentPid; 1060 if (!connections.has(connKey)) { 1061 const line = createConnectionLine(childColor); 1062 scene.add(line); 1063 connections.set(connKey, { line, childPid: pid, parentPid, childColor }); 1064 1065 // Respect current visibility 1066 const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources'; 1067 line.visible = !isSourcesTab; 1068 } 1069 } else { 1070 const connKey = pid + '->kernel'; 1071 if (!connections.has(connKey)) { 1072 const line = createConnectionLine(childColor); 1073 scene.add(line); 1074 connections.set(connKey, { line, childPid: pid, parentPid: 'kernel', childColor }); 1075 1076 // Respect current visibility 1077 const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources'; 1078 line.visible = !isSourcesTab; 1079 } 1080 } 1081 }); 1082 1083 meshes.forEach((mesh, pid) => { 1084 if (pid === 'kernel') return; 1085 if (!currentPids.has(pid) && !mesh.userData.isDead) { 1086 mesh.userData.isDead = true; 1087 mesh.userData.deathTime = Date.now(); 1088 1089 const graveyardIndex = graveyard.length; 1090 const col = graveyardIndex % 10; 1091 const row = Math.floor(graveyardIndex / 10); 1092 mesh.userData.targetPos.set((col - 4.5) * 25, GRAVEYARD_Y - row * 20, 0); 1093 1094 mesh.material.opacity = 0.25; 1095 mesh.material.color.setHex(scheme.three.deadProcess); 1096 1097 // Store full process info for graveyard labels 1098 const d = mesh.userData; 1099 const graveItem = { 1100 pid, 1101 mesh, 1102 name: d.name, 1103 icon: d.icon || '💀', 1104 cmd: d.cmdShort || d.cmd || '', 1105 category: d.category, 1106 deathTime: Date.now() 1107 }; 1108 graveyard.push(graveItem); 1109 meshes.delete(pid); 1110 1111 // Respect current visibility 1112 const isSourcesTab = window.ASTTreeViz?.getTab() === 'sources'; 1113 mesh.visible = !isSourcesTab; 1114 1115 while (graveyard.length > MAX_GRAVEYARD) { 1116 const oldest = graveyard.shift(); 1117 scene.remove(oldest.mesh); 1118 if (oldest.mesh.geometry) oldest.mesh.geometry.dispose(); 1119 if (oldest.mesh.material) oldest.mesh.material.dispose(); 1120 } 1121 } 1122 }); 1123 1124 const graveyardPids = new Set(graveyard.map(g => g.pid)); 1125 connections.forEach((conn, key) => { 1126 const childExists = meshes.has(conn.childPid) || graveyardPids.has(conn.childPid); 1127 const parentExists = meshes.has(conn.parentPid) || graveyardPids.has(conn.parentPid); 1128 if (!childExists || !parentExists) { 1129 scene.remove(conn.line); 1130 conn.line.geometry.dispose(); 1131 conn.line.material.dispose(); 1132 connections.delete(key); 1133 } 1134 }); 1135 1136 // Refresh tour list if in tour mode (processes may have changed) 1137 if (tourMode) { 1138 const oldPid = tourProcessList[tourIndex]; 1139 tourProcessList = buildTourList(); 1140 // Try to stay on the same process if it still exists 1141 const newIndex = tourProcessList.indexOf(oldPid); 1142 if (newIndex !== -1) { 1143 tourIndex = newIndex; 1144 } else if (tourIndex >= tourProcessList.length) { 1145 tourIndex = Math.max(0, tourProcessList.length - 1); 1146 } 1147 updateTourUI(); 1148 } 1149 } 1150 1151 let time = 0; 1152 function animate() { 1153 requestAnimationFrame(animate); 1154 time += 0.016; 1155 1156 if (focusedPid && meshes.has(focusedPid)) { 1157 focusTarget.lerp(meshes.get(focusedPid).position, 0.08); 1158 } 1159 1160 controls.target.lerp(focusTarget, transitioning ? 0.06 : 0.02); 1161 1162 if (focusDistance !== null) { 1163 const currentDist = camera.position.distanceTo(controls.target); 1164 if (Math.abs(currentDist - focusDistance) > 5) { 1165 const dir = camera.position.clone().sub(controls.target).normalize(); 1166 const targetPos = controls.target.clone().add(dir.multiplyScalar(focusDistance)); 1167 camera.position.lerp(targetPos, 0.04); 1168 } else { 1169 transitioning = false; 1170 } 1171 } else { 1172 transitioning = false; 1173 } 1174 1175 controls.update(); 1176 1177 if (kernelGlow) { 1178 kernelGlow.rotation.z = time * 0.3; 1179 kernelGlow.rotation.x = Math.PI / 2 + Math.sin(time * 0.5) * 0.1; 1180 } 1181 if (kernelCore) { 1182 const pulse = 1 + Math.sin(time * 0.8) * 0.1; 1183 kernelCore.scale.setScalar(pulse); 1184 } 1185 if (kernelMesh) { 1186 kernelMesh.rotation.y = time * 0.1; 1187 } 1188 1189 // Graveyard animation with null checks 1190 graveyard.forEach((grave, i) => { 1191 const mesh = grave.mesh; 1192 if (mesh && mesh.userData && mesh.material) { 1193 const d = mesh.userData; 1194 mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.02; 1195 mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.015; 1196 mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.02; 1197 mesh.position.x += Math.sin(time * 0.3 + i) * 0.05; 1198 const age = (Date.now() - grave.deathTime) / 1000; 1199 mesh.material.opacity = Math.max(0.1, 0.3 - age * 0.005); 1200 } 1201 }); 1202 1203 // Active meshes animation with null checks 1204 meshes.forEach((mesh, pid) => { 1205 if (!mesh || !mesh.userData || !mesh.material) return; 1206 const d = mesh.userData; 1207 const cpu = d.cpu || 0; 1208 const isFocused = focusedPid === pid; 1209 const isRelated = focusedPid && (d.parentInteresting === parseInt(focusedPid) || String(d.parentInteresting) === focusedPid); 1210 1211 mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.03; 1212 mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.03; 1213 mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.03; 1214 1215 const float = Math.sin(time * 0.5 + d.pulsePhase) * 2; 1216 mesh.position.y += float * 0.02; 1217 1218 const pulseAmp = isFocused ? 0.2 : (0.1 + cpu * 0.005); 1219 const pulse = 1 + Math.sin(time * (1 + cpu * 0.05) + d.pulsePhase) * pulseAmp; 1220 const sizeMultiplier = isFocused ? 1.5 : (isRelated ? 1.2 : 1); 1221 mesh.scale.setScalar((d.size / 6) * pulse * sizeMultiplier); 1222 1223 if (focusedPid) { 1224 mesh.material.opacity = isFocused ? 1 : (isRelated ? 0.8 : 0.3); 1225 } else { 1226 mesh.material.opacity = 0.7 + cpu * 0.003; 1227 } 1228 }); 1229 1230 connections.forEach(conn => { 1231 const childMesh = meshes.get(conn.childPid); 1232 const parentMesh = meshes.get(conn.parentPid); 1233 if (childMesh && parentMesh) { 1234 updateConnectionMesh(conn, childMesh.position, parentMesh.position); 1235 const involvesFocus = focusedPid && (conn.childPid === focusedPid || conn.parentPid === focusedPid); 1236 conn.line.material.opacity = focusedPid ? (involvesFocus ? 0.9 : 0.15) : 0.6; 1237 // Use child's category color for the line (color-coded connections) 1238 const childCategory = childMesh.userData?.category; 1239 const lineColor = involvesFocus ? scheme.three.connectionActive : (colors[childCategory] || conn.childColor || scheme.three.connectionLine); 1240 conn.line.material.color.setHex(lineColor); 1241 const thickness = involvesFocus ? 2.5 : 1.5; 1242 conn.line.scale.x = thickness / 1.5; 1243 conn.line.scale.z = thickness / 1.5; 1244 } 1245 }); 1246 1247 // 🌳 AST Tree Animation (if loaded) 1248 if (window.ASTTreeViz?.animateAST) { 1249 window.ASTTreeViz.animateAST(); 1250 } 1251 1252 renderer.render(scene, camera); 1253 updateLabels(); 1254 } 1255 1256 // Connection state tracking 1257 let connectionState = 'disconnected'; // disconnected, connecting, connected 1258 let reconnectAttempts = 0; 1259 let lastConnectTime = 0; 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 1272 function updateConnectionUI() { 1273 const dot = document.getElementById('status-dot'); 1274 let indicator = document.getElementById('connection-indicator'); 1275 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; 1289 transition: opacity 0.5s ease; 1290 font-family: monospace; 1291 font-size: 10px; 1292 line-height: 1.5; 1293 `; 1294 document.body.appendChild(indicator); 1295 } 1296 1297 if (connectionState === 'connected') { 1298 dot?.classList.add('online'); 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); 1305 } else { 1306 dot?.classList.remove('online'); 1307 indicator.style.display = 'block'; 1308 indicator.style.opacity = '1'; 1309 1310 if (connectionState === 'connecting') { 1311 addConnectionLog(`connecting... (attempt ${reconnectAttempts})`); 1312 } else { 1313 addConnectionLog(`waiting to reconnect (attempt ${reconnectAttempts})`); 1314 } 1315 1316 renderConnectionIndicator(indicator); 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 `; 1340 } 1341 1342 function connectWS() { 1343 connectionState = 'connecting'; 1344 reconnectAttempts++; 1345 updateConnectionUI(); 1346 1347 try { 1348 ws = new WebSocket('ws://127.0.0.1:7890/ws'); 1349 1350 ws.onopen = () => { 1351 connectionState = 'connected'; 1352 reconnectAttempts = 0; 1353 lastConnectTime = Date.now(); 1354 updateConnectionUI(); 1355 console.log('🟢 Connected to process server'); 1356 }; 1357 1358 ws.onclose = () => { 1359 connectionState = 'disconnected'; 1360 updateConnectionUI(); 1361 // Exponential backoff: 1s, 2s, 4s, 8s, max 10s 1362 const delay = Math.min(1000 * Math.pow(2, Math.min(reconnectAttempts - 1, 3)), 10000); 1363 console.log(`🔴 Disconnected, reconnecting in ${delay}ms (attempt ${reconnectAttempts})`); 1364 setTimeout(connectWS, delay); 1365 }; 1366 1367 ws.onerror = (err) => { 1368 console.log('🔴 WebSocket error:', err); 1369 ws.close(); 1370 }; 1371 1372 ws.onmessage = (e) => { 1373 try { 1374 const data = JSON.parse(e.data); 1375 if (data.system) { 1376 document.getElementById('uptime').textContent = data.system.uptime.formatted; 1377 document.getElementById('cpus').textContent = data.system.cpus; 1378 const m = data.system.memory; 1379 document.getElementById('mem-text').textContent = m.used + ' / ' + m.total; 1380 1381 // Update stats graph with system data 1382 updateStatsGraph(data.system); 1383 } 1384 updateViz(data.processes); 1385 } catch {} 1386 }; 1387 } catch (err) { 1388 console.log('🔴 WebSocket creation error:', err); 1389 connectionState = 'disconnected'; 1390 updateConnectionUI(); 1391 setTimeout(connectWS, 2000); 1392 } 1393 } 1394 1395 // 📊 Stats Graph (CPU/Memory history) 1396 const GRAPH_POINTS = 60; // 60 data points 1397 const cpuHistory = new Array(GRAPH_POINTS).fill(0); 1398 const memHistory = new Array(GRAPH_POINTS).fill(0); 1399 let statsCanvas = null; 1400 let statsCtx = null; 1401 1402 function initStatsGraph() { 1403 statsCanvas = document.getElementById('stats-graph-canvas'); 1404 if (statsCanvas) { 1405 statsCtx = statsCanvas.getContext('2d'); 1406 // Set actual pixel dimensions for crisp rendering 1407 const rect = statsCanvas.getBoundingClientRect(); 1408 statsCanvas.width = rect.width * window.devicePixelRatio; 1409 statsCanvas.height = rect.height * window.devicePixelRatio; 1410 statsCtx.scale(window.devicePixelRatio, window.devicePixelRatio); 1411 } 1412 } 1413 1414 function updateStatsGraph(system) { 1415 if (!statsCtx) initStatsGraph(); 1416 if (!statsCtx) return; 1417 1418 // Parse memory usage 1419 const m = system.memory; 1420 let memPct = 0; 1421 if (m && m.used && m.total) { 1422 const usedNum = parseFloat(m.used.replace(/[^\d.]/g, '')); 1423 const totalNum = parseFloat(m.total.replace(/[^\d.]/g, '')); 1424 if (totalNum > 0) { 1425 memPct = (usedNum / totalNum) * 100; 1426 } 1427 } 1428 1429 // Calculate total CPU usage from all processes 1430 let totalCpu = 0; 1431 meshes.forEach((mesh, pid) => { 1432 if (pid !== 'kernel' && mesh.userData.cpu) { 1433 totalCpu += mesh.userData.cpu; 1434 } 1435 }); 1436 // Normalize to percentage (divide by number of CPUs) 1437 const numCpus = parseInt(system.cpus) || 1; 1438 const cpuPct = Math.min(100, totalCpu / numCpus); 1439 1440 // Shift history and add new values 1441 cpuHistory.shift(); 1442 cpuHistory.push(cpuPct); 1443 memHistory.shift(); 1444 memHistory.push(memPct); 1445 1446 // Update text labels 1447 const cpuEl = document.getElementById('cpu-pct'); 1448 const memEl = document.getElementById('mem-pct'); 1449 if (cpuEl) cpuEl.textContent = cpuPct.toFixed(1); 1450 if (memEl) memEl.textContent = memPct.toFixed(1); 1451 1452 // Draw graph 1453 drawStatsGraph(); 1454 } 1455 1456 function drawStatsGraph() { 1457 if (!statsCtx || !statsCanvas) return; 1458 1459 const rect = statsCanvas.getBoundingClientRect(); 1460 const w = rect.width; 1461 const h = rect.height; 1462 1463 // Clear canvas 1464 statsCtx.clearRect(0, 0, w, h); 1465 1466 // Colors based on theme 1467 const cpuColor = currentTheme === 'light' ? '#006400' : '#50fa7b'; 1468 const memColor = currentTheme === 'light' ? '#c71585' : '#ff79c6'; 1469 const gridColor = currentTheme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)'; 1470 1471 // Draw grid lines 1472 statsCtx.strokeStyle = gridColor; 1473 statsCtx.lineWidth = 0.5; 1474 for (let i = 0; i <= 4; i++) { 1475 const y = (h / 4) * i; 1476 statsCtx.beginPath(); 1477 statsCtx.moveTo(0, y); 1478 statsCtx.lineTo(w, y); 1479 statsCtx.stroke(); 1480 } 1481 1482 // Draw CPU line 1483 statsCtx.strokeStyle = cpuColor; 1484 statsCtx.lineWidth = 1.5; 1485 statsCtx.beginPath(); 1486 for (let i = 0; i < GRAPH_POINTS; i++) { 1487 const x = (w / (GRAPH_POINTS - 1)) * i; 1488 const y = h - (cpuHistory[i] / 100) * h; 1489 if (i === 0) statsCtx.moveTo(x, y); 1490 else statsCtx.lineTo(x, y); 1491 } 1492 statsCtx.stroke(); 1493 1494 // Fill CPU area (semi-transparent) 1495 statsCtx.fillStyle = cpuColor.replace(')', ',0.15)').replace('rgb', 'rgba').replace('#', ''); 1496 if (cpuColor.startsWith('#')) { 1497 const r = parseInt(cpuColor.slice(1, 3), 16); 1498 const g = parseInt(cpuColor.slice(3, 5), 16); 1499 const b = parseInt(cpuColor.slice(5, 7), 16); 1500 statsCtx.fillStyle = `rgba(${r},${g},${b},0.15)`; 1501 } 1502 statsCtx.beginPath(); 1503 statsCtx.moveTo(0, h); 1504 for (let i = 0; i < GRAPH_POINTS; i++) { 1505 const x = (w / (GRAPH_POINTS - 1)) * i; 1506 const y = h - (cpuHistory[i] / 100) * h; 1507 statsCtx.lineTo(x, y); 1508 } 1509 statsCtx.lineTo(w, h); 1510 statsCtx.closePath(); 1511 statsCtx.fill(); 1512 1513 // Draw Memory line 1514 statsCtx.strokeStyle = memColor; 1515 statsCtx.lineWidth = 1.5; 1516 statsCtx.beginPath(); 1517 for (let i = 0; i < GRAPH_POINTS; i++) { 1518 const x = (w / (GRAPH_POINTS - 1)) * i; 1519 const y = h - (memHistory[i] / 100) * h; 1520 if (i === 0) statsCtx.moveTo(x, y); 1521 else statsCtx.lineTo(x, y); 1522 } 1523 statsCtx.stroke(); 1524 1525 // Fill Memory area (semi-transparent) 1526 if (memColor.startsWith('#')) { 1527 const r = parseInt(memColor.slice(1, 3), 16); 1528 const g = parseInt(memColor.slice(3, 5), 16); 1529 const b = parseInt(memColor.slice(5, 7), 16); 1530 statsCtx.fillStyle = `rgba(${r},${g},${b},0.15)`; 1531 } 1532 statsCtx.beginPath(); 1533 statsCtx.moveTo(0, h); 1534 for (let i = 0; i < GRAPH_POINTS; i++) { 1535 const x = (w / (GRAPH_POINTS - 1)) * i; 1536 const y = h - (memHistory[i] / 100) * h; 1537 statsCtx.lineTo(x, y); 1538 } 1539 statsCtx.lineTo(w, h); 1540 statsCtx.closePath(); 1541 statsCtx.fill(); 1542 } 1543 1544 window.addEventListener('resize', () => { 1545 width = window.innerWidth; 1546 height = window.innerHeight; 1547 camera.aspect = width / height; 1548 camera.updateProjectionMatrix(); 1549 renderer.setSize(width, height); 1550 1551 // Reinitialize stats graph canvas on resize 1552 statsCanvas = null; 1553 statsCtx = null; 1554 initStatsGraph(); 1555 }); 1556 1557 // 🎨 Theme switching function 1558 function setTheme(themeName) { 1559 if (themeName !== 'light' && themeName !== 'dark') return; 1560 currentTheme = themeName; 1561 scheme = colorSchemes[currentTheme]; 1562 colors = scheme.categories; 1563 1564 // Update scene background 1565 scene.background.setHex(scheme.three.sceneBackground); 1566 renderer.setClearColor(scheme.three.sceneBackground); 1567 1568 // Update body styling 1569 document.body.dataset.theme = themeName; 1570 document.body.style.background = scheme.background; 1571 document.body.style.color = scheme.foreground; 1572 1573 // Update kernel mesh colors 1574 if (kernelMesh) { 1575 kernelMesh.children[0].material.color.setHex(scheme.three.kernelOuter); 1576 if (kernelGlow) kernelGlow.material.color.setHex(scheme.three.kernelRing); 1577 if (kernelCore) kernelCore.material.color.setHex(scheme.three.kernelCore); 1578 } 1579 1580 // Update all process node colors 1581 meshes.forEach((mesh, pid) => { 1582 if (pid === 'kernel') return; 1583 const category = mesh.userData.category; 1584 const newColor = colors[category] || 0x666666; 1585 mesh.material.color.setHex(newColor); 1586 mesh.userData.baseColor = newColor; 1587 }); 1588 1589 // Update connections 1590 connections.forEach(conn => { 1591 conn.line.material.color.setHex(scheme.three.connectionLine); 1592 }); 1593 1594 // Update graveyard 1595 graveyard.forEach(grave => { 1596 if (grave.mesh && grave.mesh.material) { 1597 grave.mesh.material.color.setHex(scheme.three.deadProcess); 1598 } 1599 }); 1600 1601 // Update CSS styles 1602 updateThemeStyles(); 1603 } 1604 1605 function toggleTheme() { 1606 setTheme(currentTheme === 'dark' ? 'light' : 'dark'); 1607 return currentTheme; 1608 } 1609 1610 function updateThemeStyles() { 1611 // Update dynamic CSS based on theme 1612 let styleEl = document.getElementById('theme-dynamic-styles'); 1613 if (!styleEl) { 1614 styleEl = document.createElement('style'); 1615 styleEl.id = 'theme-dynamic-styles'; 1616 document.head.appendChild(styleEl); 1617 } 1618 styleEl.textContent = ` 1619 /* Header styles */ 1620 .title .dot { color: ${scheme.accentBright}; } 1621 .status-dot { background: ${scheme.accent}; } 1622 .status-dot.online { background: ${scheme.statusOnline}; } 1623 .header-center { color: ${scheme.foregroundMuted}; } 1624 .header-center .val { color: ${scheme.foregroundBright}; } 1625 .header-btn { 1626 background: ${currentTheme === 'light' ? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.08)'}; 1627 border-color: ${currentTheme === 'light' ? 'rgba(0,0,0,0.15)' : 'rgba(255,255,255,0.15)'}; 1628 color: ${scheme.foregroundBright}; 1629 } 1630 .header-btn:hover { border-color: ${scheme.accentBright}; } 1631 1632 /* Center panel styles */ 1633 .center-panel { 1634 background: ${currentTheme === 'light' ? 'rgba(252,247,197,0.8)' : 'rgba(24,19,24,0.7)'}; 1635 border-color: ${currentTheme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.08)'}; 1636 } 1637 .process-counter .count { color: ${scheme.foregroundBright}; } 1638 .process-counter .label { color: ${scheme.foregroundMuted}; } 1639 1640 /* Status bar styles */ 1641 .status-bar { color: ${scheme.foregroundMuted}; } 1642 .dev-badge { 1643 background: ${scheme.accentBright}; 1644 color: ${currentTheme === 'light' ? '#fff' : '#000'}; 1645 } 1646 1647 /* Label styles - no background, stronger text shadow */ 1648 .proc-label { 1649 text-shadow: 0 0 6px ${scheme.background}, 0 0 10px ${scheme.background}, 0 0 14px ${scheme.background}; 1650 background: transparent; 1651 } 1652 .proc-label .info { color: ${scheme.foregroundMuted}; } 1653 1654 /* Tour UI styles */ 1655 #tour-ui { background: ${scheme.ui.overlay}; border-color: ${scheme.foregroundMuted}; } 1656 `; 1657 } 1658 1659 // Apply initial theme styles 1660 updateThemeStyles(); 1661 1662 // Expose for external use (mock data injection, etc.) 1663 window.ProcessTreeViz = { 1664 updateViz, 1665 scene, 1666 camera, 1667 renderer, 1668 controls, 1669 meshes, 1670 connections, 1671 graveyard, 1672 // Tour mode 1673 startTour, 1674 exitTour, 1675 tourNext, 1676 tourPrev, 1677 toggleAutoPlay, 1678 isTourMode: () => tourMode, 1679 // Theme control 1680 setTheme, 1681 toggleTheme, 1682 getTheme: () => currentTheme, 1683 getScheme: () => scheme, 1684 colorSchemes 1685 }; 1686 1687 // Add tour button to #header-right (new structure) or .header-right (old structure) 1688 const headerRight = document.getElementById('header-right') || document.querySelector('.header-right'); 1689 if (headerRight) { 1690 const tourBtn = document.createElement('button'); 1691 tourBtn.id = 'tour-btn'; 1692 tourBtn.className = 'hdr-btn'; 1693 tourBtn.textContent = '🎬'; 1694 tourBtn.title = 'Tour Mode'; 1695 tourBtn.onclick = () => { 1696 const isSourceTour = window.ASTTreeViz?.getTab() === 'sources' && window.ASTTreeViz?.isTourMode(); 1697 if (!tourMode && !isSourceTour) startTour(); 1698 else exitTour(); 1699 }; 1700 headerRight.insertBefore(tourBtn, headerRight.firstChild); 1701 } 1702 1703 animate(); 1704 connectWS(); 1705})();