diff --git a/src/bundles/robot_simulation/src/__tests__/index.test.ts b/src/bundles/robot_simulation/src/__tests__/index.test.ts index f8c30b04f3..734bf42544 100644 --- a/src/bundles/robot_simulation/src/__tests__/index.test.ts +++ b/src/bundles/robot_simulation/src/__tests__/index.test.ts @@ -145,15 +145,15 @@ describe(RobotSimulationModulePlugin, () => { }); }); - describe('run_robot_code', () => { + describe('running code from the tab ($runReplCode)', () => { test('drives the shared robot Python context across repeated calls, sharing state between runs', async () => { const { plugin } = makePlugin(); await runAsyncGenerator((plugin as any).init_default_simulation()); - await runAsyncGenerator((plugin as any).run_robot_code(stringValue('x = 1'))); + (plugin as any).__runReplCode('x = 1'); const firstProgram = (plugin as any).__state.replProgram; - await runAsyncGenerator((plugin as any).run_robot_code(stringValue('y = x + 1'))); + (plugin as any).__runReplCode('y = x + 1'); const secondProgram = (plugin as any).__state.replProgram; // A fresh Program per call... @@ -165,11 +165,9 @@ describe(RobotSimulationModulePlugin, () => { expect((plugin as any).__state.replPyContext).toBeDefined(); }); - test('throws if the world has not been initialised yet', async () => { + test('throws if the world has not been initialised yet', () => { const { plugin } = makePlugin(); - await expect( - runAsyncGenerator((plugin as any).run_robot_code(stringValue('ev3_pause(1)'))) - ).rejects.toThrow(); + expect(() => (plugin as any).__runReplCode('ev3_pause(1)')).toThrow(); }); test('ev3_pause() pauses the currently-running Program, not a stale one from an earlier run', async () => { @@ -179,13 +177,13 @@ describe(RobotSimulationModulePlugin, () => { // First run: a Program that finishes immediately and is left behind, stopped, in // world.controllers.controllers - exactly what a real student's first REPL/embedded-editor // Run leaves behind once they move on to a second one. - await runAsyncGenerator((plugin as any).run_robot_code(stringValue('x = 1'))); + (plugin as any).__runReplCode('x = 1'); const firstProgram = (plugin as any).__state.replProgram; // Second run calls ev3_pause() itself - if ev3_pause found the *first* Program with a // matching name (the bug this guards against), it would pause a dead, already-stopped // Program that no longer affects anything, leaving this run's own isPaused false forever. - await runAsyncGenerator((plugin as any).run_robot_code(stringValue('ev3_pause(1000000)'))); + (plugin as any).__runReplCode('ev3_pause(1000000)'); const secondProgram = (plugin as any).__state.replProgram; // Drive the second run's Python code far enough to actually execute the ev3_pause() call @@ -200,4 +198,54 @@ describe(RobotSimulationModulePlugin, () => { expect(firstProgram.isPaused).toBe(false); }); }); + + describe('createPythonCSE', () => { + test('sends the control program text to the tab so its editor shows what is running', async () => { + const { plugin, stateChannel } = makePlugin(); + await runAsyncGenerator((plugin as any).createPythonCSE(stringValue('x = 1'))); + + expect(stateChannel.send).toHaveBeenCalledWith({ kind: 'editor-code', code: 'x = 1' }); + }); + + test('replays the program text to a tab that connects afterwards', async () => { + const { plugin, stateChannel } = makePlugin(); + await runAsyncGenerator((plugin as any).createPythonCSE(stringValue('x = 2'))); + stateChannel.send.mockClear(); + + const onMessage = stateChannel.subscribe.mock.calls[0][0]; + onMessage({ kind: 'request-replay' }); + + expect(stateChannel.send).toHaveBeenCalledWith({ kind: 'editor-code', code: 'x = 2' }); + }); + + test('running code from the tab afterwards replaces the program it made', async () => { + const { plugin } = makePlugin(); + await runAsyncGenerator((plugin as any).init_default_simulation()); + await runAsyncGenerator((plugin as any).createPythonCSE(stringValue('x = 1'))); + const program = (plugin as any).__setupProgram; + expect(program.isStopped).toBe(false); + + (plugin as any).__runReplCode('y = 1'); + + expect(program.isStopped).toBe(true); + }); + + test('rejects a second call, since a robot runs one program at a time', async () => { + const { plugin, stateChannel } = makePlugin(); + await runAsyncGenerator((plugin as any).createPythonCSE(stringValue('x = 1'))); + stateChannel.send.mockClear(); + + await expect(runAsyncGenerator((plugin as any).createPythonCSE(stringValue('x = 2')))).rejects.toThrow(); + expect(stateChannel.send).not.toHaveBeenCalled(); + }); + + test('sends nothing before createPythonCSE is called', () => { + const { stateChannel } = makePlugin(); + const onMessage = stateChannel.subscribe.mock.calls[0][0]; + stateChannel.send.mockClear(); + onMessage({ kind: 'request-replay' }); + + expect(stateChannel.send).not.toHaveBeenCalledWith(expect.objectContaining({ kind: 'editor-code' })); + }); + }); }); diff --git a/src/bundles/robot_simulation/src/controllers/program/Program.ts b/src/bundles/robot_simulation/src/controllers/program/Program.ts index 1cc39e47eb..5520542a73 100644 --- a/src/bundles/robot_simulation/src/controllers/program/Program.ts +++ b/src/bundles/robot_simulation/src/controllers/program/Program.ts @@ -50,7 +50,7 @@ export class Program implements Controller { * stashed here and re-thrown from the *next* `fixedUpdate` call instead, so it still surfaces to (and is convertible by) the same call site a synchronous evaluator would throw from. */ private pendingError: unknown = null; - /** Set by `stop()` when a newer REPL run replaces this one - see `run_robot_code` (index.ts). + /** Set by `stop()` when a newer REPL run replaces this one - see `__runReplCode` (index.ts). * Prevents this Program from pumping its (now-superseded) generator any further; several * `Program`s can share one `pyContext` across REPL re-runs (that's how variables persist between * runs), and `runPythonECEvaluator` reassigns `context.control`/`context.stash` at the *start* of diff --git a/src/bundles/robot_simulation/src/engine/World.ts b/src/bundles/robot_simulation/src/engine/World.ts index 2f21ebe7ad..ceb43ff508 100644 --- a/src/bundles/robot_simulation/src/engine/World.ts +++ b/src/bundles/robot_simulation/src/engine/World.ts @@ -73,8 +73,8 @@ export class World extends TypedEventTarget { * `addController`'s `start()` hookup only fires on the `worldStart` event, which for a live * world already happened once inside `init()`, so a controller added afterwards would never get * its `start()` called and would throw the first time it ticks (e.g. `Program.fixedUpdate`'s - * "Program not started"). Used by `run_robot_code` to add a fresh `Program` controller each time - * the REPL tab's registered evaluator runs, without restarting the whole world. + * "Program not started"). Used by `__runReplCode` (index.ts) to add a fresh `Program` controller each time + * the embedded editor's Run is pressed, without restarting the whole world. */ addLiveController(...controllers: Controller[]) { this.addController(...controllers); diff --git a/src/bundles/robot_simulation/src/ev3_functions.ts b/src/bundles/robot_simulation/src/ev3_functions.ts index 3005784134..4ca936ed7d 100644 --- a/src/bundles/robot_simulation/src/ev3_functions.ts +++ b/src/bundles/robot_simulation/src/ev3_functions.ts @@ -45,7 +45,7 @@ export function createEv3Functions(deps: { /** * Pauses for a period of time. * - * Every `run_robot_code`/`$runReplCode` call (each REPL/embedded-editor Run) adds a fresh + * Every `$runReplCode` call (each REPL/embedded-editor Run) adds a fresh * `Program` controller rather than replacing one in place - see `Program`'s doc comment - so * a World that has had more than one control-program run can have several `Program`s in * `world.controllers.controllers` at once, all sharing the same `name` diff --git a/src/bundles/robot_simulation/src/index.ts b/src/bundles/robot_simulation/src/index.ts index cd21090365..239f3c4526 100644 --- a/src/bundles/robot_simulation/src/index.ts +++ b/src/bundles/robot_simulation/src/index.ts @@ -15,16 +15,16 @@ * * `from robot_simulation import ...` still only works for the *setup* program, not the robot's own * control program: whether that code arrives as a string literal (`createPythonCSE`) or from the - * `repl` tab (`run_robot_code`), it's evaluated by a private, hand-built py-slang `Context` (see + * RobotSimulation tab's embedded editor, it's evaluated by a private, hand-built py-slang `Context` (see * controllers/program/pythonRuntime.ts) stepped in lockstep with the physics tick, entirely * separate from Conductor's own evaluator/module-loading machinery - there is no * `ModuleLoaderRunnerPlugin` inside that shadow context for an `import` to resolve through. The * `ev3_*` API is available to it directly by name instead (no import). * - * The recommended student-facing shape is: `init_default_simulation()` + `add_wall`/`add_paper` - * calls in the main pane (one-time scene setup), then `set_evaluator(run_robot_code)` (from the - * `repl` module) to hand the robot's own code to a separate, rerunnable REPL tab - see - * `run_robot_code`'s doc comment. + * The robot's own code can be supplied two ways: `addControllerToWorld(createPythonCSE(code), world)` + * in a setup built with `init_simulation` (the code runs as soon as the simulation starts, and is + * shown in the tab's embedded editor), or typed into that embedded editor and run with its Run + * button - the way to go after `init_default_simulation()`, which takes no control program. * * @module robot_simulation * @author Joel Chan @@ -92,7 +92,6 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { 'add_wall', 'add_paper', 'add_color_patch', - 'run_robot_code', 'ev3_motorA', 'ev3_motorB', 'ev3_motorC', @@ -113,6 +112,12 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { private readonly __sceneRegistry = new SceneRegistry(); private readonly __ev3Fns: Ev3Functions; private __tabLoaded = false; + /** The latest control program text `createPythonCSE` was given, replayed to a tab that connects + after setup ran - see EditorCodeMessage. */ + private __editorCode: string | undefined; + /** The one `Program` `createPythonCSE` may make - stopped by `__runReplCode` so that running code + from the tab's editor replaces it rather than driving the robot alongside it. */ + private __setupProgram: Program | undefined; /** What `saveToContext`/the `ev3_*` API read/write - one plugin instance per run, so this replaces the pre-migration `context.moduleContexts.robot_simulation.state`. */ @@ -136,9 +141,9 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { this.__runReplCode(code); } catch (error) { // No live World yet (e.g. the embedded editor's Run button was clicked before the main - // program ran) - there is no evaluator/caller boundary here to surface this to, unlike - // run_robot_code's own throw (see its doc comment). The World-state readout already - // shown in this same tab makes "nothing is running yet" obvious without one. + // program ran) - there is no evaluator/caller boundary here to surface this to. The + // World-state readout already shown in this same tab makes "nothing is running yet" + // obvious without one. console.warn('robot_simulation: could not run code from the RobotSimulation tab\'s embedded editor', error); } }, @@ -155,6 +160,9 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { this.__stateChannel.subscribe(message => { if (message.kind === 'request-replay') { this.__sceneRegistry.replaySpawns(); + if (this.__editorCode !== undefined) { + this.__stateChannel.send({ kind: 'editor-code', code: this.__editorCode }); + } } }); } @@ -417,12 +425,25 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { * whole `ev3_*` API directly by name; no `import` is needed (and none is possible - see * pythonRuntime.ts). `print(...)` goes to the simulation's Robot Console panel. * + * A robot runs one program at a time, so this may only be called once per simulation - a second + * call throws rather than leaving two programs driving the same motors. + * + * Also shows `code` in the RobotSimulation tab's embedded editor (overwriting whatever it held), + * so the editor shows what is running. Running code from that editor afterwards replaces this + * program. + * * @param code The robot's control program, written in Python (SICPy §4). * @category Control Program */ async* createPythonCSE(code: TypedValue): AsyncGenerator, undefined> { + if (this.__setupProgram !== undefined) { + throw new EvaluatorRuntimeError('createPythonCSE: a robot runs one program at a time, so it can only be called once per simulation'); + } const pyContext = createRobotPythonContext(this.__ev3Fns, () => this.__getWorldFromContext()); const program = new Program(code.value, undefined, pyContext); + this.__setupProgram = program; + this.__editorCode = code.value; + this.__stateChannel.send({ kind: 'editor-code', code: code.value }); return await this.evaluator.opaque_make(program, true); } @@ -487,9 +508,9 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { /** * The boilerplate-free alternative to `init_simulation`: builds default physics, a default * world, a default floor and a default EV3 (the same defaults `createPhysics`/`createFloor`/ - * `createEv3` use), and starts the simulation - all in one call. Takes no control program: pair - * this with `run_robot_code`/the `repl` module (see that method's doc comment) to drive the EV3 - * from a separate, rerunnable REPL tab instead of a control-code string baked into setup. + * `createEv3` use), and starts the simulation - all in one call. Takes no control program: drive + * the EV3 by typing code into the RobotSimulation tab's embedded editor and pressing Run, instead + * of a control-code string baked into setup. * * For a customised World (non-default physics/gravity, a control program written in * Source/Scheme instead of Python), use `createPhysics`/`createWorld`/`createWall`/ @@ -632,49 +653,19 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { } /** - * The `repl`-module hook: pass this function itself to `repl`'s `set_evaluator`, and the `repl` - * tab it opens will call it with whatever the student typed there each time they hit Run - - * `code` is exactly what `createPythonCSE`/`init_default_simulation`'s old `control_code` - * argument used to be, just supplied interactively instead of baked into the setup program. + * Runs `code` as the robot's control program - the `$runReplCode` handler (the RobotSimulation + * tab's own embedded mini-editor - see protocol.ts's doc comment on `RobotSimulationModuleRpc`). * * Each call adds a fresh `Program` controller (via `World.addLiveController`, since the world is * already running by this point) rather than editing one in place, but all calls share one * `pyContext` (lazily created on the first call, cached in `__state`) - `runPythonECEvaluator` * re-analyzes each run against that same context's global environment (see evaluate.ts), so - * variables and function defs a student's REPL code creates in one run are still visible in the - * next, the way a REPL is expected to behave. The previous run's `Program` is `stop()`'d first so - * it can't keep pumping its now-superseded generator against the same shared `pyContext` (see - * `Program.stop`'s doc comment). + * variables and function defs one run creates are still visible in the next, the way a REPL is + * expected to behave. The previous run's `Program`, and the one `createPythonCSE` made, are + * `stop()`'d first so they can't keep driving the robot (see `Program.stop`'s doc comment). * - * Requires `init_default_simulation`/`init_simulation` to have already been called - there must - * be a live World for the robot code to act on. - * - * Also calls `$focusTab` on success, asking the RobotSimulation tab to bring itself to the - * front - but the frontend's side-content host only actually honours that the *first* time any - * tab is shown in a session (see `SideContentManager.showTab`'s "don't yank the student away from - * wherever they navigated" guard), so in practice this rarely does anything once the student has - * looked at any tab at all. A student who wants the 3D view and the code they're driving the - * robot with on screen *together*, without fighting that guard, should use the RobotSimulation - * tab's own embedded editor instead (`$runReplCode` in protocol.ts) - same effect as this - * function, just triggered from inside the tab that's already showing the 3D view, so there's - * nothing to focus/switch away from in the first place. - * @param code The robot's control program, written in Python (SICPy §4). - * @category Control Program - */ - async* run_robot_code( - code: TypedValue - ): AsyncGenerator, undefined> { - this.__runReplCode(code.value); - return { type: DataType.VOID, value: undefined }; - } - - /** - * Shared by `run_robot_code` (the `repl`-module hook, above) and `$runReplCode` (the - * RobotSimulation tab's own embedded mini-editor - see protocol.ts's doc comment on - * `RobotSimulationModuleRpc`) - same effect either way, just reached from two different callers. - * Throws (via `__getWorldFromContext`) if no World exists yet; `run_robot_code` lets that - * propagate (repl displays it as an error), while the `$runReplCode` RPC handler catches and logs - * it instead, since there is no evaluator/caller boundary there to surface a throw to. + * Throws (via `__getWorldFromContext`) if no World exists yet; the `$runReplCode` RPC handler + * catches and logs it, since there is no evaluator/caller boundary there to surface a throw to. */ private __runReplCode(code: string): void { const world = this.__getWorldFromContext(); @@ -685,12 +676,11 @@ export default class RobotSimulationModulePlugin extends BaseModulePlugin { const pyContext = this.__state.replPyContext as ReturnType; (this.__state.replProgram as Program | undefined)?.stop(); + this.__setupProgram?.stop(); const program = new Program(code, undefined, pyContext); world.addLiveController(program); this.__state.replProgram = program; - - this.__tabRpc.$focusTab(); } // [EV3] @@ -804,7 +794,6 @@ attachModuleMethod(RobotSimulationModulePlugin, 'init_default_simulation', [], D attachModuleMethod(RobotSimulationModulePlugin, 'add_wall', [DataType.NUMBER, DataType.NUMBER, DataType.NUMBER, DataType.NUMBER, DataType.NUMBER], DataType.VOID); attachModuleMethod(RobotSimulationModulePlugin, 'add_paper', [DataType.CONST_STRING, DataType.NUMBER, DataType.NUMBER, DataType.NUMBER, DataType.NUMBER, DataType.NUMBER], DataType.VOID); attachModuleMethod(RobotSimulationModulePlugin, 'add_color_patch', [DataType.CONST_STRING, DataType.NUMBER, DataType.NUMBER, DataType.NUMBER, DataType.NUMBER], DataType.VOID); -attachModuleMethod(RobotSimulationModulePlugin, 'run_robot_code', [DataType.CONST_STRING], DataType.VOID); attachModuleMethod(RobotSimulationModulePlugin, 'ev3_motorA', [], DataType.OPAQUE); attachModuleMethod(RobotSimulationModulePlugin, 'ev3_motorB', [], DataType.OPAQUE); attachModuleMethod(RobotSimulationModulePlugin, 'ev3_motorC', [], DataType.OPAQUE); diff --git a/src/bundles/robot_simulation/src/protocol.ts b/src/bundles/robot_simulation/src/protocol.ts index b2c3e5f927..10efb038b7 100644 --- a/src/bundles/robot_simulation/src/protocol.ts +++ b/src/bundles/robot_simulation/src/protocol.ts @@ -55,13 +55,24 @@ export interface StateSnapshotMessage { buffer: ArrayBuffer; } +/** + * Module -> tab: the source text of the robot's control program, sent by `createPythonCSE` so the + * tab's embedded editor shows what is actually running. Overwrites whatever the editor held + * (including text it restored from `localStorage`). The module remembers the latest one and + * replays it on `request-replay`, like entity spawns, for a tab that connects after setup ran. + */ +export interface EditorCodeMessage { + kind: 'editor-code'; + code: string; +} + /** Tab -> module: replay every entity spawned so far (a tab that just mounted / reconnected), mirrors csg/rune's `{ type: 'request' }`. */ export interface RequestReplayMessage { kind: 'request-replay'; } -export type StateChannelMessage = EntitySpawnedMessage | StateSnapshotMessage | RequestReplayMessage; +export type StateChannelMessage = EntitySpawnedMessage | StateSnapshotMessage | EditorCodeMessage | RequestReplayMessage; export type WorldStateName = 'unintialized' | 'loading' | 'ready' | 'running' | 'error'; @@ -81,10 +92,6 @@ export interface RobotSimulationTabRpc { $consoleLog(message: string, level: 'error' | 'source'): void; $worldStateChanged(state: WorldStateName): void; $sensorSnapshot(snapshot: SensorSnapshot): void; - /** Brings the RobotSimulation tab to the front - sent once `run_robot_code` successfully hands a - * fresh run to the physics loop, so a student driving the robot from the `repl` tab lands back - on the 3D view to watch it, without having to switch tabs manually. */ - $focusTab(): void; } /** @@ -98,8 +105,8 @@ export interface RobotSimulationTabRpc { * render whatever a plugin wants, including its own split "3D view + code editor" layout in one. */ export interface RobotSimulationModuleRpc { - /** Same effect as `run_robot_code` (see index.ts) - runs `code` as the robot's control program - * against the shared REPL `pyContext`, replacing whatever the previous run left ticking. Silently + /** Runs `code` as the robot's control program against the shared REPL `pyContext`, replacing + * whatever the previous run (or the `createPythonCSE` program) left ticking. Silently * a no-op (logged, not thrown - there is no caller/evaluator boundary here to catch or display a * throw) if no World exists yet (the embedded editor's Run button was clicked before the main program set one up). */ diff --git a/src/tabs/RobotSimulation/src/index.tsx b/src/tabs/RobotSimulation/src/index.tsx index 0bd942a3a6..f4722e1641 100644 --- a/src/tabs/RobotSimulation/src/index.tsx +++ b/src/tabs/RobotSimulation/src/index.tsx @@ -28,6 +28,11 @@ interface ViewState { worldState: WorldStateName; sensors: SensorSnapshot | undefined; logs: readonly LogEntry[]; + /** + * Control-program text pushed by the module (`createPythonCSE`); `version` bumps on every push + * so the embedded editor can tell a repeat of identical text from no update. + */ + presetCode: { code: string, version: number } | undefined; } const MAX_LOGS = 200; @@ -131,6 +136,7 @@ export default class RobotSimulationTabPlugin implements IPlugin, RobotSimulatio worldState: 'unintialized', sensors: undefined, logs: [], + presetCode: undefined, }; constructor(_conduit: IConduit, [controlChannel, stateChannel]: IChannel[], tabService: ITabService) { @@ -153,6 +159,8 @@ export default class RobotSimulationTabPlugin implements IPlugin, RobotSimulatio this.__spawnEntity(message.id, message.descriptor); } else if (message.kind === 'state-snapshot') { this.__applySnapshot(message.buffer); + } else if (message.kind === 'editor-code') { + this.__presetEditorCode(message.code); } }); // A tab that mounts after the module has already spawned entities needs the backlog replayed @@ -205,6 +213,20 @@ export default class RobotSimulationTabPlugin implements IPlugin, RobotSimulatio this.__emit(); } + /** + * The module's control program overwrites whatever the embedded editor held, including text + * it would restore from `localStorage` - so the cache is written here, not only by the editor, + * in case the editor has not mounted yet. + */ + private __presetEditorCode(code: string): void { + try { + window.localStorage.setItem(EMBEDDED_EDITOR_CODE_STORAGE_KEY, code); + } catch { + // Best-effort, like the editor's own saves. + } + this.__setState({ presetCode: { code, version: (this.__state.presetCode?.version ?? 0) + 1 } }); + } + private __spawnEntity(id: number, descriptor: EntityDescriptor): void { if (this.__entities.has(id)) return; @@ -443,16 +465,10 @@ export default class RobotSimulationTabPlugin implements IPlugin, RobotSimulatio this.__setState({ sensors: snapshot }); } - $focusTab(): void { - this.__tabService.showTab(ROBOT_SIMULATION_TAB_ID); - } - /** * Called by the embedded mini-editor's Run button (see `RobotSimulationView_`) - sends the - * student's typed code to the module's `$runReplCode` (same effect as `run_robot_code`/the - * `repl` module's Run button, just from an editor that lives right next to the 3D view instead of - * a separate tab - see protocol.ts's doc comment on `RobotSimulationModuleRpc` for why this - * exists at all). Fire-and-forget: there's no return value to wait on, and any problem running + * student's typed code to the module's `$runReplCode` (see protocol.ts's doc comment on + * `RobotSimulationModuleRpc` for why the editor lives right next to the 3D view). Fire-and-forget: there's no return value to wait on, and any problem running * the code shows up in this same tab's own Robot Console (routed via `$consoleLog`) rather than coming back through this call. */ @@ -522,7 +538,7 @@ function RobotSimulationView_({ ))} - + ); } @@ -538,7 +554,11 @@ function RobotSimulationView_({ * output history, background image, custom font size, `set_program_text` support, ...) - just * enough to type and run code, since output already has a home in this same tab's Robot Console. */ -function EmbeddedReplEditor({ onRunCode, height }: { onRunCode: (code: string) => void, height: number }) { +function EmbeddedReplEditor({ onRunCode, preset, height }: { + onRunCode: (code: string) => void; + preset: ViewState['presetCode']; + height: number; +}) { const [code, setCode] = useState(() => { try { return window.localStorage.getItem(EMBEDDED_EDITOR_CODE_STORAGE_KEY) ?? EMBEDDED_EDITOR_PLACEHOLDER; @@ -547,6 +567,10 @@ function EmbeddedReplEditor({ onRunCode, height }: { onRunCode: (code: string) = } }); + useEffect(() => { + if (preset !== undefined) setCode(preset.code); + }, [preset]); + const handleChange = (value: string) => { setCode(value); try {