← back to Terminal 3d

.claude/worktrees/terminal-3d-build

125 lines

commit ffe3c48052cd98d58dacd68f09d819369a9ea9e8
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 25 12:00:03 2026 -0700

    terminal-3d: fix Map.size crash, click-through overlays, HUD z-order, noise filter, README
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EUeESiT9xNPuEWGTrhbk4T

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..224eecb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# Terminal Space (terminal-3d)
+
+A live 3D view of every open iTerm2 terminal and what it is doing, built with React + three.js
+(react-three-fiber + drei, loaded from esm.sh — no build step, zero npm deps).
+
+```sh
+node server.js            # → http://127.0.0.1:9931   (PORT=… to change; binds localhost only)
+```
+
+## What you see
+- One glowing **monitor per terminal**. Its bezel and text use the tab's semantic dot colour
+  (green working · orange paste · purple gated · yellow needs direction · light-blue needs Steve · pink parked · grey no dot).
+- Each screen shows the tty, the ticket label and title, the foreground command, the top child processes by CPU, and the cwd.
+- Floating **orb** = the dot. It pulses while the terminal is working and **bobs** when the terminal needs Steve.
+- Side **bar** = CPU of the terminal's whole process tree.
+- Layouts: **Arc** (curved wall), **Wall** (grid), **By status** (grouped by colour, ≤4 high).
+- Colour chips filter the scene. The layout, orbit and filter choices persist in localStorage.
+- **Click a monitor** → the camera flies to it and a detail panel opens with **Jump to this tab in iTerm**
+  (runs `allcolordots.sh jump <tty>`). Esc or a click on empty space returns to the overview.
+
+## Data (GET /api/terminals, polled every 3s)
+- `ps -axo …`: one snapshot, grouped by tty. A terminal is any `ttysNNN` that has a login shell.
+- `~/.claude/skills/allcolordots/allcolordots.sh --json`: dot colour and label.
+- `lsof -a -d cwd`: one call for every terminal's working directory.
+- `~/.claude/tickets/events.jsonl`: ticket titles, cached by mtime.
+
+Read-only, apart from the optional "jump to tab" focus action. $0, all local.
diff --git a/public/index.html b/public/index.html
index 113eb24..6b9ffea 100644
--- a/public/index.html
+++ b/public/index.html
@@ -9,7 +9,7 @@
   :root { --bg:#05070d; --panel:rgba(12,16,28,.82); --line:rgba(148,163,184,.18); --ink:#e2e8f0; --dim:#94a3b8; }
   * { box-sizing:border-box; }
   html,body,#root { margin:0; height:100%; background:var(--bg); color:var(--ink); font:13px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif; overflow:hidden; }
-  .hud { position:fixed; left:16px; top:16px; right:16px; display:flex; gap:10px; flex-wrap:wrap; align-items:center; pointer-events:none; z-index:5; }
+  .hud { position:fixed; left:16px; top:16px; right:16px; display:flex; gap:10px; flex-wrap:wrap; align-items:center; pointer-events:none; z-index:30; }
   .hud > * { pointer-events:auto; }
   .brand { font-weight:700; letter-spacing:.04em; padding:8px 12px; background:var(--panel); border:1px solid var(--line); border-radius:10px; backdrop-filter:blur(8px); }
   .brand small { color:var(--dim); font-weight:400; margin-left:8px; }
@@ -19,7 +19,7 @@
   .seg { display:inline-flex; background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; }
   .seg button { background:none; border:0; color:var(--dim); padding:7px 11px; font:inherit; cursor:pointer; }
   .seg button.on { color:var(--ink); background:rgba(148,163,184,.16); }
-  .side { position:fixed; right:16px; bottom:16px; width:min(380px, calc(100vw - 32px)); max-height:60vh; overflow:auto; background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:14px 16px; backdrop-filter:blur(10px); z-index:6; }
+  .side { position:fixed; right:16px; bottom:16px; width:min(380px, calc(100vw - 32px)); max-height:60vh; overflow:auto; background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:14px 16px; backdrop-filter:blur(10px); z-index:31; }
   .side h2 { margin:0 0 4px; font-size:15px; display:flex; align-items:center; gap:8px; }
   .side .kv { display:grid; grid-template-columns:80px 1fr; gap:4px 10px; margin:10px 0; }
   .side .kv b { color:var(--dim); font-weight:500; }
@@ -29,8 +29,8 @@
   .btn { background:#1e293b; color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:7px 12px; cursor:pointer; font:inherit; }
   .btn.primary { background:#0f766e; border-color:#14b8a6; }
   .row { display:flex; gap:8px; margin-top:10px; }
-  .help { position:fixed; left:16px; bottom:16px; color:var(--dim); font-size:12px; background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:6px 10px; z-index:5; }
-  .err { position:fixed; left:50%; top:70px; transform:translateX(-50%); background:#7f1d1d; padding:8px 14px; border-radius:8px; z-index:9; }
+  .help { position:fixed; left:16px; bottom:16px; color:var(--dim); font-size:12px; background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:6px 10px; z-index:30; }
+  .err { position:fixed; left:50%; top:70px; transform:translateX(-50%); background:#7f1d1d; padding:8px 14px; border-radius:8px; z-index:32; }
   /* in-scene screen content */
   .screen { width:340px; height:210px; padding:12px 14px; font:12px/1.35 ui-monospace,SFMono-Regular,Menlo,monospace; color:#d1fae5; overflow:hidden; pointer-events:none; user-select:none; }
   .screen .top { display:flex; justify-content:space-between; font-weight:700; font-size:13px; }
@@ -107,7 +107,7 @@ function layout(terms, mode) {
       pos.groups.push({ key: k, x: x + widths[gi] / 2, n: groups[k].length });
       x += widths[gi] + GAP;
     });
-    pos.size = { w: total, h: Math.min(ROWS, Math.max(...keys.map((k) => groups[k].length))) * 3.1 };
+    pos.dims = { w: total, h: Math.min(ROWS, Math.max(...keys.map((k) => groups[k].length))) * 3.1 };
   } else { // arc — two curved tiers of monitors facing the camera
     const rows = n > 10 ? 2 : 1, per = Math.ceil(n / rows);
     const span = Math.min(1.9, Math.max(0.4, per * 0.25));
@@ -162,7 +162,7 @@ function Monitor({ t, target, selected, onSelect }) {
         <planeGeometry args=${[3.64, 2.3]} />
         <meshStandardMaterial ref=${screenMat} color="#020617" emissive=${col} emissiveIntensity=${0.4} toneMapped=${false} />
       </mesh>
-      <${Html} transform distanceFactor=${3.6} position=${[0, 0, 0.13]} zIndexRange=${[10, 0]}>
+      <${Html} transform distanceFactor=${3.6} position=${[0, 0, 0.13]} zIndexRange=${[10, 0]} pointerEvents="none">
         <div class="screen" style=${{ color: c.hex, textShadow: '0 0 6px ' + c.hex + '55', background: 'rgba(2,6,23,.92)', borderRadius: '6px' }}>
           <div class="top"><span>${t.tty}</span><span>${t.kind} · ${t.cpu}% cpu</span></div>
           <div class="lbl">${t.label || c.name}</div>
@@ -221,7 +221,7 @@ function Scene({ terms, mode, selected, setSelected, autoRotate }) {
     }
     let w, hgt;
     if (mode === 'grid') { const cols = Math.ceil(Math.sqrt(n * 1.6)); w = cols * 4.4; hgt = Math.ceil(n / cols) * 3.1; }
-    else ({ w, h: hgt } = pos.size || { w: 5, h: 3 });
+    else ({ w, h: hgt } = pos.dims || { w: 5, h: 3 });
     const d = Math.max(10, Math.max(w / 1.4, hgt * 1.2) + 3);
     const h = 1.8 + hgt / 2 - 1.5;
     return { cam: new THREE.Vector3(0, h + 2, d), look: new THREE.Vector3(0, h, 0) };
@@ -239,7 +239,7 @@ function Scene({ terms, mode, selected, setSelected, autoRotate }) {
       ${terms.map((t) => html`<${Monitor} key=${t.tty} t=${t} target=${pos.get(t.tty)}
         selected=${selected === t.tty} onSelect=${setSelected} />`)}
       ${mode === 'color' && (pos.groups || []).map((g) => html`
-        <${Html} key=${g.key} position=${[g.x, 0.1, 1.8]} center>
+        <${Html} key=${g.key} position=${[g.x, 0.1, 1.8]} center pointerEvents="none">
           <div style=${{ color: COLORS[g.key]?.hex || '#94a3b8', font: '700 13px ui-sans-serif,system-ui', letterSpacing: '.08em', whiteSpace: 'nowrap', textShadow: '0 0 8px currentColor' }}>
             ${(COLORS[g.key]?.name || g.key).toUpperCase()} · ${g.n}</div>
         <//>`)}
diff --git a/server.js b/server.js
index 8efe2a8..f8f79c2 100644
--- a/server.js
+++ b/server.js
@@ -37,7 +37,7 @@ function ticketTitles() {
 }
 
 const WRAPPERS = /^(\/usr\/bin\/login|login|-?zsh|-?bash|-?sh|\/bin\/sh \/usr\/bin\/command)\b/;
-const NOISE = /^(caffeinate|npm exec|.*mcp|claude bg-spare|claude bg-pty-host|claude\.exe daemon|bash .*statusline|curl -s --max-time 1 |Python .*terminal_status\.py|git -c core\.fsmonitor=)/;
+const NOISE = /^(caffeinate|npm exec|.*mcp|claude(\.exe)? (--)?bg-(spare|pty-host)|claude\.exe daemon|bash .*statusline|curl -s --max-time 1 |Python .*terminal_status\.py|git -c core\.fsmonitor=)/;
 const short = (c) => c.replace(/^\S*\//, '').slice(0, 120);
 
 async function snapshot() {