# Plan: Multi-Board Simulator UX — Full Design ## Context The codebase already has a solid multi-board foundation: `boards[]` array in `useSimulatorStore`, per-board file groups in `useEditorStore`, separate simulator instances (`simulatorMap`, `pinManagerMap`), QEMU bridges for Raspberry Pi 3 and ESP32, and board-specific compilation via `BOARD_KIND_FQBN`. However, several UX layers are missing: the editor doesn't visually indicate which board you're editing, there's no way to compile/run all boards at once, the serial monitor only shows one board, and the Raspberry Pi 3 has no special terminal/VFS interface. ## What Already Exists (Do NOT Re-implement) - `boards[]` + `addBoard()` / `removeBoard()` / `setActiveBoardId()` in `useSimulatorStore` - `fileGroups` / `createFileGroup()` / `setActiveGroup()` in `useEditorStore` - `simulatorMap`, `pinManagerMap`, `bridgeMap`, `esp32BridgeMap` runtime maps - `compileBoardProgram()`, `startBoard()`, `stopBoard()`, `resetBoard()` - `RaspberryPi3Bridge` with `sendSerialBytes()` / `onSerialData` callback - `BOARD_KIND_FQBN`, `BOARD_KIND_LABELS` in `frontend/src/types/board.ts` - `BoardPickerModal`, `BoardOnCanvas`, `SerialMonitor` components --- ## Phase 1 — Board-Aware Editor UI (Foundation) ### 1A. Canvas → Editor Sync **File:** `frontend/src/components/simulator/BoardOnCanvas.tsx` - Add `onBoardClick?: (boardId: string) => void` prop - On click (not drag — detect by < 4px mouse movement), call `onBoardClick(board.id)` - In `SimulatorCanvas.tsx`, pass `onBoardClick={(id) => useSimulatorStore.getState().setActiveBoardId(id)}` — `setActiveBoardId` already calls `useEditorStore.getState().setActiveGroup()`, so this is one line ### 1B. Board-Grouped FileExplorer **File:** `frontend/src/components/editor/FileExplorer.tsx` Replace flat `files.map()` with a grouped tree: ``` boards.map(board => ( setActiveBoardId(board.id)}> {fileGroups[board.activeFileGroupId].map(file => )} // scoped to this group )) ``` - Section header: board emoji icon + label + status dot (green=running, amber=compiled, gray=idle) - `createFile`, `deleteFile`, `renameFile` operate on active group — clicking section header first sets the active board ### 1C. Board Context Pill in EditorToolbar **File:** `frontend/src/components/editor/EditorToolbar.tsx` Add a colored pill at the left of the toolbar: - Shows: `{emoji} Arduino Uno #1` - Color by family: Arduino=blue, Raspberry Pi=red, ESP32=green - Clickable — opens a small dropdown to switch active board without going to the canvas --- ## Phase 2 — Compile All / Run All Orchestration ### 2A. New component: `CompileAllProgress` **File:** `frontend/src/components/editor/CompileAllProgress.tsx` (new) Sliding panel showing per-board compile status: ```typescript interface BoardCompileStatus { boardId: string; boardKind: BoardKind; label: string; state: 'pending' | 'compiling' | 'success' | 'error' | 'skipped'; error?: string; } ``` - Each row: board icon + label + spinner/checkmark/X - Error rows expand to show compiler stderr - "Run All" button at bottom — enabled after all compilations finish; only starts boards that succeeded or skipped ### 2B. "Compile All" + "Run All" buttons in EditorToolbar **File:** `frontend/src/components/editor/EditorToolbar.tsx` `handleCompileAll` logic: 1. Iterate `boards[]` **sequentially** (not parallel — `arduino-cli` is CPU-heavy, shares temp dirs) 2. For `raspberry-pi-3`: mark `skipped`, continue 3. For each other board: read files via `useEditorStore.getState().getGroupFiles(board.activeFileGroupId)` 4. Call `compileCode(sketchFiles, fqbn)`, on success call `compileBoardProgram(boardId, program)` 5. **Always continue to next board on error** — never abort 6. If panel is closed mid-run, compilation continues in background `handleRunAll`: iterate boards, call `startBoard(id)` for all that have `compiledProgram !== null` or are Pi/ESP32 --- ## Phase 3 — Multi-Board Serial Monitor **File:** `frontend/src/components/simulator/SerialMonitor.tsx` Redesign with a tab strip (one tab per board): - Each tab: board emoji + short label + unread dot (new output since tab last viewed) - Output area/input row operates on `activeTab` board - Add to `useSimulatorStore`: - `serialWriteToBoard(boardId, text)` — like `serialWrite` but with explicit boardId (6 lines) - `clearBoardSerialOutput(boardId)` — like `clearSerialOutput` with explicit boardId (4 lines) - Default active tab follows `activeBoardId` --- ## Phase 4 — Raspberry Pi 3 Special Workspace This is the most complex phase. When `activeBoard.boardKind === 'raspberry-pi-3'`, the left panel switches from Monaco editor to a specialized workspace. ### 4A. Install xterm.js ```bash cd frontend npm install @xterm/xterm @xterm/addon-fit ``` Use scoped packages (`@xterm/xterm` v5+), NOT deprecated `xterm` v4. ### 4B. New store: `useVfsStore` **File:** `frontend/src/store/useVfsStore.ts` (new) Keep separate from `useSimulatorStore` (which is already 970+ lines). VFS is a tree structure, fundamentally different from the flat file-group lists in `useEditorStore`. ```typescript interface VfsNode { id: string; name: string; type: 'file' | 'directory'; content?: string; children?: VfsNode[]; parentId: string | null; } // State: trees: Record (root "/" per board) // Actions: createNode, deleteNode, renameNode, setContent, setSelectedNode, initBoardVfs ``` Default tree for new Pi board: ``` / home/pi/ script.py (default Python template) hello.sh ``` Call `initBoardVfs(boardId)` inside `useSimulatorStore.addBoard()` when `boardKind === 'raspberry-pi-3'`. ### 4C. New: `PiTerminal.tsx` **File:** `frontend/src/components/raspberry-pi/PiTerminal.tsx` (new) - Mounts xterm.js Terminal into a `ref` div - `term.onData(data => bridge.sendSerialBytes(...))` — input → QEMU - Intercept `bridge.onSerialData` to write to terminal (save+restore prev callback to keep store's `serialOutput` in sync) - `ResizeObserver` → `fitAddon.fit()` for responsive layout - Lazy-loaded via `React.lazy()` in EditorPage to keep xterm.js out of the main bundle ### 4D. New: `VirtualFileSystem.tsx` **File:** `frontend/src/components/raspberry-pi/VirtualFileSystem.tsx` (new) - Recursive tree component, reads from `useVfsStore` - Expand/collapse directories - Click file → calls `onFileSelect(nodeId, content, filename)` - Right-click context menu: New File, New Folder, Rename, Delete - "Upload to Pi" button in header: serializes tree to `{path, content}[]` and calls `bridge.sendFile(path, content)` for each node (requires new `sendFile` method on `RaspberryPi3Bridge` and backend protocol message `{ type: 'vfs_write', data: { path, content } }`) ### 4E. New: `RaspberryPiWorkspace.tsx` **File:** `frontend/src/components/raspberry-pi/RaspberryPiWorkspace.tsx` (new) Two-pane layout: - **Left**: `VirtualFileSystem` - **Right**: Tab strip with "Terminal" tab + open file tabs - Terminal tab → `PiTerminal` - File tab → Monaco `CodeEditor` (content synced to `useVfsStore.setContent`) - Pi-specific toolbar: Connect / Disconnect / Upload Files to Pi ### 4F. EditorPage — conditional render **File:** `frontend/src/pages/EditorPage.tsx` ```typescript const isRaspberryPi3 = activeBoard?.boardKind === 'raspberry-pi-3'; // In JSX: {isRaspberryPi3 && activeBoardId ? Loading...}> : } ``` Hide `FileTabs` when in Pi mode (VFS replaces them). --- ## Phase 5 — Board Status Indicators on Canvas **File:** `frontend/src/components/simulator/BoardOnCanvas.tsx` Add two overlays (absolutely positioned, `pointerEvents: none`): 1. **Active board highlight ring**: 2px `#007acc` border around board bounds when `board.id === activeBoardId` 2. **Status dot**: 12px circle at top-right corner - Green (`#22c55e`) = running - Amber (`#f59e0b`) = compiled, not running - Gray (`#6b7280`) = idle Pass `activeBoardId` as a prop from `SimulatorCanvas`. --- ## Implementation Order ``` Phase 1A → 1B → 1C (foundation — pure UI, no new deps) │ ↓ Phase 5 (30 min, canvas badges — immediate visual feedback) │ ↓ Phase 3 (serial monitor tabs — adds 2 store actions) │ ↓ Phase 2 (compile all — uses existing store APIs) │ ↓ Phase 4A → 4B → 4C → 4D → 4E → 4F (Pi workspace — most complex, new npm dep) ``` --- ## Critical Files to Modify | File | Phase | Changes | |------|-------|---------| | `frontend/src/components/simulator/BoardOnCanvas.tsx` | 1A, 5 | `onBoardClick` prop, status badges, active ring | | `frontend/src/components/simulator/SimulatorCanvas.tsx` | 1A | Pass `onBoardClick` handler | | `frontend/src/components/editor/FileExplorer.tsx` | 1B | Board-grouped tree replacing flat list | | `frontend/src/components/editor/EditorToolbar.tsx` | 1C, 2B | Board pill, Compile All, Run All | | `frontend/src/store/useSimulatorStore.ts` | 3 | Add `serialWriteToBoard`, `clearBoardSerialOutput` | | `frontend/src/components/simulator/SerialMonitor.tsx` | 3 | Board tabs | | `frontend/src/pages/EditorPage.tsx` | 4F | Conditional Pi workspace vs CodeEditor | ## New Files to Create | File | Phase | |------|-------| | `frontend/src/components/editor/CompileAllProgress.tsx` | 2A | | `frontend/src/store/useVfsStore.ts` | 4B | | `frontend/src/components/raspberry-pi/PiTerminal.tsx` | 4C | | `frontend/src/components/raspberry-pi/VirtualFileSystem.tsx` | 4D | | `frontend/src/components/raspberry-pi/RaspberryPiWorkspace.tsx` | 4E | ## Verification 1. **Phase 1**: Click an Arduino on the canvas → FileExplorer highlights that board's files, toolbar pill updates 2. **Phase 2**: Add 2 Arduino boards with different code → "Compile All" → progress panel shows both → "Run All" starts both 3. **Phase 3**: 2 boards running → Serial Monitor has 2 tabs, unread dot appears when output arrives on background tab 4. **Phase 4**: Add Raspberry Pi 3 → editor area switches to VFS/terminal; create a `script.py` in VFS, upload to Pi, run it from terminal 5. **Phase 5**: Canvas shows green dot on running boards, amber on compiled-not-running, gray on idle; blue ring on active board