Verdict (short): DO IT — but "lightweight 3D", not a tenacitOS clone. Vanilla Three.js, vendored locally, new tab/page served by the existing viewer, reusing /api/state. Effort ≈ M (a few days). Confidence high.
Verdict (short): DO IT — but "lightweight 3D", not a tenacitOS clone. Vanilla Three.js, vendored locally, new tab/page served by the existing viewer, reusing /api/state. Effort ≈ M (a few days). Confidence high.
Product: tenacitOS — "OpenClaw Mission Control Dashboard", a tabbed web dashboard for an OpenClaw instance. https://github.com/carlosazaustre/tenacitOS
Stack (README tech table): Next.js 15 (App Router) · React 19 · Tailwind CSS v4 · React Three Fiber + Drei · Recharts · SQLite. https://github.com/carlosazaustre/tenacitOS
Exact deps (fetched package.json, branch main): three ^0.183.0, @react-three/fiber ^9.5.0, @react-three/drei ^10.7.7, @react-three/rapier ^2.2.0 (physics engine), next 16.1.6, react 19.2.3. https://raw.githubusercontent.com/carlosazaustre/tenacitOS/main/package.json
Feature description: "🏢 Office 3D — Interactive 3D office with one desk per agent (React Three Fiber)", screenshot captioned "one voxel avatar per agent". https://github.com/carlosazaustre/tenacitOS
Route: it's its own (unprotected) page — src/app/office/ — separate from the protected dashboard. Component folder src/components/Office3D/. https://github.com/carlosazaustre/tenacitOS · https://api.github.com/repos/carlosazaustre/tenacitOS/contents/src/components/Office3D
Office3D module contents (fetched dir listing): OfficeScene/AgentDesk.tsx, AgentPanel.tsx, Avatar.tsx, AvatarModel.tsx, CoffeeMachine.tsx, FileCabinet.tsx, … i.e. props/prop-models + GLB avatars + an agent detail side panel. https://api.github.com/repos/carlosazaustre/tenacitOS/contents/src/components/Office3D
Agents/avatars: positions/names/colors live in src/components/Office3D/agentsConfig.ts; custom avatars are Ready Player Me .glb models in public/models/<agentId>.glb, falling back to a colored sphere if no file exists. https://github.com/carlosazaustre/tenacitOS
Takeaway: tenacitOS's Office 3D = React 19 + R3F + Drei + Rapier physics, rendered client-side, one desk + one GLB/voxel avatar per agent, agent data auto-discovered from openclaw.json. It is a Next.js+React ecosystem feature; adopting it wholesale means adopting the whole app.
"3D AI Agent Office" (WebGL showcase): React 19 · Three.js · React Three Fiber · Drei · Zustand; isometric/WebGL office where role-based agents (Backend/UX/QA/PO) move and chat, 100% local. https://medium.com/@agustinafassina_92108/building-a-3d-ai-agent-office-100-local-0-api-costs-and-fully-autonomous-884cf4fcc37b
three.js forum: "Agent visualization three.js R3F openclaw integration" — community discussion thread (concept, not a product) confirming the R3F+OpenClaw pattern is a recognised direction. https://discourse.threejs.org/t/agent-visualization-three-js-r3f-openclaw-integration/90247
Babylon.js is the main alternative 3D engine (Apache-2.0, @babylonjs/core 9.30.0, huge unpacked footprint but tree-shaken at build). https://registry.npmjs.org/@babylonjs/core/latest
Everything above renders in the browser (R3F is "a React renderer for Three.js" in the browser; the demos are WebGL apps). Our VM needs zero graphics capability — it's just an HTTP file server. https://registry.npmjs.org/@react-three/fiber/latest · https://medium.com/@agustinafassina_92108/building-a-3d-ai-agent-office-100-local-0-api-costs-and-fully-autonomous-884cf4fcc37b
CDN (no vendoring): page loads Three.js from a CDN (r163+ defaults to jsDelivr; unpkg also works) via an import map — no build step. Requires the owner's browser to have internet at view time. https://sbcode.net/threejs/importmap · https://discourse.threejs.org/t/unpkg-cdn-link-sometimes-seem-to-fail/64109 · https://discoverthreejs.com/book/introduction/get-threejs
three.module.js core bundle ≈ 1.2 MB unminified (and you'd normally tree-shake/serve more) — so vendor the single module build, don't ship the whole 20 MB npm package. https://discourse.threejs.org/t/unpkg-cdn-link-sometimes-seem-to-fail/64109 · https://registry.npmjs.org/three/latest
React Three Fiber is the wrong choice for us. R3F ^9.x requires React (peer dep; pulls react, scheduler, zustand, etc.) and a bundler in practice — our MC is a single hand-written vanilla-JS HTML page with no React and no build step. Adopting R3F would mean rebuilding the app as a React/bundler project (i.e. adopting tenacitOS's whole stack) — not justified for one tab. https://registry.npmjs.org/@react-three/fiber/latest · https://www.npmjs.com/package/three
Recommended lib: plain Three.js (three.module.js, MIT) + optionally the OrbitControls addon, loaded via one <script type="importmap"> + <script type="module">. This is exactly how the docs/community load it without a build tool. https://sbcode.net/threejs/importmap · https://registry.npmjs.org/three/latest
We have ~15 agents and a handful of walk edges at a time. A scene of ~15 low-poly avatars + room boxes + 2 lights is hundreds of triangles — trivial for any WebGL2 browser, including integrated GPUs. The real perf traps are post-processing (bloom/SSAO/soft shadows) and per-frame allocation, which we simply avoid. https://discourse.threejs.org/t/agent-visualization-three-js-r3f-openclaw-integration/90247
1. Lib & loading (no build): html <script type="importmap"> { "imports": { "three": "/vendor/three.module.js", "three/addons/": "/vendor/jsm/" } } </script> <script type="module"> import as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; </script> Vendor three.module.js (+ jsm/controls/OrbitControls.js) into canvas/vendor/. https://sbcode.net/threejs/importmap 2. Camera: OrthographicCamera at 45° (true isometric, echoes the current look); optional OrbitControls with enablePan/zoom, clamped. 3. Scene: a floor plane + BoxGeometry room slabs using the existing ROOMS[] x/y/w/h; desks as small boxes at DEFAULT_AGENTS[].x/y. 4. Avatars (one per agent): a CapsuleGeometry (or box) coloured by the agent's color, with an emissive intensity driven by status (busy=amber, done=green, idle=grey) — same semantics as today's .dot. 5. Labels: CanvasTexture sprite or CSS2DRenderer for name/role/label billboards. 6. Motion: reuse the 2D logic — when a walks edge fires, tween the avatar's position vector along a straight (or L-shaped) path desk→desk (requestAnimationFrame, linear/ease) and back; play the "→ recipient" bubble as a sprite. Same walks/PLAYED de-dup. 7. Data loop: keep the existing poll() on /api/state every ~1.5 s and the same status/busySince diffing — only the drawing* target changes from DOM left/top to mesh.position. 8. Frame pacing: render on requestAnimationFrame; pause rendering when the tab is hidden (document.hidden) and when the page's Pause button is on — mirrors the current Pause semantic and protects weak clients. 9. Fallback: if WebGL2 context creation fails, show a link to the existing 2D page. Keep 2D as the default; 3D is an opt-in tab.
Weak/mobile client: WebGL2 is near-universal today, but low-end phones/integrated GPUs can struggle with effects, not with our polygon count. Mitigate by avoiding post-FX, capping DPR (renderer.setPixelRatio(Math.min(devicePixelRatio,2))), pausing when hidden, and keeping the 2D page as fallback. The VM's GPU is irrelevant (client renders). https://discourse.threejs.org/t/agent-visualization-three-js-r3f-openclaw-integration/90247
CDN/offline dependency: a CDN import means "internet required at view time" and a third-party availability risk ("you can't rely on CDNs, they fail"). Vendoring the lib removes this. https://discourse.threejs.org/t/unpkg-cdn-link-sometimes-seem-to-fail/64109
Import-map-on-single-file friction: import maps + module scripts are well supported in current browsers but fiddly to hand-wire for many deps — another reason to keep deps to one lib + one addon, not a React tree. https://discourse.threejs.org/t/threejs-install-put-import-into-its-own-js-file/48016
Scope creep: tenacitOS bundles physics (Rapier), Monaco editor, Recharts, Monaco, etc. We need none of that; resist copying the app. https://raw.githubusercontent.com/carlosazaustre/tenacitOS/main/package.json
Confidence: high (~85%) that Option B is a few-days job with the data we already expose. Medium confidence on exact effort if we later add GLB avatar models (asset sourcing/rigging becomes the long pole) — which is why v1 uses primitives.
1. tenacitOS repo + README (stack, Office 3D feature, office route, agentsConfig, GLB avatars) — https://github.com/carlosazaustre/tenacitOS 2. tenacitOS package.json (three ^0.183.0, @react-three/fiber ^9.5.0, drei ^10.7.7, rapidier ^2.2.0, next 16.1.6, react 19.2.3) — https://raw.githubusercontent.com/carlosazaustre/tenacitOS/main/package.json 3. tenacitOS Office3D component listing — https://api.github.com/repos/carlosazaustre/tenacitOS/contents/src/components/Office3D 4. "3D AI Agent Office" (R3F + Drei + Zustand, WebGL isometric) — https://medium.com/@agustinafassina_92108/building-a-3d-ai-agent-office-100-local-0-api-costs-and-fully-autonomous-884cf4fcc37b 5. three.js forum: Agent visualization / R3F / OpenClaw thread — https://discourse.threejs.org/t/agent-visualization-three-js-r3f-openclaw-integration/90247 6. three.js npm metadata (v0.186.1, MIT, three.module.js) — https://registry.npmjs.org/three/latest 7. R3F npm metadata (v9.8.1, requires React) — https://registry.npmjs.org/@react-three/fiber/latest 8. Babylon.js core npm metadata — https://registry.npmjs.org/@babylonjs/core/latest 9. three.js import maps / CDN, no build step (sbcode.net) — https://sbcode.net/threejs/importmap 10. three.js forum: CDN reliability + three.module.js ≈1.2 MB + r163→jsDelivr — https://discourse.threejs.org/t/unpkg-cdn-link-sometimes-seem-to-fail/64109 11. discoverthreejs: import from CDN without a bundler — https://discoverthreejs.com/book/introduction/get-threejs 12. three.js forum: hand-wiring import maps across files is fiddly — https://discourse.threejs.org/t/threejs-install-put-import-into-its-own-js-file/48016 13. Our own code: /home/ubuntu/.openclaw/canvas/mission-control.html, /home/ubuntu/.local/bin/mc_viewer.py; host facts via nproc/free/lspci/node -v.