From d08693543258dd20058b38517c9e94b09c3f0e0e Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Thu, 19 Feb 2026 08:16:23 +0100 Subject: [PATCH 01/17] refactor(simulator): extract Sidebar UI into SimulatorSidebar component (fix types/imports) --- .../features/simulator/SimulatorSidebar.tsx | 61 ++++++++++++++++++ client/src/pages/arduino-simulator.tsx | 64 ++++++++----------- 2 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 client/src/components/features/simulator/SimulatorSidebar.tsx diff --git a/client/src/components/features/simulator/SimulatorSidebar.tsx b/client/src/components/features/simulator/SimulatorSidebar.tsx new file mode 100644 index 00000000..6ab19d23 --- /dev/null +++ b/client/src/components/features/simulator/SimulatorSidebar.tsx @@ -0,0 +1,61 @@ +import { PinMonitor } from "@/components/features/pin-monitor"; +import { ArduinoBoard } from "@/components/features/arduino-board"; + +type BatchStats = { lastBatchMs: number; lastBatchSize: number; lastFrameAt: number }; + +type SimulationStatus = "running" | "stopped" | "paused"; + +type SimulatorSidebarProps = { + pinMonitorVisible: boolean; + pinStates: any[]; + batchStats: BatchStats; + simulationStatus: SimulationStatus | undefined; + txActivity: number; + rxActivity: number; + onReset: () => void; + onPinToggle: (pin: number, newValue: number) => void; + analogPins: number[]; + onAnalogChange: (pin: number, newValue: number) => void; + isMobile?: boolean; +}; + +export default function SimulatorSidebar({ + pinMonitorVisible, + pinStates, + batchStats, + simulationStatus, + txActivity, + rxActivity, + onReset, + onPinToggle, + analogPins, + onAnalogChange, + isMobile = false, +}: SimulatorSidebarProps) { + // Pure UI/presentation component — receives data + callbacks from parent hooks. + const isRunning = simulationStatus !== "stopped"; + + return ( +
+ {pinMonitorVisible && ( +
+ +
+ )} + +
+ +
+
+ ); +} diff --git a/client/src/pages/arduino-simulator.tsx b/client/src/pages/arduino-simulator.tsx index f29cb694..75ea37e9 100644 --- a/client/src/pages/arduino-simulator.tsx +++ b/client/src/pages/arduino-simulator.tsx @@ -33,10 +33,9 @@ import { CompilationOutput } from "@/components/features/compilation-output"; import { ParserOutput } from "@/components/features/parser-output"; import { SketchTabs } from "@/components/features/sketch-tabs"; import { ExamplesMenu } from "@/components/features/examples-menu"; -import { ArduinoBoard } from "@/components/features/arduino-board"; -import { PinMonitor } from "@/components/features/pin-monitor"; import { AppHeader } from "@/components/features/app-header"; import { SimCockpit } from "@/components/features/sim-cockpit"; +import SimulatorSidebar from "@/components/features/simulator/SimulatorSidebar"; import { useWebSocket } from "@/hooks/use-websocket"; import { useWebSocketHandler } from "@/hooks/useWebSocketHandler"; import { useCompilation } from "@/hooks/use-compilation"; @@ -2032,24 +2031,18 @@ export default function ArduinoSimulator() { /> -
- {pinMonitorVisible && ( - - )} -
- -
-
+
@@ -2233,24 +2226,19 @@ export default function ArduinoSimulator() { )} {mobilePanel === "board" && (
-
- {pinMonitorVisible && ( - - )} -
- -
-
+
)} From 15aa8bf2238ad3e382bbe280487fa5391e79e189 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Thu, 19 Feb 2026 12:31:41 +0100 Subject: [PATCH 02/17] refactor(simulator): accept visual baseline + provider refactor --- client/src/components/features/app-header.tsx | 114 +-- .../features/simulator/SimulatorHeader.tsx | 173 ++++ .../simulator/SimulatorOutputPanel.tsx | 288 ++++++ .../features/simulator/SimulatorSidebar.tsx | 185 +++- client/src/hooks/use-compilation.ts | 71 +- client/src/hooks/use-simulation-controls.ts | 53 +- client/src/hooks/use-simulation-ui.tsx | 186 ++++ client/src/hooks/useWebSocketHandler.ts | 7 +- client/src/lib/debugBridge.ts | 0 client/src/pages/arduino-simulator.tsx | 895 +++--------------- e2e/analyze-diff.cjs | 36 + e2e/baseline-simulator.png | Bin 55563 -> 58872 bytes e2e/current-simulator.png | Bin 0 -> 58872 bytes .../arduino-simulator-codechange.test.tsx | 9 +- tests/client/hooks/use-compilation.test.tsx | 46 +- .../hooks/use-simulation-controls.test.tsx | 106 ++- 16 files changed, 1147 insertions(+), 1022 deletions(-) create mode 100644 client/src/components/features/simulator/SimulatorHeader.tsx create mode 100644 client/src/components/features/simulator/SimulatorOutputPanel.tsx create mode 100644 client/src/hooks/use-simulation-ui.tsx create mode 100644 client/src/lib/debugBridge.ts create mode 100644 e2e/analyze-diff.cjs create mode 100644 e2e/current-simulator.png diff --git a/client/src/components/features/app-header.tsx b/client/src/components/features/app-header.tsx index 6740488e..0903b6f8 100644 --- a/client/src/components/features/app-header.tsx +++ b/client/src/components/features/app-header.tsx @@ -383,118 +383,8 @@ export const AppHeader: React.FC = ({
- {/* Center: Simulate Button */} -
- -
+ {/* Center simulate control moved to `SimulatorHeader` (keeps AppHeader small) */} +
{/* Right: Optional telemetry/extra controls */}
diff --git a/client/src/components/features/simulator/SimulatorHeader.tsx b/client/src/components/features/simulator/SimulatorHeader.tsx new file mode 100644 index 00000000..c9878737 --- /dev/null +++ b/client/src/components/features/simulator/SimulatorHeader.tsx @@ -0,0 +1,173 @@ +import React from "react"; +import { Play, Zap, Server } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; + +type Props = { + simulationStatus: "idle" | "running" | "compiling" | "stopped" | "paused"; + simulateDisabled?: boolean; + isCompiling?: boolean; + isStarting?: boolean; + isStopping?: boolean; + isPausing?: boolean; + isResuming?: boolean; + onSimulate?: () => void; + onStop?: () => void; + onPause?: () => void; + onResume?: () => void; + simulationTimeout?: number; + onTimeoutChange?: (n: number) => void; + onCompile: () => void; + onCompileAndStart: () => void; + board?: string; +}; + +function getStatusTextClass(status: Props["simulationStatus"]) { + switch (status) { + case "idle": + return "text-gray-500 italic"; + case "ready": + return "text-gray-700"; + case "running": + return "text-green-600"; + case "stopped": + return "text-gray-600"; + default: + return ""; + } +} + +export default function SimulatorHeader({ + simulationStatus, + simulateDisabled = false, + isCompiling = false, + isStarting = false, + isStopping = false, + isPausing = false, + isResuming = false, + onSimulate, + onStop, + onPause, + onResume, + simulationTimeout, + onTimeoutChange, + onCompile, + onCompileAndStart, + board = "Arduino UNO", +}: Props) { + return ( +
+ {/* Simulate toggle (compact desktop) */} +
+ + + {/* timeout display + small setter */} +
+ {simulationTimeout ?? 60}s +
+
+ + {/* Simulate toggle (mobile) */} +
+ +
+ + {/* Compile button + small action menu */} + + + + + + Compile + Compile & Run + + + + {/* Board selector (display-only for now) */} +
+ + {board} +
+ + {/* Status text (keeps visual parity with previous header) */} +
+ {simulationStatus} +
+
+ ); +} diff --git a/client/src/components/features/simulator/SimulatorOutputPanel.tsx b/client/src/components/features/simulator/SimulatorOutputPanel.tsx new file mode 100644 index 00000000..1306c81b --- /dev/null +++ b/client/src/components/features/simulator/SimulatorOutputPanel.tsx @@ -0,0 +1,288 @@ +import React, { Suspense } from "react"; +import { InputGroup } from "@/components/ui/input-group"; +import { SerialMonitor } from "@/components/features/serial-monitor"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; +import { Button } from "@/components/ui/button"; +import { Terminal, BarChart, Columns, Trash2, ChevronsDown, LayoutGrid, Table } from "lucide-react"; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; +import { useWebSocket } from "@/hooks/use-websocket"; +import { useToast } from "@/hooks/use-toast"; +import { useQueryClient } from "@tanstack/react-query"; +import { useBackendHealth } from "@/hooks/use-backend-health"; +import { useTelemetryStore } from "@/hooks/use-telemetry-store"; + +const SerialPlotter = React.lazy(() => import("@/components/features/serial-plotter").then((m) => ({ default: m.SerialPlotter }))); + +const LoadingPlaceholder = () => ( +
+ Loading chart... +
+); + +export default function SimulatorOutputPanel() { + const queryClient = useQueryClient(); + const { ensureBackendConnected } = useBackendHealth(queryClient); + const { toast } = useToast(); + const { sendMessage } = useWebSocket(); + + const telemetryData = useTelemetryStore(); + + const { + serialOutput = [], + renderedSerialOutput = [], + serialViewMode = "monitor", + autoScrollEnabled = false, + setAutoScrollEnabled, + serialInputValue = "", + setSerialInputValue, + showSerialMonitor = true, + showSerialPlotter = false, + cycleSerialViewMode, + clearSerialOutput, + } = useSimulationUi(); + + const { + simulationStatus, + setTxActivity, + debugMode, + debugMessages, + debugViewMode, + setDebugViewMode, + debugMessageFilter, + setDebugMessageFilter, + debugMessagesContainerRef, + setDebugMessages, + } = useSimulationUi(); + + const handleSerialSend = (message: string) => { + if (!ensureBackendConnected("Serial senden")) return; + + if (simulationStatus !== "running") { + toast({ + title: simulationStatus === "paused" ? "Simulation paused" : "Simulation not running", + description: + simulationStatus === "paused" + ? "Resume the simulation to send serial input." + : "Start the simulation to send serial input.", + variant: "destructive", + }); + return; + } + + setTxActivity?.((prev) => prev + 1); + sendMessage({ type: "serial_input", data: message }); + }; + + const handleSerialInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") handleSerialSend(serialInputValue); + }; + + return ( +
+ {/* Static Serial Header (always full width) */} +
+
+
+ Serial Output +
+ + {debugMode && (simulationStatus === "running" || simulationStatus === "paused") && telemetryData.last ? ( +
+
+ Serial Events + {(telemetryData.last.serialOutputPerSecond ?? 0).toFixed(1)} /s +
+
+ Serial Bytes + {(telemetryData.last.serialBytesPerSecond ?? 0).toFixed(1)} /s +
+
+ ) : null} + +
+ +
+ + + + + +
+
+
+ +
+ {/* Serial area */} + {showSerialMonitor && showSerialPlotter ? ( + + +
+
+ +
+
+
+ + + + +
+ }> + + +
+
+
+ ) : showSerialMonitor ? ( +
+
+ +
+
+ ) : ( +
+ }> + + +
+ )} +
+ + {/* Input area */} +
+
+ setSerialInputValue(e.target.value)} + onKeyDown={handleSerialInputKeyDown} + onSubmit={() => handleSerialSend(serialInputValue)} + disabled={!serialInputValue.trim() || simulationStatus !== "running"} + inputTestId="input-serial" + buttonTestId="button-send-serial" + /> +
+
+ + {/* Debug console (moved here from Sidebar) */} + {debugMode && ( +
+
+
+ Filter: + +
+ +
+
+ )} +
+ ); +} diff --git a/client/src/components/features/simulator/SimulatorSidebar.tsx b/client/src/components/features/simulator/SimulatorSidebar.tsx index 6ab19d23..e03b309f 100644 --- a/client/src/components/features/simulator/SimulatorSidebar.tsx +++ b/client/src/components/features/simulator/SimulatorSidebar.tsx @@ -1,5 +1,14 @@ import { PinMonitor } from "@/components/features/pin-monitor"; import { ArduinoBoard } from "@/components/features/arduino-board"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { CompilationOutput } from "@/components/features/compilation-output"; +import { ParserOutput } from "@/components/features/parser-output"; +import { ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; +import { X } from "lucide-react"; +import { useSimulationStore } from "@/hooks/use-simulation-store"; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; type BatchStats = { lastBatchMs: number; lastBatchSize: number; lastFrameAt: number }; @@ -7,11 +16,6 @@ type SimulationStatus = "running" | "stopped" | "paused"; type SimulatorSidebarProps = { pinMonitorVisible: boolean; - pinStates: any[]; - batchStats: BatchStats; - simulationStatus: SimulationStatus | undefined; - txActivity: number; - rxActivity: number; onReset: () => void; onPinToggle: (pin: number, newValue: number) => void; analogPins: number[]; @@ -19,26 +23,53 @@ type SimulatorSidebarProps = { isMobile?: boolean; }; +export type OutputApi = { + cliOutput: string; + handleClearCompilationOutput: () => void; + isSuccessState: boolean; + isModified: boolean; + parserMessages: any[]; + ioRegistry: any[]; + parserMessagesContainerRef: React.RefObject; + activeOutputTab: "compiler" | "messages" | "registry"; + setActiveOutputTab: (v: any) => void; + showCompilationOutput: boolean; + setShowCompilationOutput: (v: boolean) => void; + setParserPanelDismissed: (v: boolean) => void; + outputPanelRef: any; + outputTabsHeaderRef: React.RefObject; + outputPanelMinPercent: number; + compilationPanelSize: number; + setCompilationPanelSize: (n: number) => void; + outputPanelManuallyResizedRef: React.MutableRefObject; + openOutputPanel: (tab: any) => void; + toast: (args: any) => void; + setParserPanelDismissedLocal?: (v: boolean) => void; +}; + export default function SimulatorSidebar({ - pinMonitorVisible, - pinStates, - batchStats, - simulationStatus, - txActivity, - rxActivity, + pinMonitorVisible = true, onReset, onPinToggle, - analogPins, + analogPins = [], onAnalogChange, isMobile = false, -}: SimulatorSidebarProps) { - // Pure UI/presentation component — receives data + callbacks from parent hooks. - const isRunning = simulationStatus !== "stopped"; +}: Partial<{ + pinMonitorVisible: boolean; + onReset: () => void; + onPinToggle: (pin: number, newValue: number) => void; + analogPins: number[]; + onAnalogChange: (pin: number, newValue: number) => void; + isMobile?: boolean; +}>) { + const { pinStates, batchStats } = useSimulationStore(); + const { simulationStatus, txActivity, rxActivity } = useSimulationUi(); + const isRunning = simulationStatus === "running"; return (
{pinMonitorVisible && ( -
+
)} @@ -51,11 +82,129 @@ export default function SimulatorSidebar({ txActive={txActivity} rxActive={rxActivity} onReset={onReset} - onPinToggle={onPinToggle} + onPinToggle={onPinToggle as any} analogPins={analogPins} - onAnalogChange={onAnalogChange} + onAnalogChange={onAnalogChange as any} />
); } + +export function SimulatorOutput({ outputApi }: { outputApi: OutputApi }) { + const { + cliOutput, + handleClearCompilationOutput, + isSuccessState, + isModified, + parserMessages, + ioRegistry, + parserMessagesContainerRef, + activeOutputTab, + setActiveOutputTab, + showCompilationOutput, + setShowCompilationOutput, + setParserPanelDismissed, + outputPanelRef, + outputTabsHeaderRef, + outputPanelMinPercent, + compilationPanelSize, + setCompilationPanelSize, + outputPanelManuallyResizedRef, + openOutputPanel, + toast, + } = outputApi; + + return ( + <> + + + setActiveOutputTab(v as any)} + className="h-full flex flex-col" + > +
+ + openOutputPanel("compiler")} + className={"h-[var(--ui-button-height)] px-2 text-ui-xs data-[state=active]:bg-background rounded-sm py-0 leading-none flex items-center"} + > + Compiler + + openOutputPanel("messages")} + className={"h-[var(--ui-button-height)] px-2 text-ui-xs data-[state=active]:bg-background rounded-sm py-0 leading-none flex items-center"} + > + Messages + + openOutputPanel("registry")} + className={"h-[var(--ui-button-height)] px-2 text-ui-xs data-[state=active]:bg-background rounded-sm py-0 leading-none flex items-center"} + > + I/O Registry + + + +
+
+ +
+
+ + + + + + + setParserPanelDismissed(true)} hideHeader={true} /> + + + + {}} hideHeader={true} defaultTab="registry" /> + + + + + + + ); +} + diff --git a/client/src/hooks/use-compilation.ts b/client/src/hooks/use-compilation.ts index f54ea1c3..4c66e54b 100644 --- a/client/src/hooks/use-compilation.ts +++ b/client/src/hooks/use-compilation.ts @@ -37,8 +37,6 @@ type UseCompilationParams = { setIoRegistry: SetState; setHasCompiledOnce: SetState; setIsModified: SetState; - setDebugMessages: SetState; - addDebugMessage: (params: DebugMessageParams) => void; ensureBackendConnected: (reason: string) => boolean; isBackendUnreachableError: (error: unknown) => boolean; triggerErrorGlitch: () => void; @@ -50,6 +48,8 @@ type UseCompilationParams = { startSimulation: () => void; }; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; + export function useCompilation({ editorRef, tabs, @@ -63,14 +63,14 @@ export function useCompilation({ setIoRegistry, setHasCompiledOnce, setIsModified, - setDebugMessages, - addDebugMessage, ensureBackendConnected, isBackendUnreachableError, triggerErrorGlitch, toast, startSimulation, }: UseCompilationParams) { + // pull debug helpers from UI context (provider owns debug state) + const { addDebugMessage, setDebugMessages } = useSimulationUi(); const [compilationStatus, setCompilationStatus] = useState( "ready", ); @@ -94,16 +94,16 @@ export function useCompilation({ const uploadMutation = useMutation({ mutationFn: async (payload: CompilePayload) => { - addDebugMessage({ - source: "frontend", - type: "upload_request", - data: JSON.stringify( + addDebugMessage( + "frontend", + "upload_request", + JSON.stringify( { endpoint: "POST /api/upload", codeLength: payload.code.length }, null, 2, ), - protocol: "http", - }); + "http", + ); const response = await apiRequest("POST", "/api/upload", payload); const ct = (response.headers.get("content-type") || "").toLowerCase(); if (ct.includes("application/json")) { @@ -166,16 +166,16 @@ export function useCompilation({ mutationFn: async (payload: CompilePayload) => { setArduinoCliStatus("compiling"); setLastCompilationResult(null); - addDebugMessage({ - source: "frontend", - type: "compile_request", - data: JSON.stringify( + addDebugMessage( + "frontend", + "compile_request", + JSON.stringify( { endpoint: "POST /api/compile", codeLength: payload.code.length }, null, 2, ), - protocol: "http", - }); + "http", + ); const response = await apiRequest("POST", "/api/compile", payload); const ct = (response.headers.get("content-type") || "").toLowerCase(); if (ct.includes("application/json")) { @@ -195,34 +195,34 @@ export function useCompilation({ setHasCompilationErrors(false); setLastCompilationResult("success"); setCliOutput(data.output || "✓ Arduino-CLI Compilation succeeded."); - addDebugMessage({ - source: "server", - type: "compilation_status", - data: JSON.stringify({ gccStatus: "success" }, null, 2), - protocol: "http", - }); + addDebugMessage( + "server", + "compilation_status", + JSON.stringify({ gccStatus: "success" }, null, 2), + "http", + ); } else { setArduinoCliStatus("error"); setHasCompilationErrors(true); setLastCompilationResult("error"); triggerErrorGlitch(); setCliOutput(data.errors || "✗ Arduino-CLI Compilation failed."); - addDebugMessage({ - source: "server", - type: "compilation_error", - data: JSON.stringify( + addDebugMessage( + "server", + "compilation_error", + JSON.stringify( { type: "compilation_error", data: data.errors }, null, 2, ), - protocol: "http", - }); - addDebugMessage({ - source: "server", - type: "compilation_status", - data: JSON.stringify({ gccStatus: "error" }, null, 2), - protocol: "http", - }); + "http", + ); + addDebugMessage( + "server", + "compilation_status", + JSON.stringify({ gccStatus: "error" }, null, 2), + "http", + ); } if (data.parserMessages && Array.isArray(data.parserMessages)) { @@ -326,7 +326,8 @@ export function useCompilation({ const handleCompileAndStart = useCallback(() => { if (!ensureBackendConnected("Simulation starten")) return; - setDebugMessages([]); +// clear debug messages via provider + setDebugMessages([]); let mainSketchCode: string = ""; if (editorRef.current) { diff --git a/client/src/hooks/use-simulation-controls.ts b/client/src/hooks/use-simulation-controls.ts index e7b43a5c..605cbb42 100644 --- a/client/src/hooks/use-simulation-controls.ts +++ b/client/src/hooks/use-simulation-controls.ts @@ -18,7 +18,6 @@ type UseSimulationControlsParams = { sendMessage: (message: any) => void; resetPinUI: (opts?: { keepDetected?: boolean }) => void; clearOutputs: () => void; - addDebugMessage: (params: DebugMessageParams) => void; serialEventQueueRef: MutableRefObject< Array<{ payload: any; receivedAt: number }> >; @@ -35,12 +34,13 @@ type UseSimulationControlsParams = { startSimulationRef: MutableRefObject<(() => void) | null>; }; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; + export function useSimulationControls({ ensureBackendConnected, sendMessage, resetPinUI, clearOutputs, - addDebugMessage, serialEventQueueRef, toast, pendingPinConflicts, @@ -50,6 +50,7 @@ export function useSimulationControls({ handleCompileAndStart, startSimulationRef, }: UseSimulationControlsParams) { + const { addDebugMessage } = useSimulationUi(); const [simulationStatus, setSimulationStatus] = useState( "stopped", ); @@ -62,12 +63,12 @@ export function useSimulationControls({ const stopMutation = useMutation({ mutationFn: async () => { - addDebugMessage({ - source: "frontend", - type: "stop_simulation", - data: JSON.stringify({ type: "stop_simulation" }, null, 2), - protocol: "websocket", - }); + addDebugMessage( + "frontend", + "stop_simulation", + JSON.stringify({ type: "stop_simulation" }, null, 2), + "websocket", + ); sendMessage({ type: "stop_simulation" }); return { success: true }; }, @@ -80,12 +81,12 @@ export function useSimulationControls({ const pauseMutation = useMutation({ mutationFn: async () => { - addDebugMessage({ - source: "frontend", - type: "pause_simulation", - data: JSON.stringify({ type: "pause_simulation" }, null, 2), - protocol: "websocket", - }); + addDebugMessage( + "frontend", + "pause_simulation", + JSON.stringify({ type: "pause_simulation" }, null, 2), + "websocket", + ); sendMessage({ type: "pause_simulation" }); return { success: true }; }, @@ -103,12 +104,12 @@ export function useSimulationControls({ const resumeMutation = useMutation({ mutationFn: async () => { - addDebugMessage({ - source: "frontend", - type: "resume_simulation", - data: JSON.stringify({ type: "resume_simulation" }, null, 2), - protocol: "websocket", - }); + addDebugMessage( + "frontend", + "resume_simulation", + JSON.stringify({ type: "resume_simulation" }, null, 2), + "websocket", + ); sendMessage({ type: "resume_simulation" }); return { success: true }; }, @@ -127,16 +128,16 @@ export function useSimulationControls({ const startMutation = useMutation({ mutationFn: async () => { resetPinUI({ keepDetected: true }); - addDebugMessage({ - source: "frontend", - type: "start_simulation", - data: JSON.stringify( + addDebugMessage( + "frontend", + "start_simulation", + JSON.stringify( { type: "start_simulation", timeout: simulationTimeout }, null, 2, ), - protocol: "websocket", - }); + "websocket", + ); sendMessage({ type: "start_simulation", timeout: simulationTimeout }); return { success: true }; }, diff --git a/client/src/hooks/use-simulation-ui.tsx b/client/src/hooks/use-simulation-ui.tsx new file mode 100644 index 00000000..240bd7a7 --- /dev/null +++ b/client/src/hooks/use-simulation-ui.tsx @@ -0,0 +1,186 @@ +import React, { createContext, useContext, useState, useRef } from "react"; + +import { useDebugConsole } from "@/hooks/use-debug-console"; +import { useOutputPanel } from "@/hooks/use-output-panel"; + +type SimulationStatus = "running" | "paused" | "stopped"; + +type DebugMessage = { + id: string; + timestamp: Date; + sender: "server" | "frontend"; + type: string; + content: string; + protocol?: "websocket" | "http"; +}; + +type SimulationUiContextType = { + simulationStatus: SimulationStatus; + txActivity: number; + rxActivity: number; + setTxActivity?: React.Dispatch>; + setRxActivity?: React.Dispatch>; + + // Serial I/O (optional — page provides) + serialOutput?: any[]; + renderedSerialOutput?: any[]; + serialViewMode?: "monitor" | "plotter" | "both"; + autoScrollEnabled?: boolean; + setAutoScrollEnabled?: (v: boolean) => void; + serialInputValue?: string; + setSerialInputValue?: (v: string) => void; + showSerialMonitor?: boolean; + showSerialPlotter?: boolean; + cycleSerialViewMode?: () => void; + clearSerialOutput?: () => void; + + // Output panel state (managed by provider) + activeOutputTab?: "compiler" | "messages" | "registry" | "debug"; + setActiveOutputTab?: (v: any) => void; + showCompilationOutput?: boolean; + setShowCompilationOutput?: (v: boolean) => void; + parserPanelDismissed?: boolean; + setParserPanelDismissed?: (v: boolean) => void; + parserMessagesContainerRef?: React.RefObject; + outputPanelRef?: any; + outputTabsHeaderRef?: React.RefObject; + outputPanelMinPercent?: number; + compilationPanelSize?: number; + setCompilationPanelSize?: (n: number) => void; + outputPanelManuallyResizedRef?: React.MutableRefObject; + openOutputPanel?: (tab: any) => void; + + // Debug console (provided by provider) + debugMode?: boolean; + setDebugMode?: (v: boolean) => void; + debugMessages?: DebugMessage[]; + setDebugMessages?: (msgs: DebugMessage[]) => void; + addDebugMessage?: (sender: "server" | "frontend", type: string, content: string, protocol?: "websocket" | "http") => void; + debugMessageFilter?: string; + setDebugMessageFilter?: (v: string) => void; + debugViewMode?: "table" | "tiles"; + setDebugViewMode?: (v: "table" | "tiles") => void; + debugMessagesContainerRef?: React.RefObject; +}; + +const SimulationUiContext = createContext(null); + +export const SimulationUiProvider = (props: React.PropsWithChildren & { + // data the provider needs from the page to drive output sizing + cliOutput?: string; + parserMessages?: any[]; + lastCompilationResult?: "success" | "error" | null; + hasCompilationErrors?: boolean; + code?: string; +}>) => { + const { children, cliOutput = "", parserMessages = [], lastCompilationResult = null, hasCompilationErrors = false, code = "", ...rest } = props as any; + + // Output panel state managed by provider so multiple components can consume it + const [activeOutputTab, setActiveOutputTab] = useState<"compiler" | "messages" | "registry" | "debug">("compiler"); + const [showCompilationOutput, setShowCompilationOutput] = useState(() => { + try { + const stored = typeof window !== "undefined" ? window.localStorage.getItem("unoShowCompileOutput") : null; + return stored === null ? true : stored === "1"; + } catch { + return true; + } + }); + const [parserPanelDismissed, setParserPanelDismissed] = useState(false); + const parserMessagesContainerRef = useRef(null); + + // Hook that manages the output panel sizing/behavior + const outputPanel = useOutputPanel( + Boolean(hasCompilationErrors), + cliOutput, + parserMessages, + lastCompilationResult ?? null, + parserMessagesContainerRef, + showCompilationOutput, + setShowCompilationOutput, + setParserPanelDismissed, + setActiveOutputTab, + code, + ); + + // Debug console is owned by the provider now so pages/components don't + // need to instantiate or forward debug state. We pass the current + // active output tab to the debug-hook so scroll/auto-open behavior + // continues to work as before. + const { + debugMode, + setDebugMode, + debugMessages, + setDebugMessages, + debugMessageFilter, + setDebugMessageFilter, + debugViewMode, + setDebugViewMode, + debugMessagesContainerRef, + addDebugMessage, + } = useDebugConsole(activeOutputTab); + + + const contextValue: SimulationUiContextType = { + // Spread any serial/debug values passed in by the page (keeps backward compat) + ...(rest as SimulationUiContextType), + + // Output panel + activeOutputTab, + setActiveOutputTab, + showCompilationOutput, + setShowCompilationOutput, + parserPanelDismissed, + setParserPanelDismissed, + parserMessagesContainerRef, + outputPanelRef: outputPanel.outputPanelRef, + outputTabsHeaderRef: outputPanel.outputTabsHeaderRef, + outputPanelMinPercent: outputPanel.outputPanelMinPercent, + compilationPanelSize: outputPanel.compilationPanelSize, + setCompilationPanelSize: outputPanel.setCompilationPanelSize, + outputPanelManuallyResizedRef: outputPanel.outputPanelManuallyResizedRef, + openOutputPanel: outputPanel.openOutputPanel, + + // debug console (managed by provider) + debugMode, + setDebugMode, + debugMessages, + setDebugMessages, + addDebugMessage, + debugMessageFilter, + setDebugMessageFilter, + debugViewMode, + setDebugViewMode, + debugMessagesContainerRef, + + // minimal tx/rx defaults (may be overridden by page props) + txActivity: (rest as any)?.txActivity ?? 0, + rxActivity: (rest as any)?.rxActivity ?? 0, + setTxActivity: (rest as any)?.setTxActivity, + setRxActivity: (rest as any)?.setRxActivity, + + // serial fields (if provided by page via props) + serialOutput: (rest as any)?.serialOutput, + renderedSerialOutput: (rest as any)?.renderedSerialOutput, + serialViewMode: (rest as any)?.serialViewMode, + autoScrollEnabled: (rest as any)?.autoScrollEnabled, + setAutoScrollEnabled: (rest as any)?.setAutoScrollEnabled, + serialInputValue: (rest as any)?.serialInputValue, + setSerialInputValue: (rest as any)?.setSerialInputValue, + showSerialMonitor: (rest as any)?.showSerialMonitor, + showSerialPlotter: (rest as any)?.showSerialPlotter, + cycleSerialViewMode: (rest as any)?.cycleSerialViewMode, + clearSerialOutput: (rest as any)?.clearSerialOutput, + } as SimulationUiContextType; + + return ( + {children} + ); +}; + +export function useSimulationUi() { + const ctx = useContext(SimulationUiContext); + if (!ctx) { + throw new Error("useSimulationUi must be used within a SimulationUiProvider"); + } + return ctx; +} diff --git a/client/src/hooks/useWebSocketHandler.ts b/client/src/hooks/useWebSocketHandler.ts index 39a38e10..398bcac6 100644 --- a/client/src/hooks/useWebSocketHandler.ts +++ b/client/src/hooks/useWebSocketHandler.ts @@ -16,7 +16,6 @@ type UseWebSocketHandlerParams = { simulationStatus: string; // callbacks / setters from parent scope - addDebugMessage: (source: "frontend" | "server", type: string, data: string, protocol?: "websocket" | "http") => void; setRxActivity: React.Dispatch>; appendSerialOutput: (text: string) => void; appendRenderedText: (text: string) => void; @@ -51,10 +50,11 @@ type UseWebSocketHandlerParams = { setParserMessages: React.Dispatch>; }; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; + export function useWebSocketHandler(params: UseWebSocketHandlerParams) { const { simulationStatus, - addDebugMessage, setRxActivity, appendSerialOutput, appendRenderedText, @@ -84,6 +84,9 @@ export function useWebSocketHandler(params: UseWebSocketHandlerParams) { setParserMessages, } = params; + // get debug function from UI context + const { addDebugMessage } = useSimulationUi(); + const { isConnected, messageQueue, diff --git a/client/src/lib/debugBridge.ts b/client/src/lib/debugBridge.ts new file mode 100644 index 00000000..e69de29b diff --git a/client/src/pages/arduino-simulator.tsx b/client/src/pages/arduino-simulator.tsx index 75ea37e9..7b42f1f9 100644 --- a/client/src/pages/arduino-simulator.tsx +++ b/client/src/pages/arduino-simulator.tsx @@ -19,14 +19,10 @@ import { BarChart, Monitor, Columns, - X, - Table, - LayoutGrid, } from "lucide-react"; import { InputGroup } from "@/components/ui/input-group"; import { clsx } from "clsx"; import { Button } from "@/components/ui/button"; -import { ScrollArea } from "@/components/ui/scroll-area"; import { CodeEditor } from "@/components/features/code-editor"; import { SerialMonitor } from "@/components/features/serial-monitor"; import { CompilationOutput } from "@/components/features/compilation-output"; @@ -35,7 +31,8 @@ import { SketchTabs } from "@/components/features/sketch-tabs"; import { ExamplesMenu } from "@/components/features/examples-menu"; import { AppHeader } from "@/components/features/app-header"; import { SimCockpit } from "@/components/features/sim-cockpit"; -import SimulatorSidebar from "@/components/features/simulator/SimulatorSidebar"; +import SimulatorSidebar, { SimulatorOutput } from "@/components/features/simulator/SimulatorSidebar"; +import SimulatorOutputPanel from "@/components/features/simulator/SimulatorOutputPanel"; import { useWebSocket } from "@/hooks/use-websocket"; import { useWebSocketHandler } from "@/hooks/useWebSocketHandler"; import { useCompilation } from "@/hooks/use-compilation"; @@ -44,12 +41,15 @@ import { usePinState } from "@/hooks/use-pin-state"; import { useToast } from "@/hooks/use-toast"; import { useBackendHealth } from "@/hooks/use-backend-health"; import { useMobileLayout } from "@/hooks/use-mobile-layout"; -import { useDebugConsole } from "@/hooks/use-debug-console"; + import { useDebugMode } from "@/hooks/use-debug-mode-store"; import { useSketchTabs } from "@/hooks/use-sketch-tabs"; import { useSerialIO } from "@/hooks/use-serial-io"; import { useOutputPanel } from "@/hooks/use-output-panel"; import { useSimulationStore } from "@/hooks/use-simulation-store"; +import { SimulationUiProvider, useSimulationUi } from "@/hooks/use-simulation-ui"; +import SimulatorHeader from "@/components/features/simulator/SimulatorHeader"; + import { useSketchAnalysis } from "@/hooks/use-sketch-analysis"; import { useTelemetryStore } from "@/hooks/use-telemetry-store"; import { useFileManager } from "@/hooks/use-file-manager"; @@ -85,7 +85,7 @@ const LoadingPlaceholder = () => ( import { Logger } from "@shared/logger"; const logger = new Logger("ArduinoSimulator"); -export default function ArduinoSimulator() { +function ArduinoSimulatorInner() { const [currentSketch, setCurrentSketch] = useState(null); const [code, setCode] = useState(""); const editorRef = useRef<{ getValue: () => string } | null>(null); @@ -120,20 +120,6 @@ export default function ArduinoSimulator() { // Track if user manually dismissed the parser panel (reset on new compile with messages) const [parserPanelDismissed, setParserPanelDismissed] = useState(false); - // Initialize I/O Registry with all 20 Arduino pins (will be populated at runtime) - const [ioRegistry, setIoRegistry] = useState(() => { - const pins: IOPinRecord[] = []; - // Digital pins 0-13 - for (let i = 0; i <= 13; i++) { - pins.push({ pin: String(i), defined: false, usedAt: [] }); - } - // Analog pins A0-A5 - for (let i = 0; i <= 5; i++) { - pins.push({ pin: `A${i}`, defined: false, usedAt: [] }); - } - return pins; - }); - const [activeOutputTab, setActiveOutputTab] = useState< "compiler" | "messages" | "registry" | "debug" >("compiler"); @@ -147,6 +133,25 @@ export default function ArduinoSimulator() { } }, ); + + // --- Debug proxies (provider owns the real state) + // Hooks below will emit debug messages via the bridge (see `debugBridge`). + + + + // Initialize I/O Registry with all 20 Arduino pins (will be populated at runtime) + const [ioRegistry, setIoRegistry] = useState(() => { + const pins: IOPinRecord[] = []; + // Digital pins 0-13 + for (let i = 0; i <= 13; i++) { + pins.push({ pin: String(i), defined: false, usedAt: [] }); + } + // Analog pins A0-A5 + for (let i = 0; i <= 5; i++) { + pins.push({ pin: `A${i}`, defined: false, usedAt: [] }); + } + return pins; + }); const [isModified, setIsModified] = useState(false); const { @@ -180,20 +185,6 @@ export default function ArduinoSimulator() { // File manager hook — instantiated after `handleFilesLoaded` to avoid TDZ (see below) - // Debug console state and functions - const { - debugMode, - setDebugMode: _setDebugMode, - debugMessages, - setDebugMessages, - debugMessageFilter, - setDebugMessageFilter, - debugViewMode, - setDebugViewMode, - debugMessagesContainerRef, - addDebugMessage, - } = useDebugConsole(activeOutputTab); - void _setDebugMode; // Mark as intentionally unused (managed by hook) // Subscribe to telemetry updates (to re-render when metrics change) const telemetryData = useTelemetryStore(); @@ -335,14 +326,6 @@ export default function ArduinoSimulator() { setIoRegistry, setHasCompiledOnce: setHasCompiledOnceProxy, setIsModified, - setDebugMessages, - addDebugMessage: (params) => - addDebugMessage( - params.source, - params.type, - params.data, - params.protocol, - ), ensureBackendConnected, isBackendUnreachableError, triggerErrorGlitch, @@ -350,6 +333,28 @@ export default function ArduinoSimulator() { startSimulation, }); + // Output panel refs / sizing (kept here for AppHeader & legacy `SimulatorOutput` props) + const { + outputPanelRef, + outputTabsHeaderRef, + outputPanelMinPercent, + compilationPanelSize, + setCompilationPanelSize, + outputPanelManuallyResizedRef, + openOutputPanel, + } = useOutputPanel( + Boolean(hasCompilationErrors), + cliOutput, + parserMessages, + lastCompilationResult, + parserMessagesContainerRef, + showCompilationOutput, + setShowCompilationOutput, + setParserPanelDismissed, + setActiveOutputTab, + code, + ); + const { simulationStatus, setSimulationStatus, @@ -369,13 +374,6 @@ export default function ArduinoSimulator() { sendMessage, resetPinUI, clearOutputs, - addDebugMessage: (params) => - addDebugMessage( - params.source, - params.type, - params.data, - params.protocol, - ), serialEventQueueRef, toast, pendingPinConflicts, @@ -402,45 +400,12 @@ export default function ArduinoSimulator() { hasCompilationErrors, }); - // Output panel sizing and management - const { - outputPanelRef, - outputTabsHeaderRef, - compilationPanelSize, - setCompilationPanelSize, - outputPanelMinPercent, - outputPanelManuallyResizedRef, - openOutputPanel, - } = useOutputPanel( - hasCompilationErrors, - cliOutput, - parserMessages, - lastCompilationResult, - parserMessagesContainerRef, - showCompilationOutput, - setShowCompilationOutput, - setParserPanelDismissed, - setActiveOutputTab, - code, - ); + // Output panel sizing/logic and activeOutputTab are now owned by + // `SimulationUiProvider` (consumed by `useSimulationUi()`). This removes + // prop drilling and centralizes output behavior. + - // Auto-switch output tab based on errors and messages - useEffect(() => { - if (hasCompilationErrors) { - setActiveOutputTab("compiler"); - } else if (parserMessages.length > 0 && !parserPanelDismissed) { - setActiveOutputTab("messages"); - } - }, [hasCompilationErrors, parserMessages.length, parserPanelDismissed]); - // Auto-scroll debug console to latest message - useEffect(() => { - if (activeOutputTab === "debug" && debugMessagesContainerRef.current) { - requestAnimationFrame(() => { - debugMessagesContainerRef.current?.scrollTo(0, debugMessagesContainerRef.current.scrollHeight); - }); - } - }, [debugMessages, activeOutputTab]); // Fetch default sketch const { data: sketches } = useQuery({ @@ -703,7 +668,6 @@ export default function ArduinoSimulator() { // WebSocket message handling moved to `useWebSocketHandler` (extracted for better separation of concerns) useWebSocketHandler({ simulationStatus, - addDebugMessage, setRxActivity, appendSerialOutput, appendRenderedText, @@ -1191,21 +1155,48 @@ export default function ArduinoSimulator() { void stopDisabled; void buttonsClassName; + function HeaderRightSlot() { + const { debugMode } = useSimulationUi(); + return ( +
+ { if (!compileMutation.isPending) handleCompile(); }} + onCompileAndStart={handleCompileAndStart} + board={board} + /> + {debugMode ? : null} +
+ ); + } + return (
- {/* Glitch overlay when compilation fails */} - {showErrorGlitch && ( -
- {/* Single red border flash */} -
-
-
-
-
+ {/* Glitch overlay when compilation fails */} + {showErrorGlitch && ( +
+ {/* Single red border flash */} +
+
+
+
+