ai-agent-session-center 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +484 -429
  2. package/docs/3D/ADAPTATION_GUIDE.md +592 -0
  3. package/docs/3D/index.html +754 -0
  4. package/docs/AGENT_TEAM_TASKS.md +716 -0
  5. package/docs/CYBERDROME_V2_SPEC.md +531 -0
  6. package/docs/ERROR_185_ANALYSIS.md +263 -0
  7. package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
  8. package/docs/SESSION_DETAIL_FEATURES.md +98 -0
  9. package/docs/_3d_multimedia_features.md +1080 -0
  10. package/docs/_frontend_features.md +1057 -0
  11. package/docs/_server_features.md +1077 -0
  12. package/docs/session-duplication-fixes.md +271 -0
  13. package/docs/session-terminal-linkage.md +412 -0
  14. package/package.json +63 -5
  15. package/public/apple-touch-icon.svg +21 -0
  16. package/public/css/dashboard.css +0 -161
  17. package/public/css/detail-panel.css +25 -0
  18. package/public/css/layout.css +18 -1
  19. package/public/css/modals.css +0 -26
  20. package/public/css/settings.css +0 -150
  21. package/public/css/terminal.css +34 -0
  22. package/public/favicon.svg +18 -0
  23. package/public/index.html +6 -26
  24. package/public/js/alarmManager.js +0 -21
  25. package/public/js/app.js +21 -7
  26. package/public/js/detailPanel.js +63 -64
  27. package/public/js/historyPanel.js +61 -55
  28. package/public/js/quickActions.js +132 -48
  29. package/public/js/sessionCard.js +5 -20
  30. package/public/js/sessionControls.js +8 -0
  31. package/public/js/settingsManager.js +0 -142
  32. package/server/apiRouter.js +60 -15
  33. package/server/apiRouter.ts +774 -0
  34. package/server/approvalDetector.ts +94 -0
  35. package/server/authManager.ts +144 -0
  36. package/server/autoIdleManager.ts +110 -0
  37. package/server/config.ts +121 -0
  38. package/server/constants.ts +150 -0
  39. package/server/db.ts +475 -0
  40. package/server/hookInstaller.d.ts +3 -0
  41. package/server/hookProcessor.ts +108 -0
  42. package/server/hookRouter.ts +18 -0
  43. package/server/hookStats.ts +116 -0
  44. package/server/index.js +15 -1
  45. package/server/index.ts +230 -0
  46. package/server/logger.ts +75 -0
  47. package/server/mqReader.ts +349 -0
  48. package/server/portManager.ts +55 -0
  49. package/server/processMonitor.ts +239 -0
  50. package/server/serverConfig.ts +29 -0
  51. package/server/sessionMatcher.js +17 -6
  52. package/server/sessionMatcher.ts +403 -0
  53. package/server/sessionStore.js +109 -3
  54. package/server/sessionStore.ts +1145 -0
  55. package/server/sshManager.js +167 -24
  56. package/server/sshManager.ts +671 -0
  57. package/server/teamManager.ts +289 -0
  58. package/server/wsManager.ts +200 -0
@@ -0,0 +1,592 @@
1
+ # Cyberdrome 3D Robot → Agent Manager Adaptation Guide
2
+
3
+ Replace the 20 CSS-only 2D character models on session cards with interactive Three.js 3D robotic characters driven by real session state.
4
+
5
+ ---
6
+
7
+ ## Architecture Overview
8
+
9
+ ```
10
+ CURRENT (agent-manager) TARGET
11
+ ─────────────────────────────────────────────────────────────────
12
+ SessionCard SessionCard
13
+ └─ .robotViewport (empty div) └─ <Robot3DViewport>
14
+ ↓ styled by CSS ↓ Three.js canvas
15
+ CharacterModel (20 CSS divs) Robot3D (boxy mech)
16
+ data-status → CSS keyframes sessionStatus → 3D state machine
17
+ --robot-color → CSS var neonColor → emissive material
18
+ ```
19
+
20
+ ### What Gets Replaced
21
+
22
+ | Current | Replacement |
23
+ |---------|-------------|
24
+ | `src/components/character/CharacterModel.tsx` | `src/components/character/Robot3DModel.tsx` |
25
+ | `src/components/character/RobotViewport.tsx` | `src/components/character/Robot3DViewport.tsx` |
26
+ | `src/components/character/CharacterSelector.tsx` | Updated to show 3D previews |
27
+ | `src/styles/characters/*.css` (22 files, ~80KB) | Removed (Three.js handles visuals) |
28
+ | `src/styles/animations.css` (48KB) | Reduced (keep card glow/border effects only) |
29
+
30
+ ### What Stays Unchanged
31
+
32
+ - `SessionCard.tsx` — only the `.robotViewport` child changes
33
+ - `SessionCard.module.css` — card border/glow/status-badge CSS stays
34
+ - `sessionStore.ts`, `settingsStore.ts` — no schema changes
35
+ - `session.ts` types — `AnimationState` finally gets used
36
+ - Server/WebSocket/API — zero backend changes
37
+
38
+ ---
39
+
40
+ ## Technical Stack Delta
41
+
42
+ ```
43
+ NEW DEPENDENCIES
44
+ ────────────────
45
+ three ^0.160.0 # 3D engine
46
+ @react-three/fiber ^8.15.0 # React reconciler for Three.js
47
+ @react-three/drei ^9.92.0 # Helpers (OrbitControls, useGLTF, etc.)
48
+ ```
49
+
50
+ ```bash
51
+ cd /Users/kasonzhan/Documents/claude/agent-manager
52
+ npm install three @react-three/fiber @react-three/drei
53
+ npm install -D @types/three
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Session Status → 3D State Machine
59
+
60
+ The current CSS system drives animations via `data-status`. The 3D system maps the same `SessionStatus` values to robot behavior states:
61
+
62
+ ```typescript
63
+ // src/lib/robotStateMap.ts
64
+
65
+ import type { SessionStatus } from '@/types/session'
66
+
67
+ export type Robot3DState =
68
+ | 'idle' // gentle hover, slow blink, antenna pulse
69
+ | 'thinking' // head tilt, eye glow cycle, antenna fast pulse
70
+ | 'working' // seated at desk, typing animation, sweat particles
71
+ | 'waiting' // standing, looking around, slow bounce
72
+ | 'alert' // urgent bounce, visor flash yellow, floating "!"
73
+ | 'input' // gentle sway, visor flash purple, floating "?"
74
+ | 'offline' // powered down, dim materials, no glow
75
+ | 'connecting' // boot-up sequence, parts assembling
76
+
77
+ export function mapStatusTo3D(status: SessionStatus): Robot3DState {
78
+ const map: Record<SessionStatus, Robot3DState> = {
79
+ idle: 'idle',
80
+ prompting: 'thinking',
81
+ working: 'working',
82
+ waiting: 'waiting',
83
+ approval: 'alert',
84
+ input: 'input',
85
+ ended: 'offline',
86
+ connecting: 'connecting',
87
+ }
88
+ return map[status]
89
+ }
90
+ ```
91
+
92
+ ### Animation Details Per State
93
+
94
+ | Robot3DState | Body | Arms | Legs | Visor | Core | Special |
95
+ |---|---|---|---|---|---|---|
96
+ | `idle` | gentle Y bob (sin, 0.03 amp) | rest at sides | standing | neon color, slow pulse | slow pulse | antenna tip pulse |
97
+ | `thinking` | slight lean forward | one arm to chin | standing | brighter, faster pulse | fast pulse | head tilt L/R |
98
+ | `working` | seated (y=-0.12) | typing oscillation (±0.05) | bent forward (1.2 rad) | steady bright | rapid pulse | sweat particle emitter* |
99
+ | `waiting` | bounce (sin, 0.08 amp) | slight swing | standing | blue tint | blue pulse | look-around head rotation |
100
+ | `alert` | urgent bounce (fast) | raised | standing | **yellow**, flash | **yellow**, urgent | floating "!" mesh, shake |
101
+ | `input` | gentle sway | one raised (question) | standing | **purple**, gentle | **purple** | floating "?" mesh |
102
+ | `offline` | static, y=-0.05 | limp at sides | standing | **grey**, no glow | **off** | dim all emissive to 0 |
103
+ | `connecting` | scale 0→1 over 1s | animate into position | animate into position | flicker on | boot pulse | parts "assemble" |
104
+
105
+ *Sweat particle = small sphere emitter near head, matches the CSS sweat-drop effect
106
+
107
+ ---
108
+
109
+ ## File-by-File Implementation Plan
110
+
111
+ ### 1. `src/lib/robot3DGeometry.ts` — Shared Geometry + Materials
112
+
113
+ Extract from `index.html` lines 430-460. All geometries created once, shared across all robot instances.
114
+
115
+ ```typescript
116
+ // src/lib/robot3DGeometry.ts
117
+ import * as THREE from 'three'
118
+
119
+ // Shared geometries (create once, reuse for every robot)
120
+ export const robotGeo = {
121
+ head: new THREE.BoxGeometry(0.28, 0.24, 0.26),
122
+ visor: new THREE.BoxGeometry(0.24, 0.065, 0.02),
123
+ antenna: new THREE.CylinderGeometry(0.007, 0.007, 0.14, 4),
124
+ aTip: new THREE.SphereGeometry(0.02, 6, 6),
125
+ torso: new THREE.BoxGeometry(0.32, 0.38, 0.2),
126
+ core: new THREE.SphereGeometry(0.032, 8, 8),
127
+ joint: new THREE.SphereGeometry(0.035, 8, 8),
128
+ arm: new THREE.BoxGeometry(0.08, 0.26, 0.08),
129
+ leg: new THREE.BoxGeometry(0.09, 0.28, 0.09),
130
+ foot: new THREE.BoxGeometry(0.1, 0.045, 0.12),
131
+ }
132
+
133
+ export const robotEdgeGeo = {
134
+ head: new THREE.EdgesGeometry(robotGeo.head),
135
+ torso: new THREE.EdgesGeometry(robotGeo.torso),
136
+ arm: new THREE.EdgesGeometry(robotGeo.arm),
137
+ leg: new THREE.EdgesGeometry(robotGeo.leg),
138
+ }
139
+
140
+ // Shared metallic body materials (same for all robots)
141
+ export const metalMat = new THREE.MeshStandardMaterial({
142
+ color: '#2a2a3e', roughness: 0.3, metalness: 0.85,
143
+ })
144
+ export const darkMat = new THREE.MeshStandardMaterial({
145
+ color: '#1c1c2c', roughness: 0.4, metalness: 0.7,
146
+ })
147
+
148
+ // Per-color neon materials (pooled by the 8-color palette)
149
+ export function createNeonMat(hex: string) {
150
+ const c = new THREE.Color(hex)
151
+ return new THREE.MeshStandardMaterial({
152
+ color: c, emissive: c, emissiveIntensity: 2,
153
+ roughness: 0.2, metalness: 0.3,
154
+ })
155
+ }
156
+
157
+ export function createEdgeMat(hex: string) {
158
+ return new THREE.LineBasicMaterial({
159
+ color: hex, transparent: true, opacity: 0.3,
160
+ })
161
+ }
162
+ ```
163
+
164
+ ### 2. `src/components/character/Robot3DModel.tsx` — The 3D Robot Component
165
+
166
+ This is a `@react-three/fiber` component that builds the robot mesh hierarchy and runs the state-driven animation loop.
167
+
168
+ ```typescript
169
+ // src/components/character/Robot3DModel.tsx
170
+ import { useRef, useMemo } from 'react'
171
+ import { useFrame } from '@react-three/fiber'
172
+ import * as THREE from 'three'
173
+ import { robotGeo, robotEdgeGeo, metalMat, darkMat, createNeonMat, createEdgeMat } from '@/lib/robot3DGeometry'
174
+ import type { Robot3DState } from '@/lib/robotStateMap'
175
+
176
+ interface Robot3DModelProps {
177
+ neonColor: string // e.g. '#00f0ff'
178
+ state: Robot3DState // mapped from SessionStatus
179
+ scale?: number // default 1
180
+ }
181
+
182
+ export function Robot3DModel({ neonColor, state, scale = 1 }: Robot3DModelProps) {
183
+ const groupRef = useRef<THREE.Group>(null)
184
+ const armPivots = useRef<THREE.Group[]>([])
185
+ const legPivots = useRef<THREE.Group[]>([])
186
+ const bodyRef = useRef<THREE.Mesh>(null)
187
+ const bodyEdgeRef = useRef<THREE.LineSegments>(null)
188
+ const coreRef = useRef<THREE.Mesh>(null)
189
+ const aTipRef = useRef<THREE.Mesh>(null)
190
+ const glowRef = useRef<THREE.Mesh>(null)
191
+ const phase = useRef(Math.random() * Math.PI * 2)
192
+
193
+ const { neonMat, edgeMat } = useMemo(() => ({
194
+ neonMat: createNeonMat(neonColor),
195
+ edgeMat: createEdgeMat(neonColor),
196
+ }), [neonColor])
197
+
198
+ useFrame((_, delta) => {
199
+ if (!groupRef.current) return
200
+ const t = performance.now() / 1000
201
+ const p = phase.current
202
+
203
+ // Antenna + core pulse (always active)
204
+ if (aTipRef.current) {
205
+ aTipRef.current.scale.setScalar(
206
+ 0.8 + ((Math.sin(t * 6 + p) + 1) * 0.5) * 0.4
207
+ )
208
+ }
209
+ if (coreRef.current) {
210
+ coreRef.current.scale.setScalar(
211
+ 0.9 + Math.sin(t * 3 + p) * 0.12
212
+ )
213
+ }
214
+
215
+ const [lp0, lp1] = legPivots.current
216
+ const [ap0, ap1] = armPivots.current
217
+
218
+ switch (state) {
219
+ case 'idle': {
220
+ groupRef.current.position.y = Math.sin(t * 2 + p) * 0.03
221
+ if (ap0) ap0.rotation.x *= 0.9
222
+ if (ap1) ap1.rotation.x *= 0.9
223
+ if (lp0) lp0.rotation.x *= 0.9
224
+ if (lp1) lp1.rotation.x *= 0.9
225
+ break
226
+ }
227
+ case 'thinking': {
228
+ groupRef.current.position.y = Math.sin(t * 2 + p) * 0.02
229
+ if (bodyRef.current) {
230
+ bodyRef.current.rotation.z = Math.sin(t * 0.8 + p) * 0.04
231
+ }
232
+ // One arm up to "chin"
233
+ if (ap0) ap0.rotation.x = -0.7 + Math.sin(t * 1.5 + p) * 0.05
234
+ if (ap1) ap1.rotation.x *= 0.9
235
+ break
236
+ }
237
+ case 'working': {
238
+ // Seated pose
239
+ groupRef.current.position.y = -0.12
240
+ if (lp0) lp0.rotation.x = 1.2
241
+ if (lp1) lp1.rotation.x = 1.2
242
+ const typing = Math.sin(t * 10 + p) * 0.05
243
+ if (ap0) ap0.rotation.x = -0.5 + typing
244
+ if (ap1) ap1.rotation.x = -0.5 - typing
245
+ if (bodyRef.current) {
246
+ bodyRef.current.rotation.z = Math.sin(t * 0.7 + p) * 0.012
247
+ }
248
+ if (glowRef.current) glowRef.current.position.y = 0.13
249
+ break
250
+ }
251
+ case 'waiting': {
252
+ groupRef.current.position.y = Math.abs(Math.sin(t * 3 + p)) * 0.08
253
+ if (bodyRef.current) {
254
+ bodyRef.current.rotation.y = Math.sin(t * 0.5 + p) * 0.3
255
+ }
256
+ break
257
+ }
258
+ case 'alert': {
259
+ groupRef.current.position.y = Math.abs(Math.sin(t * 6 + p)) * 0.1
260
+ groupRef.current.position.x = Math.sin(t * 15 + p) * 0.02
261
+ break
262
+ }
263
+ case 'input': {
264
+ groupRef.current.position.y = Math.sin(t * 1.5 + p) * 0.04
265
+ groupRef.current.rotation.z = Math.sin(t * 1 + p) * 0.03
266
+ if (ap1) ap1.rotation.x = -0.8 + Math.sin(t * 1.5) * 0.05
267
+ break
268
+ }
269
+ case 'offline': {
270
+ groupRef.current.position.y = -0.05
271
+ break
272
+ }
273
+ case 'connecting': {
274
+ const boot = Math.min(1, t % 2)
275
+ groupRef.current.scale.setScalar(scale * boot)
276
+ break
277
+ }
278
+ }
279
+
280
+ // Sync body edge rotation
281
+ if (bodyEdgeRef.current && bodyRef.current) {
282
+ bodyEdgeRef.current.rotation.copy(bodyRef.current.rotation)
283
+ }
284
+
285
+ // Reset glow for non-sitting states
286
+ if (state !== 'working' && glowRef.current) {
287
+ glowRef.current.position.y = 0.01
288
+ }
289
+ })
290
+
291
+ return (
292
+ <group ref={groupRef} scale={scale}>
293
+ {/* HEAD */}
294
+ <mesh geometry={robotGeo.head} material={metalMat} position={[0, 1.32, 0]} castShadow />
295
+ <lineSegments geometry={robotEdgeGeo.head} material={edgeMat} position={[0, 1.32, 0]} />
296
+ <mesh geometry={robotGeo.visor} material={neonMat} position={[0, 1.32, 0.13]} />
297
+ <mesh geometry={robotGeo.antenna} material={darkMat} position={[0.05, 1.52, 0]} />
298
+ <mesh ref={aTipRef} geometry={robotGeo.aTip} material={neonMat} position={[0.05, 1.6, 0]} />
299
+
300
+ {/* TORSO */}
301
+ <mesh ref={bodyRef} geometry={robotGeo.torso} material={metalMat} position={[0, 0.87, 0]} castShadow />
302
+ <lineSegments ref={bodyEdgeRef} geometry={robotEdgeGeo.torso} material={edgeMat} position={[0, 0.87, 0]} />
303
+ <mesh ref={coreRef} geometry={robotGeo.core} material={neonMat} position={[0, 0.91, 0.105]} />
304
+
305
+ {/* SHOULDERS */}
306
+ {[-1, 1].map(s => (
307
+ <mesh key={`sh${s}`} geometry={robotGeo.joint} material={neonMat} position={[s * 0.21, 1.07, 0]} />
308
+ ))}
309
+
310
+ {/* ARMS */}
311
+ {[-1, 1].map((s, i) => (
312
+ <group key={`arm${s}`} ref={el => { if (el) armPivots.current[i] = el }} position={[s * 0.21, 1.07, 0]}>
313
+ <mesh geometry={robotGeo.arm} material={darkMat} position={[0, -0.18, 0]} castShadow />
314
+ <lineSegments geometry={robotEdgeGeo.arm} material={edgeMat} position={[0, -0.18, 0]} />
315
+ </group>
316
+ ))}
317
+
318
+ {/* HIPS */}
319
+ {[-1, 1].map(s => (
320
+ <mesh key={`hp${s}`} geometry={robotGeo.joint} material={neonMat} position={[s * 0.09, 0.54, 0]} scale={0.9} />
321
+ ))}
322
+
323
+ {/* LEGS */}
324
+ {[-1, 1].map((s, i) => (
325
+ <group key={`leg${s}`} ref={el => { if (el) legPivots.current[i] = el }} position={[s * 0.09, 0.54, 0]}>
326
+ <mesh geometry={robotGeo.leg} material={darkMat} position={[0, -0.19, 0]} castShadow />
327
+ <lineSegments geometry={robotEdgeGeo.leg} material={edgeMat} position={[0, -0.19, 0]} />
328
+ <mesh geometry={robotGeo.foot} material={metalMat} position={[0, -0.36, 0.012]} castShadow />
329
+ </group>
330
+ ))}
331
+
332
+ {/* GROUND GLOW (placeholder — needs CanvasTexture) */}
333
+ {/* <mesh ref={glowRef} rotation={[-Math.PI/2,0,0]} position={[0,0.01,0]}> ... </mesh> */}
334
+ </group>
335
+ )
336
+ }
337
+ ```
338
+
339
+ ### 3. `src/components/character/Robot3DViewport.tsx` — Canvas Container
340
+
341
+ Replaces `RobotViewport.tsx`. Wraps the 3D robot in a `<Canvas>` with lighting.
342
+
343
+ ```typescript
344
+ // src/components/character/Robot3DViewport.tsx
345
+ import { useMemo } from 'react'
346
+ import { Canvas } from '@react-three/fiber'
347
+ import { Robot3DModel } from './Robot3DModel'
348
+ import { mapStatusTo3D } from '@/lib/robotStateMap'
349
+ import { useSettingsStore } from '@/stores/settingsStore'
350
+ import type { SessionStatus } from '@/types/session'
351
+
352
+ const COLOR_PALETTE = [
353
+ '#00f0ff', '#ff00aa', '#a855f7', '#00ff88',
354
+ '#ff4444', '#ffaa00', '#00aaff', '#ff66ff',
355
+ ]
356
+
357
+ interface Robot3DViewportProps {
358
+ sessionId: string
359
+ status: SessionStatus
360
+ accentColor?: string
361
+ }
362
+
363
+ let globalColorIdx = 0
364
+
365
+ export function Robot3DViewport({ sessionId, status, accentColor }: Robot3DViewportProps) {
366
+ const color = useMemo(
367
+ () => accentColor || COLOR_PALETTE[globalColorIdx++ % COLOR_PALETTE.length],
368
+ [accentColor]
369
+ )
370
+ const state3D = mapStatusTo3D(status)
371
+
372
+ return (
373
+ <Canvas
374
+ camera={{ position: [0, 1.2, 2.8], fov: 40 }}
375
+ gl={{ antialias: true, alpha: true }}
376
+ style={{ background: 'transparent' }}
377
+ dpr={[1, 1.5]}
378
+ >
379
+ <ambientLight intensity={0.8} color="#2a2040" />
380
+ <directionalLight position={[3, 5, 3]} intensity={1.5} color="#c8b8e0" />
381
+ <pointLight position={[-2, 2, -1]} intensity={0.6} color={color} />
382
+ <Robot3DModel neonColor={color} state={state3D} scale={0.85} />
383
+ </Canvas>
384
+ )
385
+ }
386
+ ```
387
+
388
+ ### 4. Integration into SessionCard
389
+
390
+ The existing `SessionCard.tsx` has a placeholder:
391
+
392
+ ```tsx
393
+ {/* Robot viewport placeholder */}
394
+ <div className={styles.robotViewport} />
395
+ ```
396
+
397
+ Replace with:
398
+
399
+ ```tsx
400
+ <div className={styles.robotViewport}>
401
+ <Robot3DViewport
402
+ sessionId={session.sessionId}
403
+ status={session.status}
404
+ accentColor={session.accentColor}
405
+ />
406
+ </div>
407
+ ```
408
+
409
+ Ensure the `.robotViewport` CSS module class has:
410
+
411
+ ```css
412
+ .robotViewport {
413
+ width: 100%;
414
+ height: 120px; /* adjust to card layout */
415
+ overflow: hidden;
416
+ border-radius: 8px;
417
+ }
418
+ .robotViewport canvas {
419
+ width: 100% !important;
420
+ height: 100% !important;
421
+ }
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Performance Considerations
427
+
428
+ ### Problem: Many Canvases
429
+
430
+ Each `SessionCard` would mount its own `<Canvas>`, each creating a separate WebGL context. Browsers limit contexts to ~8-16; beyond that, earlier ones are lost.
431
+
432
+ ### Solution: Shared Canvas with Offscreen Viewports
433
+
434
+ Use a **single shared Canvas** that renders all robots, with each card showing a CSS-clipped region. `@react-three/drei` provides `<View>` for this:
435
+
436
+ ```typescript
437
+ // src/components/character/Robot3DStage.tsx
438
+ import { Canvas } from '@react-three/fiber'
439
+ import { View } from '@react-three/drei'
440
+
441
+ // Mount ONCE at the app root (e.g., in App.tsx or LiveView.tsx)
442
+ export function Robot3DStage({ children }: { children: React.ReactNode }) {
443
+ return (
444
+ <>
445
+ {children}
446
+ <Canvas
447
+ style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 0 }}
448
+ gl={{ antialias: true, alpha: true }}
449
+ dpr={[1, 1.5]}
450
+ eventSource={document.body}
451
+ >
452
+ <View.Port />
453
+ </Canvas>
454
+ </>
455
+ )
456
+ }
457
+
458
+ // Each card uses <View> to claim a region of the shared canvas:
459
+ // src/components/character/Robot3DViewport.tsx (updated)
460
+ import { View } from '@react-three/drei'
461
+
462
+ export function Robot3DViewport({ status, accentColor }: Props) {
463
+ const ref = useRef<HTMLDivElement>(null)
464
+ const state3D = mapStatusTo3D(status)
465
+ const color = accentColor || '#00f0ff'
466
+
467
+ return (
468
+ <div ref={ref} style={{ width: '100%', height: '120px' }}>
469
+ <View track={ref}>
470
+ <ambientLight intensity={0.8} color="#2a2040" />
471
+ <directionalLight position={[3, 5, 3]} intensity={1.5} />
472
+ <pointLight position={[-2, 2, -1]} intensity={0.6} color={color} />
473
+ <Robot3DModel neonColor={color} state={state3D} scale={0.85} />
474
+ </View>
475
+ </div>
476
+ )
477
+ }
478
+ ```
479
+
480
+ This approach uses **1 WebGL context** for all robots. Critical for dashboards showing 10-50+ sessions.
481
+
482
+ ### Other Performance Tips
483
+
484
+ | Concern | Solution |
485
+ |---|---|
486
+ | 50+ robots on screen | Use `<Instances>` from drei for shared geometry instancing |
487
+ | Material per color | Pool materials by palette index (8 materials, not N) |
488
+ | Offscreen robots | Pause `useFrame` when card is not in viewport (`IntersectionObserver`) |
489
+ | Mobile/low-end | Fall back to CSS characters via `settingsStore.use3D` toggle |
490
+ | Shadow maps | Disable `castShadow` in card viewport (only needed for full scene) |
491
+
492
+ ---
493
+
494
+ ## Migration Checklist
495
+
496
+ ```
497
+ Phase 1: Foundation
498
+ ─────────────────────────────────────────
499
+ [ ] npm install three @react-three/fiber @react-three/drei @types/three
500
+ [ ] Create src/lib/robot3DGeometry.ts (shared geo + materials)
501
+ [ ] Create src/lib/robotStateMap.ts (SessionStatus → Robot3DState)
502
+ [ ] Create src/components/character/Robot3DModel.tsx
503
+ [ ] Create src/components/character/Robot3DViewport.tsx
504
+ [ ] Create src/components/character/Robot3DStage.tsx (shared canvas)
505
+
506
+ Phase 2: Integration
507
+ ─────────────────────────────────────────
508
+ [ ] Wrap App.tsx (or LiveView.tsx) with <Robot3DStage>
509
+ [ ] Replace .robotViewport placeholder in SessionCard with <Robot3DViewport>
510
+ [ ] Wire session.status and session.accentColor props through
511
+ [ ] Test with 1 session, then 10, then 30+
512
+
513
+ Phase 3: Polish
514
+ ─────────────────────────────────────────
515
+ [ ] Add state transitions (smooth lerp between poses, not instant snap)
516
+ [ ] Add floating "!" / "?" meshes for approval/input states
517
+ [ ] Add sweat particle emitter for working state
518
+ [ ] Add boot-up animation for connecting state
519
+ [ ] Handle emote system (Wave/ThumbsUp/Jump/Yes → one-shot 3D anim)
520
+
521
+ Phase 4: Cleanup
522
+ ─────────────────────────────────────────
523
+ [ ] Add settingsStore.use3D toggle (bool) for CSS fallback
524
+ [ ] Update CharacterSelector to show 3D preview (single shared canvas)
525
+ [ ] Remove src/styles/characters/*.css (22 files) if CSS chars fully deprecated
526
+ [ ] Slim down src/styles/animations.css (keep card effects, remove character anims)
527
+ [ ] Update Vitest + Playwright tests
528
+
529
+ Phase 5: Optional Enhancements
530
+ ─────────────────────────────────────────
531
+ [ ] Per-session robot customization (color picker already exists via accentColor)
532
+ [ ] Use AnimationState field to drive walk/run/dance animations
533
+ [ ] Add desk/monitor mesh when robot is in "working" state
534
+ [ ] Add OrbitControls on detail panel (enlarged robot view when card is selected)
535
+ ```
536
+
537
+ ---
538
+
539
+ ## Key Mapping Reference
540
+
541
+ ### Color Palette (reuse existing)
542
+
543
+ ```
544
+ Current RobotViewport → Robot3DViewport
545
+ COLOR_PALETTE[0] '#00e5ff' → Neon visor + joints + edges
546
+ COLOR_PALETTE[1] '#ff9100' → Neon visor + joints + edges
547
+ ... → ...
548
+ ```
549
+
550
+ The existing auto-assignment logic in `RobotViewport` (cycling `globalColorIndex`, persisting via `PUT /api/sessions/{id}/accent-color`) works identically — just pass the color as `neonColor` to the 3D model.
551
+
552
+ ### AnimationState → Enhancement Layer
553
+
554
+ The `AnimationState` enum (`Idle/Walking/Running/Waiting/Death/Dance`) stored on sessions is currently unused. With 3D robots, it can drive additional behavior:
555
+
556
+ ```typescript
557
+ // Optional: enhance the 3D state with AnimationState
558
+ if (session.animationState === 'Dance') {
559
+ // Override normal state with celebration animation
560
+ }
561
+ if (session.animationState === 'Death') {
562
+ // Play shutdown/collapse sequence instead of normal 'offline'
563
+ }
564
+ ```
565
+
566
+ ### Settings Integration
567
+
568
+ ```
569
+ settingsStore.animationIntensity → Scale all useFrame amplitudes
570
+ settingsStore.animationSpeed → Multiply time factor in useFrame
571
+ settingsStore.characterModel → Ignored (single 3D robot model)
572
+ OR use as variant selector if you add multiple 3D models
573
+ ```
574
+
575
+ ---
576
+
577
+ ## File Structure After Adaptation
578
+
579
+ ```
580
+ src/components/character/
581
+ ├── Robot3DModel.tsx # NEW — Three.js robot mesh + animation
582
+ ├── Robot3DViewport.tsx # NEW — Per-card viewport (uses drei View)
583
+ ├── Robot3DStage.tsx # NEW — Shared Canvas at app root
584
+ ├── CharacterModel.tsx # KEEP — CSS fallback (if use3D is false)
585
+ ├── RobotViewport.tsx # KEEP — CSS fallback wrapper
586
+ ├── CharacterSelector.tsx # UPDATE — add 3D preview option
587
+
588
+ src/lib/
589
+ ├── robot3DGeometry.ts # NEW — shared Three.js geometries + materials
590
+ ├── robotStateMap.ts # NEW — SessionStatus → Robot3DState mapper
591
+ ├── ...existing files...
592
+ ```