← 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() {