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.
- package/README.md +484 -429
- package/docs/3D/ADAPTATION_GUIDE.md +592 -0
- package/docs/3D/index.html +754 -0
- package/docs/AGENT_TEAM_TASKS.md +716 -0
- package/docs/CYBERDROME_V2_SPEC.md +531 -0
- package/docs/ERROR_185_ANALYSIS.md +263 -0
- package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
- package/docs/SESSION_DETAIL_FEATURES.md +98 -0
- package/docs/_3d_multimedia_features.md +1080 -0
- package/docs/_frontend_features.md +1057 -0
- package/docs/_server_features.md +1077 -0
- package/docs/session-duplication-fixes.md +271 -0
- package/docs/session-terminal-linkage.md +412 -0
- package/package.json +63 -5
- package/public/apple-touch-icon.svg +21 -0
- package/public/css/dashboard.css +0 -161
- package/public/css/detail-panel.css +25 -0
- package/public/css/layout.css +18 -1
- package/public/css/modals.css +0 -26
- package/public/css/settings.css +0 -150
- package/public/css/terminal.css +34 -0
- package/public/favicon.svg +18 -0
- package/public/index.html +6 -26
- package/public/js/alarmManager.js +0 -21
- package/public/js/app.js +21 -7
- package/public/js/detailPanel.js +63 -64
- package/public/js/historyPanel.js +61 -55
- package/public/js/quickActions.js +132 -48
- package/public/js/sessionCard.js +5 -20
- package/public/js/sessionControls.js +8 -0
- package/public/js/settingsManager.js +0 -142
- package/server/apiRouter.js +60 -15
- package/server/apiRouter.ts +774 -0
- package/server/approvalDetector.ts +94 -0
- package/server/authManager.ts +144 -0
- package/server/autoIdleManager.ts +110 -0
- package/server/config.ts +121 -0
- package/server/constants.ts +150 -0
- package/server/db.ts +475 -0
- package/server/hookInstaller.d.ts +3 -0
- package/server/hookProcessor.ts +108 -0
- package/server/hookRouter.ts +18 -0
- package/server/hookStats.ts +116 -0
- package/server/index.js +15 -1
- package/server/index.ts +230 -0
- package/server/logger.ts +75 -0
- package/server/mqReader.ts +349 -0
- package/server/portManager.ts +55 -0
- package/server/processMonitor.ts +239 -0
- package/server/serverConfig.ts +29 -0
- package/server/sessionMatcher.js +17 -6
- package/server/sessionMatcher.ts +403 -0
- package/server/sessionStore.js +109 -3
- package/server/sessionStore.ts +1145 -0
- package/server/sshManager.js +167 -24
- package/server/sshManager.ts +671 -0
- package/server/teamManager.ts +289 -0
- package/server/wsManager.ts +200 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
# Agent Team Tasks — Scene Improvements Round 2
|
|
2
|
+
|
|
3
|
+
## Task 1: Remove Ended Sessions from the 3D Map
|
|
4
|
+
|
|
5
|
+
### Problem
|
|
6
|
+
|
|
7
|
+
When a session's status becomes `ended`, the robot stays on the map in an "offline" state — slumped over with dimming visor and core. The user wants ended sessions to simply disappear from the 3D scene entirely. The robot body glow during working/prompting is sufficient visual feedback; dead robots cluttering the map are unwanted.
|
|
8
|
+
|
|
9
|
+
### Root Cause
|
|
10
|
+
|
|
11
|
+
In `src/components/3d/CyberdromeScene.tsx:84-101`, `SceneContent` iterates over ALL sessions from the store including ended ones:
|
|
12
|
+
```tsx
|
|
13
|
+
const sessionArray = useMemo(() => [...sessions.values()], [sessions]);
|
|
14
|
+
// ↑ includes ended sessions → renders offline robots
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In `src/lib/robotStateMap.ts:31`, `ended` maps to `offline` robot state which renders a slumped, dimming robot with death animation. There's no filtering anywhere.
|
|
18
|
+
|
|
19
|
+
### Fix Required
|
|
20
|
+
|
|
21
|
+
1. **Filter out ended sessions** in `CyberdromeScene.tsx` `SceneContent`:
|
|
22
|
+
```tsx
|
|
23
|
+
const sessionArray = useMemo(
|
|
24
|
+
() => [...sessions.values()].filter(s => s.status !== 'ended'),
|
|
25
|
+
[sessions],
|
|
26
|
+
);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. **Clean up workstation occupancy** when a session ends. In `SessionRobot.tsx`, the existing unmount cleanup (line 287-294) already releases the desk on unmount. Since filtering removes ended robots, React will unmount those `SessionRobot` components and the cleanup effect fires automatically. **Verify this works** — if the robot was seated at a desk, the desk should become available after the session ends.
|
|
30
|
+
|
|
31
|
+
3. **RobotListSidebar** (`src/components/3d/RobotListSidebar.tsx`): Also filter out ended sessions from the sidebar list:
|
|
32
|
+
```tsx
|
|
33
|
+
const sessionArray = useMemo(() => {
|
|
34
|
+
const arr = [...sessions.values()].filter(s => s.status !== 'ended');
|
|
35
|
+
// ... sort logic
|
|
36
|
+
}, [sessions]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. **SceneOverlay** (`src/components/3d/SceneOverlay.tsx`): The status breakdown at top-left still shows ended counts. This is fine to keep (informational), but if desired, can also be filtered.
|
|
40
|
+
|
|
41
|
+
5. **robotPositionStore cleanup**: When a session ends, the position should be cleaned up. The existing `useEffect` cleanup on unmount (line 418-422) already handles this since the component will unmount.
|
|
42
|
+
|
|
43
|
+
### Files to Modify
|
|
44
|
+
|
|
45
|
+
| File | Change |
|
|
46
|
+
|------|--------|
|
|
47
|
+
| `src/components/3d/CyberdromeScene.tsx` | Filter `sessionArray` to exclude `ended` sessions |
|
|
48
|
+
| `src/components/3d/RobotListSidebar.tsx` | Filter `sessionArray` to exclude `ended` sessions |
|
|
49
|
+
|
|
50
|
+
### Verification
|
|
51
|
+
|
|
52
|
+
1. `npx tsc --noEmit` — type check
|
|
53
|
+
2. `npx vitest run` — tests pass
|
|
54
|
+
3. Visual: when a session ends, its robot disappears from the 3D scene within 1 frame
|
|
55
|
+
4. Visual: ended sessions no longer appear in the right sidebar
|
|
56
|
+
5. Verify the desk the ended robot occupied becomes available for other robots
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Task 2: Door-Aware Pathfinding for Robots
|
|
61
|
+
|
|
62
|
+
### Problem
|
|
63
|
+
|
|
64
|
+
Robots currently navigate by walking in a straight line toward their target, bouncing off walls via `collidesAnyWall()`. When a robot inside a room wants to reach a destination outside (or vice versa), it gets stuck against walls because it doesn't know where the door is. It randomly picks a new wander direction when blocked, which is inefficient and unrealistic.
|
|
65
|
+
|
|
66
|
+
### Current Navigation Logic (`SessionRobot.tsx:296-368`)
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
1. Compute direction to target (dx, dz)
|
|
70
|
+
2. Rotate toward target
|
|
71
|
+
3. Step forward
|
|
72
|
+
4. If wall collision → try X-only, then Z-only, then pick new random target
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This brute-force approach means robots frequently get stuck against walls, especially when their target is on the other side of a wall.
|
|
76
|
+
|
|
77
|
+
### Room Door Positions
|
|
78
|
+
|
|
79
|
+
Each room has exactly one door on the **south wall** (z = maxZ), centered at `(cx, 0, maxZ)` with `DOOR_GAP = 4` width. The door center is `(cx, 0, bounds.maxZ)` for each room.
|
|
80
|
+
|
|
81
|
+
### Fix Required — Waypoint-Based Door Navigation
|
|
82
|
+
|
|
83
|
+
Instead of walking in a straight line, robots should plan a path through doors when their target is in a different zone.
|
|
84
|
+
|
|
85
|
+
#### A. Add door waypoint computation (`src/lib/cyberdromeScene.ts`)
|
|
86
|
+
|
|
87
|
+
Add a function to compute door waypoints:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export interface DoorWaypoint {
|
|
91
|
+
roomIndex: number;
|
|
92
|
+
// Outside the door (south side, 1 unit past the wall)
|
|
93
|
+
outside: THREE.Vector3; // (cx, 0, bounds.maxZ + 1.0)
|
|
94
|
+
// Inside the door (north side, 1 unit inside the wall)
|
|
95
|
+
inside: THREE.Vector3; // (cx, 0, bounds.maxZ - 1.0)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildDoorWaypoints(rooms: RoomConfig[]): DoorWaypoint[] {
|
|
99
|
+
return rooms.map(room => {
|
|
100
|
+
const [cx] = room.center;
|
|
101
|
+
const maxZ = room.bounds.maxZ;
|
|
102
|
+
return {
|
|
103
|
+
roomIndex: room.index,
|
|
104
|
+
outside: new THREE.Vector3(cx, 0, maxZ + 1.0),
|
|
105
|
+
inside: new THREE.Vector3(cx, 0, maxZ - 1.0),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### B. Add pathfinding helper (`src/lib/cyberdromeScene.ts`)
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
/**
|
|
115
|
+
* Compute a waypoint path from current position to target.
|
|
116
|
+
* Returns an array of waypoints the robot should visit in order.
|
|
117
|
+
* - If same zone or corridor→corridor: direct path (empty waypoints)
|
|
118
|
+
* - If inside room → outside: [door.inside, door.outside, target]
|
|
119
|
+
* - If outside → inside room: [door.outside, door.inside, target]
|
|
120
|
+
* - If room A → room B: [doorA.inside, doorA.outside, doorB.outside, doorB.inside, target]
|
|
121
|
+
*/
|
|
122
|
+
export function computePathWaypoints(
|
|
123
|
+
fromX: number,
|
|
124
|
+
fromZ: number,
|
|
125
|
+
target: THREE.Vector3,
|
|
126
|
+
fromZone: number, // getZone() result for current position
|
|
127
|
+
targetZone: number, // zone the target is in (-1 for corridor, -2 coffee, -3 gym, >=0 room)
|
|
128
|
+
doors: DoorWaypoint[],
|
|
129
|
+
): THREE.Vector3[]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The logic:
|
|
133
|
+
1. If `fromZone === targetZone` → return `[target]` (direct path)
|
|
134
|
+
2. If `fromZone >= 0` (inside a room) and `targetZone !== fromZone`:
|
|
135
|
+
- Get door for `fromZone` → add `door.inside`, `door.outside`
|
|
136
|
+
- If `targetZone >= 0` → also add destination room's `door.outside`, `door.inside`
|
|
137
|
+
- Add `target`
|
|
138
|
+
3. If `fromZone < 0` (corridor/casual) and `targetZone >= 0`:
|
|
139
|
+
- Get door for `targetZone` → add `door.outside`, `door.inside`, `target`
|
|
140
|
+
4. If both `< 0` → return `[target]`
|
|
141
|
+
|
|
142
|
+
#### C. Update navigation in `SessionRobot.tsx`
|
|
143
|
+
|
|
144
|
+
Add a waypoint queue to the `NavState`:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
interface NavState {
|
|
148
|
+
// ... existing fields ...
|
|
149
|
+
waypoints: THREE.Vector3[]; // NEW: queue of intermediate waypoints
|
|
150
|
+
waypointIdx: number; // NEW: current waypoint index
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Navigation update logic change:
|
|
155
|
+
- When setting `nav.target` for NAV_GOTO or NAV_WALK to a different zone, call `computePathWaypoints()` and store the result in `nav.waypoints`
|
|
156
|
+
- In `useFrame`, instead of walking directly to `nav.target`:
|
|
157
|
+
1. Walk to `nav.waypoints[nav.waypointIdx]`
|
|
158
|
+
2. When within 0.5 of current waypoint, advance to next: `nav.waypointIdx++`
|
|
159
|
+
3. When all waypoints consumed, proceed to the original arrival logic (seat at desk, etc.)
|
|
160
|
+
|
|
161
|
+
#### D. Pass doors to SessionRobot
|
|
162
|
+
|
|
163
|
+
In `CyberdromeScene.tsx`, compute doors alongside workstations and pass to `SessionRobot`:
|
|
164
|
+
```tsx
|
|
165
|
+
const doorWaypoints = useMemo(() => buildDoorWaypoints(roomConfigs), [roomConfigs]);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Add `doors: DoorWaypoint[]` to `SessionRobotProps`.
|
|
169
|
+
|
|
170
|
+
### Files to Modify
|
|
171
|
+
|
|
172
|
+
| File | Change |
|
|
173
|
+
|------|--------|
|
|
174
|
+
| `src/lib/cyberdromeScene.ts` | Add `DoorWaypoint` interface, `buildDoorWaypoints()`, `computePathWaypoints()` |
|
|
175
|
+
| `src/components/3d/SessionRobot.tsx` | Add `waypoints`/`waypointIdx` to `NavState`, update navigation in `useFrame` to follow waypoints, call `computePathWaypoints()` when setting targets |
|
|
176
|
+
| `src/components/3d/CyberdromeScene.tsx` | Compute `doorWaypoints`, pass as prop to `SessionRobot`, update `SceneContent` props |
|
|
177
|
+
|
|
178
|
+
### Verification
|
|
179
|
+
|
|
180
|
+
1. `npx tsc --noEmit`
|
|
181
|
+
2. `npx vitest run`
|
|
182
|
+
3. Visual: robot inside Room 0 gets assigned to Room 2 → walks to Room 0 door → exits → walks to Room 2 door → enters → sits at desk
|
|
183
|
+
4. Visual: unassigned robot in corridor transitions to working → walks to a room door → enters → sits at desk
|
|
184
|
+
5. Visual: robot leaving a room (idle→coffee) walks to door → exits → walks to coffee area
|
|
185
|
+
6. No robots getting permanently stuck against walls
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Task 3: Fix React Error #185 (Maximum Update Depth + WebGL Context Lost)
|
|
190
|
+
|
|
191
|
+
### Problem
|
|
192
|
+
|
|
193
|
+
When clicking a robot in the 3D scene, the app crashes with:
|
|
194
|
+
```
|
|
195
|
+
Uncaught Error: Minified React error #185 (Maximum update depth exceeded)
|
|
196
|
+
```
|
|
197
|
+
Followed by: `THREE.WebGLRenderer: Context Lost.`
|
|
198
|
+
|
|
199
|
+
React Error #185 means a component is calling `setState` during rendering in an infinite loop. The WebGL context loss is a consequence — the browser kills the GPU context when the main thread is frozen by the infinite loop.
|
|
200
|
+
|
|
201
|
+
### Root Cause Analysis
|
|
202
|
+
|
|
203
|
+
The existing code has multiple mitigations (memo on SessionRobot/RobotLabel, startTransition for selection, hardcoded `isSelected=false`, seatedRef) but the error still occurs. The remaining culprit is the **drei `<Html>` portal cascade**:
|
|
204
|
+
|
|
205
|
+
1. **RobotLabel** uses `<Html>` (drei) which creates a React portal from WebGL into DOM
|
|
206
|
+
2. **RobotDialogue** also uses `<Html>` — another portal per robot
|
|
207
|
+
3. When the user clicks a robot, `selectSession()` is called. Even though `SessionRobot` doesn't subscribe to `selectedSessionId`, **other components** (like `SceneOverlay`, `RobotListSidebar`, or the `DetailPanel` outside the Canvas) re-render. This can cause the R3F reconciler to flush, triggering `<Html>` portal reconciliation across all robots simultaneously.
|
|
208
|
+
4. Each `<Html>` portal update can trigger more state updates (drei's internal resize observer, portal container management), creating a cascade.
|
|
209
|
+
|
|
210
|
+
Additionally, `RobotDialogue` has `useEffect` with `visible`/`fadingOut` state that may chain-update:
|
|
211
|
+
- `text` prop changes → `setVisible(true)` → re-render → `useEffect` fires again if `visible` was already truthy in the deps closure
|
|
212
|
+
|
|
213
|
+
### Fix Required
|
|
214
|
+
|
|
215
|
+
#### A. Replace `<Html>` in RobotDialogue with 3D `<Text>` (`src/components/3d/RobotDialogue.tsx`)
|
|
216
|
+
|
|
217
|
+
The dialogue bubble is the most volatile component (updates on every tool call). Replace its `<Html>` portal with drei `<Text>` (troika SDF text rendered in WebGL), eliminating one portal per robot:
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// Instead of <Html> portal:
|
|
221
|
+
<Billboard position={[0, 2.8, 0]} follow>
|
|
222
|
+
<Text fontSize={0.12} color="#fff" anchorX="center" anchorY="bottom" ...>
|
|
223
|
+
{displayText}
|
|
224
|
+
</Text>
|
|
225
|
+
</Billboard>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
This removes the DOM portal entirely. The visual result is similar — a floating text above the robot — but rendered natively in WebGL with zero DOM reconciliation overhead.
|
|
229
|
+
|
|
230
|
+
#### B. Stabilize RobotLabel memo (`src/components/3d/RobotLabel.tsx`)
|
|
231
|
+
|
|
232
|
+
The current memo comparison doesn't check `session.toolLog` length but `RobotLabelInner` reads `session.toolLog?.length` for the "N tool calls" display. Add it:
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
const RobotLabel = memo(RobotLabelInner, (prev, next) =>
|
|
236
|
+
prev.session.sessionId === next.session.sessionId &&
|
|
237
|
+
prev.session.status === next.session.status &&
|
|
238
|
+
prev.session.title === next.session.title &&
|
|
239
|
+
prev.session.projectName === next.session.projectName &&
|
|
240
|
+
prev.session.label === next.session.label &&
|
|
241
|
+
prev.session.currentPrompt === next.session.currentPrompt &&
|
|
242
|
+
prev.session.model === next.session.model &&
|
|
243
|
+
prev.session.startedAt === next.session.startedAt &&
|
|
244
|
+
(prev.session.toolLog?.length ?? 0) === (next.session.toolLog?.length ?? 0) &&
|
|
245
|
+
prev.robotState === next.robotState &&
|
|
246
|
+
prev.isSelected === next.isSelected &&
|
|
247
|
+
prev.isHovered === next.isHovered
|
|
248
|
+
);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### C. Throttle dialogue state updates (`src/components/3d/SessionRobot.tsx`)
|
|
252
|
+
|
|
253
|
+
The `useEffect` that sets `dialogue` fires on every `session.toolLog` reference change. If the store updates toolLog frequently (e.g., rapid tool calls), this causes rapid re-renders. Add a throttle:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// Only update dialogue if enough time has passed since the last update
|
|
257
|
+
const lastDialogueUpdate = useRef(0);
|
|
258
|
+
// In the useEffect:
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
if (now - lastDialogueUpdate.current < 500) return; // throttle to 2Hz
|
|
261
|
+
lastDialogueUpdate.current = now;
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### D. Verify `SessionRobot` memo stability
|
|
265
|
+
|
|
266
|
+
The memo comparison includes `prev.session === next.session`. Since the store creates a new `Map` on every update, and the session object may be a new reference even when nothing changed for this particular robot, this could cause unnecessary re-renders.
|
|
267
|
+
|
|
268
|
+
Consider making the comparison more granular — compare `session.sessionId`, `session.status`, `session.toolLog`, etc. instead of `session` reference equality.
|
|
269
|
+
|
|
270
|
+
### Files to Modify
|
|
271
|
+
|
|
272
|
+
| File | Change |
|
|
273
|
+
|------|--------|
|
|
274
|
+
| `src/components/3d/RobotDialogue.tsx` | Replace `<Html>` portal with `<Billboard><Text>` (pure WebGL) |
|
|
275
|
+
| `src/components/3d/RobotLabel.tsx` | Add `toolLog.length` and `startedAt` to memo comparison |
|
|
276
|
+
| `src/components/3d/SessionRobot.tsx` | Throttle dialogue updates, improve memo comparison to avoid session reference equality |
|
|
277
|
+
|
|
278
|
+
### Verification
|
|
279
|
+
|
|
280
|
+
1. `npx tsc --noEmit`
|
|
281
|
+
2. `npx vitest run`
|
|
282
|
+
3. **Critical test**: Click a robot in the 3D scene — no Error #185, no WebGL context loss
|
|
283
|
+
4. Click multiple robots rapidly — stable
|
|
284
|
+
5. With 10+ robots active, click any robot — no crash
|
|
285
|
+
6. Dialogue bubbles still appear above robots for tool calls and status changes
|
|
286
|
+
7. Rebuild production: `npx vite build` — open in browser, reproduce the click test
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Task 4: Persist Robot Positions Across Page Refreshes
|
|
291
|
+
|
|
292
|
+
### Problem
|
|
293
|
+
|
|
294
|
+
When the user refreshes the browser, all robots spawn at random positions near the origin and must re-navigate to their desks/casual areas. The user wants robots to remember their position and status so that after a refresh, the 3D scene looks the same as before — robots that were seated stay seated, robots that were walking continue from where they left off.
|
|
295
|
+
|
|
296
|
+
### Current State
|
|
297
|
+
|
|
298
|
+
- `robotPositionStore` (`src/components/3d/robotPositionStore.ts`) is an in-memory `Map<string, {x, y, z}>` — lost on refresh
|
|
299
|
+
- `NavState` in `SessionRobot.tsx:126-138` initializes with random positions:
|
|
300
|
+
```ts
|
|
301
|
+
posX: (Math.random() - 0.5) * 4,
|
|
302
|
+
posY: 0,
|
|
303
|
+
posZ: (Math.random() - 0.5) * 4,
|
|
304
|
+
```
|
|
305
|
+
- Workstation `occupantId` fields are in-memory only — lost on refresh
|
|
306
|
+
|
|
307
|
+
### Fix Required
|
|
308
|
+
|
|
309
|
+
#### A. Create a persistent position store (`src/lib/robotPositionPersist.ts`)
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
interface PersistedRobotState {
|
|
313
|
+
posX: number;
|
|
314
|
+
posY: number;
|
|
315
|
+
posZ: number;
|
|
316
|
+
rotY: number;
|
|
317
|
+
mode: number; // NAV_WALK, NAV_GOTO, NAV_SIT, NAV_IDLE
|
|
318
|
+
deskIdx: number; // which workstation the robot occupies (-1 if none)
|
|
319
|
+
robotState: string; // 'idle' | 'working' | etc
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const STORAGE_KEY = 'cyberdrome-robot-positions';
|
|
323
|
+
|
|
324
|
+
export function saveRobotPositions(data: Map<string, PersistedRobotState>): void {
|
|
325
|
+
// Convert to JSON and write to sessionStorage (survives refresh, not tab close)
|
|
326
|
+
const obj: Record<string, PersistedRobotState> = {};
|
|
327
|
+
data.forEach((v, k) => { obj[k] = v; });
|
|
328
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function loadRobotPositions(): Map<string, PersistedRobotState> {
|
|
332
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
333
|
+
if (!raw) return new Map();
|
|
334
|
+
const obj = JSON.parse(raw) as Record<string, PersistedRobotState>;
|
|
335
|
+
return new Map(Object.entries(obj));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function clearRobotPositions(): void {
|
|
339
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### B. Periodically save positions (`src/components/3d/CyberdromeScene.tsx`)
|
|
344
|
+
|
|
345
|
+
Add a `useFrame` hook in `SceneContent` that throttles saves to every 2 seconds:
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
const lastSave = useRef(0);
|
|
349
|
+
useFrame(() => {
|
|
350
|
+
const now = performance.now();
|
|
351
|
+
if (now - lastSave.current < 2000) return;
|
|
352
|
+
lastSave.current = now;
|
|
353
|
+
// Collect all NavState from SessionRobot refs → saveRobotPositions()
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Alternatively, expose nav state from SessionRobot via a shared non-reactive store (similar to `robotPositionStore`) and have the save logic in `CyberdromeScene`.
|
|
358
|
+
|
|
359
|
+
#### C. Initialize SessionRobot from persisted state (`src/components/3d/SessionRobot.tsx`)
|
|
360
|
+
|
|
361
|
+
On mount, check if there's a persisted position for this session:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
const persisted = loadRobotPositions().get(session.sessionId);
|
|
365
|
+
const nav = useRef<NavState>({
|
|
366
|
+
mode: persisted ? persisted.mode : NAV_WALK,
|
|
367
|
+
target: new THREE.Vector3(),
|
|
368
|
+
deskIdx: persisted ? persisted.deskIdx : -1,
|
|
369
|
+
speed: 1.0 + Math.random() * 0.7,
|
|
370
|
+
walkHz: 7 + Math.random() * 2,
|
|
371
|
+
phase: Math.random() * Math.PI * 2,
|
|
372
|
+
decisionTimer: 2 + Math.random() * 4,
|
|
373
|
+
posX: persisted ? persisted.posX : (Math.random() - 0.5) * 4,
|
|
374
|
+
posY: persisted ? persisted.posY : 0,
|
|
375
|
+
posZ: persisted ? persisted.posZ : (Math.random() - 0.5) * 4,
|
|
376
|
+
rotY: persisted ? persisted.rotY : Math.random() * Math.PI * 2,
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
If `persisted.deskIdx >= 0`, also restore `workstations[persisted.deskIdx].occupantId = session.sessionId` to reclaim the desk.
|
|
381
|
+
|
|
382
|
+
#### D. Extend `robotPositionStore` to also store nav state
|
|
383
|
+
|
|
384
|
+
Add a parallel `navStateStore` (non-reactive Map) that SessionRobot writes to every frame (or throttled). The save logic reads from this store.
|
|
385
|
+
|
|
386
|
+
### Files to Create/Modify
|
|
387
|
+
|
|
388
|
+
| File | Change |
|
|
389
|
+
|------|--------|
|
|
390
|
+
| `src/lib/robotPositionPersist.ts` | **NEW** — sessionStorage persistence for robot positions + nav state |
|
|
391
|
+
| `src/components/3d/SessionRobot.tsx` | Initialize `NavState` from persisted data; reclaim workstation desk; write nav state to shared store |
|
|
392
|
+
| `src/components/3d/robotPositionStore.ts` | Extend to also store nav mode, deskIdx, rotY |
|
|
393
|
+
| `src/components/3d/CyberdromeScene.tsx` | Add periodic save (every 2s) collecting all robot nav states |
|
|
394
|
+
|
|
395
|
+
### Verification
|
|
396
|
+
|
|
397
|
+
1. `npx tsc --noEmit`
|
|
398
|
+
2. `npx vitest run`
|
|
399
|
+
3. Open the 3D scene with multiple active sessions → robots navigate to desks and sit
|
|
400
|
+
4. Refresh the page (F5) → robots appear at the same positions, seated robots are still seated
|
|
401
|
+
5. Open DevTools → Application → Session Storage → verify `cyberdrome-robot-positions` key exists
|
|
402
|
+
6. Close tab and reopen → robots start fresh (sessionStorage only survives refresh, not close)
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Task 5: Relocate Casual Areas to North Side + Enhance Content
|
|
407
|
+
|
|
408
|
+
### Problem
|
|
409
|
+
|
|
410
|
+
The casual areas (Coffee Lounge & Gym) are currently placed **south** of the room grid (below the common area). The user wants them on the **north side** (behind the rooms, on the opposite side of the map). Additionally:
|
|
411
|
+
- Casual areas need visible **name labels** (like rooms have)
|
|
412
|
+
- Gym needs **at least 10 exercise devices** (currently only 4)
|
|
413
|
+
- Coffee area needs **coffee cups, a coffee pot, and a coffee machine** on the counter
|
|
414
|
+
|
|
415
|
+
### Current Placement (`src/lib/cyberdromeScene.ts:367-438`)
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
↑ North (z decreases)
|
|
419
|
+
[Coffee] [Gym] ← User wants here (north of rooms)
|
|
420
|
+
|
|
421
|
+
[Room 0] [Room 1] [Room 2] [Room 3] ← Dynamic rooms
|
|
422
|
+
[Room 4] [Room 5] ...
|
|
423
|
+
|
|
424
|
+
[Common Area - 10 desks] ← Corridor workstations
|
|
425
|
+
|
|
426
|
+
[Coffee Lounge] [Gym] ← Currently here (south)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Fix Required
|
|
430
|
+
|
|
431
|
+
#### A. Relocate casual areas to north of rooms (`src/lib/cyberdromeScene.ts`)
|
|
432
|
+
|
|
433
|
+
In `buildCasualAreas()`, change `baseZ` computation:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
// BEFORE: south of common area
|
|
437
|
+
// baseZ = southmostCenter[2] + ROOM_HALF + ROOM_GAP + 5 + 8 + CASUAL_HALF + 2;
|
|
438
|
+
|
|
439
|
+
// AFTER: north of all rooms
|
|
440
|
+
const minRow = 0; // rooms start at row 0
|
|
441
|
+
const northmostCenter = computeRoomCenter(0); // row 0 is always the northernmost
|
|
442
|
+
baseZ = northmostCenter[2] - ROOM_HALF - ROOM_GAP - CASUAL_HALF;
|
|
443
|
+
// This places them above (north of) the first row of rooms
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Also update `computeSceneBounds()` to account for the northward casual areas (add bounds check for negative Z values).
|
|
447
|
+
|
|
448
|
+
#### B. Add area name labels (`src/components/3d/RoomLabels.tsx` or new component)
|
|
449
|
+
|
|
450
|
+
Add labels for the casual areas similar to room labels. Either:
|
|
451
|
+
- Extend `RoomLabels` to also accept `CasualArea[]` and render labels
|
|
452
|
+
- Or create a small `CasualAreaLabels` component
|
|
453
|
+
|
|
454
|
+
Each casual area gets a ground-level 3D text label:
|
|
455
|
+
- Coffee: "COFFEE LOUNGE" in warm amber
|
|
456
|
+
- Gym: "FITNESS CENTER" in cool green/blue
|
|
457
|
+
|
|
458
|
+
Pass `casualAreas` from `CyberdromeScene.tsx` to the label component.
|
|
459
|
+
|
|
460
|
+
#### C. Expand gym to 10+ exercise devices (`src/components/3d/CyberdromeEnvironment.tsx`)
|
|
461
|
+
|
|
462
|
+
Current gym has 4 stations (bench press, treadmill, punching bag, weight rack). Expand to at least 10:
|
|
463
|
+
|
|
464
|
+
1. Bench press (existing)
|
|
465
|
+
2. Treadmill (existing)
|
|
466
|
+
3. Punching bag (existing)
|
|
467
|
+
4. Weight rack (existing)
|
|
468
|
+
5. **Rowing machine** — flat angled platform with handle bar
|
|
469
|
+
6. **Pull-up bar** — tall frame with horizontal bar at top
|
|
470
|
+
7. **Kettlebell station** — spheres on floor mat
|
|
471
|
+
8. **Stationary bike** — seat + handlebars + wheel
|
|
472
|
+
9. **Cable machine** — tall frame with pulley
|
|
473
|
+
10. **Battle ropes** — two cylinders (snaking lines) anchored to a post
|
|
474
|
+
|
|
475
|
+
Each device needs a station position in `buildCasualAreas()` for robot navigation. Expand the gym area size from 10 to ~16 to fit 10 stations in a grid layout:
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
// Expand gym size
|
|
479
|
+
const GYM_SIZE = 16; // was using CASUAL_AREA_SIZE=10
|
|
480
|
+
|
|
481
|
+
// 10 stations in a 2×5 grid
|
|
482
|
+
const gymPositions = [];
|
|
483
|
+
for (let row = 0; row < 2; row++) {
|
|
484
|
+
for (let col = 0; col < 5; col++) {
|
|
485
|
+
gymPositions.push({
|
|
486
|
+
x: gymX - 6 + col * 3,
|
|
487
|
+
z: baseZ - 3 + row * 6,
|
|
488
|
+
rot: row === 0 ? 0 : Math.PI,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### D. Add coffee accessories to Coffee Lounge (`src/components/3d/CyberdromeEnvironment.tsx`)
|
|
495
|
+
|
|
496
|
+
Add to the existing `CoffeeLounge` component:
|
|
497
|
+
|
|
498
|
+
1. **Coffee machine** — on the counter: box with a cylinder spout + small panel
|
|
499
|
+
```tsx
|
|
500
|
+
<group position={[cx - 2, 1.15, cz - 4.5]}>
|
|
501
|
+
<mesh material={equipMat}><boxGeometry args={[0.4, 0.5, 0.35]} /></mesh>
|
|
502
|
+
<mesh position={[0.1, 0.15, 0.2]} material={accentMat}>
|
|
503
|
+
<cylinderGeometry args={[0.03, 0.03, 0.15, 6]} />
|
|
504
|
+
</mesh>
|
|
505
|
+
</group>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
2. **Coffee pot** — on the counter: cylinder with handle
|
|
509
|
+
```tsx
|
|
510
|
+
<group position={[cx + 1, 1.15, cz - 4.5]}>
|
|
511
|
+
<mesh material={accentMat}><cylinderGeometry args={[0.08, 0.06, 0.2, 8]} /></mesh>
|
|
512
|
+
<mesh position={[0.1, 0.05, 0]} material={equipMat}>
|
|
513
|
+
<torusGeometry args={[0.06, 0.01, 4, 8, Math.PI]} />
|
|
514
|
+
</mesh>
|
|
515
|
+
</group>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
3. **Coffee cups** — on each table: 2 small cylinders per table
|
|
519
|
+
```tsx
|
|
520
|
+
<mesh position={[t.x - 0.15, 0.54, t.z + 0.1]} material={accentMat}>
|
|
521
|
+
<cylinderGeometry args={[0.03, 0.025, 0.06, 6]} />
|
|
522
|
+
</mesh>
|
|
523
|
+
<mesh position={[t.x + 0.15, 0.54, t.z - 0.1]} material={accentMat}>
|
|
524
|
+
<cylinderGeometry args={[0.03, 0.025, 0.06, 6]} />
|
|
525
|
+
</mesh>
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### E. Update scene bounds for northward placement
|
|
529
|
+
|
|
530
|
+
In `computeSceneBounds()`, add check for negative Z (northward areas):
|
|
531
|
+
```ts
|
|
532
|
+
// Also check for casual areas north of rooms
|
|
533
|
+
if (rooms.length > 0) {
|
|
534
|
+
const northCenter = computeRoomCenter(0);
|
|
535
|
+
maxDist = Math.max(maxDist, Math.abs(northCenter[2] - ROOM_HALF - ROOM_GAP - CASUAL_HALF - 5));
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Files to Modify
|
|
540
|
+
|
|
541
|
+
| File | Change |
|
|
542
|
+
|------|--------|
|
|
543
|
+
| `src/lib/cyberdromeScene.ts` | Relocate `buildCasualAreas()` baseZ to north of rooms; expand gym to 10 stations; increase gym area size; update `computeSceneBounds()` for northward bounds |
|
|
544
|
+
| `src/components/3d/CyberdromeEnvironment.tsx` | Add 6 new gym devices to `GymArea`; add coffee machine, pot, cups to `CoffeeLounge`; adjust area floor size for expanded gym |
|
|
545
|
+
| `src/components/3d/RoomLabels.tsx` | Add casual area labels (or create `CasualAreaLabels` component in same file); accept `CasualArea[]` prop |
|
|
546
|
+
| `src/components/3d/CyberdromeScene.tsx` | Pass `casualAreas` to `RoomLabels` or new label component |
|
|
547
|
+
|
|
548
|
+
### Verification
|
|
549
|
+
|
|
550
|
+
1. `npx tsc --noEmit`
|
|
551
|
+
2. `npx vitest run`
|
|
552
|
+
3. `npx vite build`
|
|
553
|
+
4. Visual: casual areas appear **north** of the room grid (above rooms in the scene)
|
|
554
|
+
5. Visual: "COFFEE LOUNGE" and "FITNESS CENTER" labels on the ground near each area
|
|
555
|
+
6. Visual: gym has 10 distinct exercise devices
|
|
556
|
+
7. Visual: coffee counter has a coffee machine, pot, and cups on tables
|
|
557
|
+
8. Idle robots still navigate to coffee area; waiting robots still navigate to gym
|
|
558
|
+
9. Casual areas adapt colors when switching themes
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Task 6: Increase Robot Moving Speed Significantly
|
|
563
|
+
|
|
564
|
+
### Problem
|
|
565
|
+
|
|
566
|
+
Robots move too slowly across the map. When navigating between rooms, to casual areas, or to desks, the walking animation feels sluggish. The user wants robots to move **much faster** so transitions feel snappy and the scene feels alive.
|
|
567
|
+
|
|
568
|
+
### Current Speed Values
|
|
569
|
+
|
|
570
|
+
**Base speed** (`src/components/3d/SessionRobot.tsx:130`):
|
|
571
|
+
```ts
|
|
572
|
+
speed: 1.0 + Math.random() * 0.7, // range: 1.0–1.7 units/sec
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Speed multipliers per state** (`src/lib/robotStateMap.ts:58-123`):
|
|
576
|
+
| State | `speedMultiplier` | Effective speed |
|
|
577
|
+
|-------|-------------------|-----------------|
|
|
578
|
+
| idle | 0.6 | 0.6–1.0 |
|
|
579
|
+
| thinking | 0.7 | 0.7–1.2 |
|
|
580
|
+
| working | 1.0 | 1.0–1.7 |
|
|
581
|
+
| waiting | 0.3 | 0.3–0.5 |
|
|
582
|
+
| alert | 0 | frozen |
|
|
583
|
+
| input | 0 | frozen |
|
|
584
|
+
| offline | 0 | frozen |
|
|
585
|
+
| connecting | 0 | frozen |
|
|
586
|
+
|
|
587
|
+
**Movement step** (`SessionRobot.tsx:325`):
|
|
588
|
+
```ts
|
|
589
|
+
const step = n.speed * behavior.speedMultiplier * dt;
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Turn rate** (`SessionRobot.tsx:322`):
|
|
593
|
+
```ts
|
|
594
|
+
n.rotY += diff * Math.min(1, 5 * dt); // rotation smoothing factor: 5
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Fix Required
|
|
598
|
+
|
|
599
|
+
#### A. Increase base speed (`src/components/3d/SessionRobot.tsx`)
|
|
600
|
+
|
|
601
|
+
Triple the base speed range:
|
|
602
|
+
```ts
|
|
603
|
+
// BEFORE
|
|
604
|
+
speed: 1.0 + Math.random() * 0.7, // 1.0–1.7
|
|
605
|
+
|
|
606
|
+
// AFTER
|
|
607
|
+
speed: 3.0 + Math.random() * 1.5, // 3.0–4.5
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
#### B. Increase state speed multipliers (`src/lib/robotStateMap.ts`)
|
|
611
|
+
|
|
612
|
+
Bump multipliers so robots are never crawling:
|
|
613
|
+
| State | Before | After |
|
|
614
|
+
|-------|--------|-------|
|
|
615
|
+
| idle | 0.6 | 1.0 |
|
|
616
|
+
| thinking | 0.7 | 1.2 |
|
|
617
|
+
| working | 1.0 | 1.5 |
|
|
618
|
+
| waiting | 0.3 | 0.8 |
|
|
619
|
+
| alert | 0 | 0 (stay frozen) |
|
|
620
|
+
| input | 0 | 0 (stay frozen) |
|
|
621
|
+
| offline | 0 | 0 (stay frozen) |
|
|
622
|
+
| connecting | 0 | 0 (stay frozen) |
|
|
623
|
+
|
|
624
|
+
#### C. Increase turn rate (`src/components/3d/SessionRobot.tsx`)
|
|
625
|
+
|
|
626
|
+
Faster movement needs faster turning or robots overshoot corners:
|
|
627
|
+
```ts
|
|
628
|
+
// BEFORE
|
|
629
|
+
n.rotY += diff * Math.min(1, 5 * dt);
|
|
630
|
+
|
|
631
|
+
// AFTER
|
|
632
|
+
n.rotY += diff * Math.min(1, 10 * dt);
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
#### D. Increase walk animation frequency (`src/components/3d/SessionRobot.tsx`)
|
|
636
|
+
|
|
637
|
+
The walk bounce should match the faster speed so the animation doesn't look floaty:
|
|
638
|
+
```ts
|
|
639
|
+
// BEFORE
|
|
640
|
+
walkHz: 7 + Math.random() * 2, // 7–9 Hz bounce
|
|
641
|
+
|
|
642
|
+
// AFTER
|
|
643
|
+
walkHz: 12 + Math.random() * 4, // 12–16 Hz bounce
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Files to Modify
|
|
647
|
+
|
|
648
|
+
| File | Change |
|
|
649
|
+
|------|--------|
|
|
650
|
+
| `src/components/3d/SessionRobot.tsx` | Increase `speed` (3.0–4.5), `walkHz` (12–16), turn rate (5→10) |
|
|
651
|
+
| `src/lib/robotStateMap.ts` | Increase `speedMultiplier` for idle (1.0), thinking (1.2), working (1.5), waiting (0.8) |
|
|
652
|
+
|
|
653
|
+
### Verification
|
|
654
|
+
|
|
655
|
+
1. `npx tsc --noEmit`
|
|
656
|
+
2. `npx vitest run`
|
|
657
|
+
3. Visual: robots move noticeably faster when walking between locations
|
|
658
|
+
4. Visual: robots don't overshoot targets or jitter (turn rate matches speed)
|
|
659
|
+
5. Visual: walk bounce animation looks natural at the faster speed
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## Execution Order & Parallelization Strategy
|
|
664
|
+
|
|
665
|
+
```
|
|
666
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
667
|
+
│ Task 1 │ │ Task 3 │ │ Task 6 │ ← Parallel (independent)
|
|
668
|
+
│ Remove ended │ │ Fix Error #185 │ │ Increase speed │
|
|
669
|
+
│ sessions │ │ Html → Text │ │ (robotStateMap) │
|
|
670
|
+
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
|
671
|
+
│ │ │
|
|
672
|
+
▼ ▼ ▼
|
|
673
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
674
|
+
│ Task 2 │ │ Task 4 │ ← Sequential (shared SessionRobot.tsx)
|
|
675
|
+
│ Door pathfind │ │ Position persist│
|
|
676
|
+
│ (SessionRobot) │ │ (SessionRobot) │
|
|
677
|
+
└────────┬────────┘ └────────┬────────┘
|
|
678
|
+
│ │
|
|
679
|
+
▼ ▼
|
|
680
|
+
┌─────────────────────────┐
|
|
681
|
+
│ Task 5 │ ← Sequential (touches shared layout files)
|
|
682
|
+
│ Relocate casual areas │
|
|
683
|
+
│ + content enhancement │
|
|
684
|
+
└─────────────────────────┘
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Phase 1** (parallel): Task 1 + Task 3 + Task 6
|
|
688
|
+
- Task 1 modifies `CyberdromeScene.tsx` (filter) + `RobotListSidebar.tsx`
|
|
689
|
+
- Task 3 modifies `RobotDialogue.tsx` + `RobotLabel.tsx` + `SessionRobot.tsx` (memo/throttle only)
|
|
690
|
+
- Task 6 modifies `robotStateMap.ts` (speed multipliers) + `SessionRobot.tsx` (base speed, walkHz, turn rate)
|
|
691
|
+
- **Note**: Task 3 and Task 6 both touch `SessionRobot.tsx` but in different areas (memo/dialogue vs speed init). Run Task 6 after Task 3 if conflicts arise.
|
|
692
|
+
|
|
693
|
+
**Phase 2** (sequential): Task 2 → Task 4
|
|
694
|
+
- Task 2 modifies `cyberdromeScene.ts` (add pathfinding) + `SessionRobot.tsx` (navigation logic) + `CyberdromeScene.tsx` (pass doors)
|
|
695
|
+
- Task 4 creates `robotPositionPersist.ts` + modifies `SessionRobot.tsx` (init from persistence) + `robotPositionStore.ts` + `CyberdromeScene.tsx` (save loop)
|
|
696
|
+
- **Conflict on SessionRobot.tsx and CyberdromeScene.tsx** — must coordinate. Task 2 changes NavState structure + useFrame navigation; Task 4 changes NavState init + adds persistence write. Run sequentially with Task 2 first.
|
|
697
|
+
|
|
698
|
+
**Phase 3** (sequential): Task 5
|
|
699
|
+
- Modifies `cyberdromeScene.ts`, `CyberdromeEnvironment.tsx`, `RoomLabels.tsx`, `CyberdromeScene.tsx`
|
|
700
|
+
- Must run after Task 2 (which also modifies `cyberdromeScene.ts`)
|
|
701
|
+
|
|
702
|
+
**Recommended serial order if not parallelizing:**
|
|
703
|
+
1. Task 1 — smallest
|
|
704
|
+
2. Task 3 — fixes critical crash
|
|
705
|
+
3. Task 6 — speed increase (small, isolated)
|
|
706
|
+
4. Task 2 — navigation overhaul
|
|
707
|
+
5. Task 4 — persistence (depends on Task 2's NavState changes)
|
|
708
|
+
6. Task 5 — largest, content + relocation
|
|
709
|
+
|
|
710
|
+
## Build Commands
|
|
711
|
+
|
|
712
|
+
```bash
|
|
713
|
+
npx tsc --noEmit # Type check
|
|
714
|
+
npx vitest run # Test suite (407 tests)
|
|
715
|
+
npx vite build # Production build to dist/
|
|
716
|
+
```
|