Skip to content

Migrate repl module to Conductor - #865

Merged
martin-henz merged 4 commits into
masterfrom
conductor-migration-repl
Aug 2, 2026
Merged

Migrate repl module to Conductor#865
martin-henz merged 4 commits into
masterfrom
conductor-migration-repl

Conversation

@martin-henz

Copy link
Copy Markdown
Member

Summary

  • Migrates the repl bundle (src/bundles/repl) from the legacy js-slang module pipeline to Conductor's BaseModulePlugin/channel protocol, following the same pattern as the matrix/sound/pix_n_flix/csg migrations.
  • Rewrites the Repl tab (src/tabs/Repl) from the legacy defineTab/getModuleState API to a Conductor IPlugin that subscribes to the repl channel, replays/relays output and program-text messages, and drives the AceEditor UI (Run button, draggable editor height, plaintext/rich-text output history).
  • default_js_slang (previously ran Repl input directly through js-slang, reusing the enclosing program's context) is no longer supported under Conductor — js-slang* imports resolve to undefined at runtime in a Conductor bundle's Worker, the same problem csg hit. The function is kept as an exported symbol (so set_evaluator(default_js_slang) still type-checks) but now throws a clear error directing the caller to write their own evaluator via set_evaluator.
  • Adds unit tests for the bundle's pure logic (rich_display.ts's XSS filtering/colour validation/nested rich-text parsing, stringify_value.ts's value stringification and recursion guard) and for the tab's channel wiring (backlog replay, localStorage caching, run dispatch). A test that imports the bundle's decorated index.ts directly isn't possible: vitest's esbuild-based SSR transform only lowers native (TC39 stage-3) decorators via its bundling API, not the single-file transform it actually uses to load modules, so any file with an @moduleMethod-decorated async generator method throws a raw SyntaxError on import. This is a pre-existing, already-documented limitation (see midi/src/conductorAdapters.ts) that applies equally to sound/matrix/pix_n_flix — none of them test their decorated index.ts directly either.

Test plan

  • tsc, lint, and vitest all pass for both src/bundles/repl and src/tabs/Repl
  • buildtools build (the real production pipeline, not just tsc) succeeds for both packages
  • Manually verified end-to-end locally via the modules dev server + a local frontend build (Conductor mode enabled): the Repl tab lazily loads on first repl_display()/rich_repl_display()/run, the editor → Run → evaluator-closure → output round trip works, the "no evaluator registered" warning renders as rich text, a thrown evaluator error renders in red, and the editor's cached program text persists across separate Runs via localStorage

🤖 Generated with Claude Code

https://claude.ai/code/session_01PyGDAXkee7m9yRQSfHhVyZ

martin-henz and others added 2 commits August 1, 2026 17:35
Bundle side (src/bundles/repl) was already done in a prior interrupted
session: index.ts/protocol.ts/rich_display.ts/stringify_value.ts implement
the Conductor BaseModulePlugin, channel protocol, rich-text formatting and
value stringification. This commit adds the missing host-side piece: the
Repl tab (src/tabs/Repl) was still the legacy pre-Conductor implementation
referencing the now-deleted programmable_repl.ts, so it has been rewritten
as a Conductor IPlugin that subscribes to the repl channel, replays/relays
output and program-text messages, and drives the AceEditor UI.

Still to do: run lint/tsc/test for both packages and fix any fallout,
then review the diff end-to-end (including a leftover unused
REPL_RUNNER_ID export in protocol.ts).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyGDAXkee7m9yRQSfHhVyZ
Bundle:
- Remove dead REPL_RUNNER_ID export (unused anywhere, unlike the
  RUNNER_ID/WEB_ID pattern in matrix/sound's protocol.ts).
- Fix a genuine unnecessary-assertion lint error and import ordering.
- Add unit tests for the pure logic (rich_display.ts, stringify_value.ts).
  A test for ReplModulePlugin's decorated index.ts itself is not possible:
  vitest's esbuild-based SSR transform only lowers native (TC39 stage-3)
  decorators via its bundling API, not the single-file transform API it
  actually uses to load modules, so any file with an `@moduleMethod`-
  decorated async generator method fails with a raw SyntaxError on import.
  This is a pre-existing, documented limitation (see midi's
  conductorAdapters.ts) that already applies to sound/matrix/pix_n_flix -
  none of them test their decorated index.ts directly either.

Tab:
- Fix a non-exhaustive switch (explicitly no-op the tab->bundle 'run'/
  'request' message variants instead of a catch-all default) and two
  eslint-disable comments that turned out to guard against a rule that
  isn't actually enabled here.
- Fix a test race: a localStorage write assertion needs to wait out the
  100ms throttle window rather than checking synchronously.

Verified tsc, lint, vitest and the real buildtools build pipeline are all
green for both packages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyGDAXkee7m9yRQSfHhVyZ
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@martin-henz

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The REPL was migrated from standalone js-slang functions to Conductor-based bundle and tab plugins. A shared channel protocol, rich display processing, value stringification, persistence, editor controls, and plugin tests were added.

Changes

REPL Conductor migration

Layer / File(s) Summary
Protocol and value formatting
src/bundles/repl/src/protocol.ts, src/bundles/repl/src/rich_display.ts, src/bundles/repl/src/stringify_value.ts, src/bundles/repl/src/__tests__/*, src/bundles/repl/package.json, src/bundles/repl/tsconfig.json
The REPL channel contract and formatting utilities were added. Tests cover rich display validation, nested styles, recursive values, placeholders, and cyclic pairs.
Bundle-side REPL plugin
src/bundles/repl/src/index.ts
ReplModulePlugin now manages evaluator registration, execution, output, editor state, program text, tab loading, and unsupported default evaluation.
Host tab plugin and view
src/tabs/Repl/src/index.tsx, src/tabs/Repl/src/__tests__/index.test.ts, src/tabs/Repl/package.json, src/tabs/Repl/tsconfig.json
ReplTabPlugin and ReplView provide channel handling, persisted editor text, execution controls, resizing, styling, and output rendering. Tests cover lifecycle, persistence, messages, and execution requests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReplView
  participant ReplTabPlugin
  participant REPLChannel
  participant ReplModulePlugin
  ReplView->>ReplTabPlugin: request code execution
  ReplTabPlugin->>REPLChannel: publish run message
  REPLChannel->>ReplModulePlugin: deliver run message
  ReplModulePlugin->>ReplModulePlugin: execute registered evaluator
  ReplModulePlugin->>REPLChannel: publish formatted output
  REPLChannel->>ReplTabPlugin: deliver output
  ReplTabPlugin->>ReplView: render output
Loading

Possibly related PRs

Suggested reviewers: aaravmalani

Poem

A rabbit hops through channels bright,
Sends evaluator runs in flight.
Rich spans bloom, values align,
The editor saves each latest line.
“Conductor guides the REPL tonight!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: migrating the REPL module to Conductor.
Description check ✅ Passed The description provides detailed motivation, scope, dependencies, and testing, but omits the issue reference, change type, and checklist sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch conductor-migration-repl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
src/bundles/repl/src/index.ts (1)

303-330: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider guarding __runCode against concurrent runs.

Each run message starts a new evaluator call immediately. Two quick Run clicks interleave two generator drains, and their output messages interleave in __outputHistory. A simple in-flight flag keeps output ordered and prevents a second call while the first evaluator is still running.

♻️ Proposed refactor
+  private __running = false;
+
   private async __runCode(code: string): Promise<void> {
+    if (this.__running) return;
     if (!this.__evaluator) {

Reset __running in a finally block around the try.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bundles/repl/src/index.ts` around lines 303 - 330, Update __runCode to
guard against concurrent evaluator calls with an in-flight __running flag:
return immediately when a run is already active, set the flag before starting
evaluation, and reset it in a finally block so both success and error paths
re-enable execution.
src/bundles/repl/src/__tests__/rich_display.test.ts (1)

36-52: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a case for trailing characters after the hex colour.

The current cases do not detect the unanchored-regex problem reported on src/bundles/repl/src/rich_display.ts Line 41. Add a case that rejects extra characters after the six hex digits.

💚 Proposed test
   test('rejects a colour missing digits', () => {
     expect(checkColorStringValidity('`#fff`')).toBe(false);
   });
+
+  test('rejects trailing characters after the hex digits', () => {
+    expect(checkColorStringValidity('`#ff0000`" onmouseover="alert(1)')).toBe(false);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bundles/repl/src/__tests__/rich_display.test.ts` around lines 36 - 52,
Add a test in the checkColorStringValidity suite that passes a six-digit hex
color followed by extra characters and expects false, covering the
trailing-character validation case without changing the existing cases.
src/tabs/Repl/src/__tests__/index.test.ts (2)

101-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the restored text, not just the getItem call.

expect(freshPlugin).toBeDefined() and the getItem spy pass even when the cached value is ignored. Assert that the restored text reaches the program state, for example by calling freshPlugin.__runCode() on its own channel and checking the run payload.

💚 Proposed test
   test('loads the initially cached program text from localStorage', () => {
     localStorageMock.setItem('programmable_repl_saved_editor_code', 'cached_code();');
-    const freshPlugin = new ReplTabPlugin({} as any, [new MockChannel()], new MockTabService());
-    expect(freshPlugin).toBeDefined();
-    expect(localStorageMock.getItem).toHaveBeenCalledWith('programmable_repl_saved_editor_code');
+    const freshChannel = new MockChannel<any>();
+    const freshPlugin = new ReplTabPlugin({} as any, [freshChannel], new MockTabService());
+    freshPlugin.__runCode();
+    expect(freshChannel.sent).toContainEqual({ type: 'run', code: 'cached_code();' });
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tabs/Repl/src/__tests__/index.test.ts` around lines 101 - 106, Strengthen
the test around ReplTabPlugin initialization by verifying that the cached editor
text is restored into program state, not only that localStorage was queried.
After creating freshPlugin, invoke its __runCode() using the test channel and
assert the emitted run payload contains cached_code(); while retaining the
existing localStorage interaction assertion.

108-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use fake timers instead of real sleeps.

Two tests wait 150 ms of wall-clock time each. vi.useFakeTimers() with vi.advanceTimersByTime(150) removes the delay and the flake risk from timing.

Also applies to: 136-147

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tabs/Repl/src/__tests__/index.test.ts` around lines 108 - 113, Update the
tests covering __setProgramText, including the additional test around lines
136-147, to use Vitest fake timers instead of awaiting real 150 ms sleeps.
Enable fake timers for the test scope, advance them by 150 ms after triggering
the throttled save, and restore real timers during cleanup while preserving the
existing localStorage assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bundles/repl/src/rich_display.ts`:
- Around line 40-42: Update checkColorStringValidity so its hexadecimal color
regex is anchored at both the start and end, allowing only exactly six
hexadecimal digits prefixed by # and rejecting any surrounding or trailing
content.
- Around line 49-107: Bound recursion in processRichDisplayContent using the
existing MAX_DEPTH convention from stringify_value.ts. Add and propagate a depth
parameter through recursive calls, and throw the established runtime error when
the maximum depth is exceeded, while preserving current string, pair, style, and
validation behavior.

In `@src/bundles/repl/src/stringify_value.ts`:
- Around line 29-47: Update stringifyReplValueDepth to track a shared total-node
budget in addition to MAX_DEPTH, decrementing it for each visited value and
returning the existing truncation marker when exhausted. Thread the same budget
through both head/tail recursion in the PAIR case and every recursive call in
the ARRAY case, so branching cycles cannot exceed the total traversal limit
while preserving the depth cap.

In `@src/tabs/Repl/src/__tests__/index.test.ts`:
- Around line 121-134: Update the test around __runCode so it actually verifies
that the two emitted output entries are accumulated, rather than only asserting
the run message. Expose the accumulated outputs through a read-only getter or
render the tab body, then assert that both “hello” and “world” entries are
present in order.

In `@src/tabs/Repl/src/index.tsx`:
- Around line 80-92: Guard both storage operations: update the constructor’s
localStorage.getItem access used to initialize programText and saveProgramText’s
localStorage.setItem call so SecurityError or quota failures are caught without
interrupting tab registration, REPL initialization, or the throttled save
callback. Preserve the existing value and save behavior when storage succeeds.
- Around line 115-120: Update ReplTabPlugin’s destroy() method to unsubscribe
its __handleMessage channel subscription while leaving the tab registration
intact. Ensure each Run removes the discarded plugin instance’s listener so it
no longer receives messages or mutates state after teardown.
- Around line 231-237: Update the AceEditor integration so changes to
state.fontSize are applied after mount, using an effect keyed on state.fontSize
or the component’s supported fontSize prop. Keep editorInstanceRef for the
editor instance, but ensure set_font_size triggers the mounted editor to receive
the new font size rather than applying it only in the ref callback.

---

Nitpick comments:
In `@src/bundles/repl/src/__tests__/rich_display.test.ts`:
- Around line 36-52: Add a test in the checkColorStringValidity suite that
passes a six-digit hex color followed by extra characters and expects false,
covering the trailing-character validation case without changing the existing
cases.

In `@src/bundles/repl/src/index.ts`:
- Around line 303-330: Update __runCode to guard against concurrent evaluator
calls with an in-flight __running flag: return immediately when a run is already
active, set the flag before starting evaluation, and reset it in a finally block
so both success and error paths re-enable execution.

In `@src/tabs/Repl/src/__tests__/index.test.ts`:
- Around line 101-106: Strengthen the test around ReplTabPlugin initialization
by verifying that the cached editor text is restored into program state, not
only that localStorage was queried. After creating freshPlugin, invoke its
__runCode() using the test channel and assert the emitted run payload contains
cached_code(); while retaining the existing localStorage interaction assertion.
- Around line 108-113: Update the tests covering __setProgramText, including the
additional test around lines 136-147, to use Vitest fake timers instead of
awaiting real 150 ms sleeps. Enable fake timers for the test scope, advance them
by 150 ms after triggering the throttled save, and restore real timers during
cleanup while preserving the existing localStorage assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9282be2a-de28-4b4d-85ac-2f39db172aa3

📥 Commits

Reviewing files that changed from the base of the PR and between 3ad9766 and 377a606.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (17)
  • src/bundles/repl/package.json
  • src/bundles/repl/src/__tests__/index.test.ts
  • src/bundles/repl/src/__tests__/rich_display.test.ts
  • src/bundles/repl/src/__tests__/stringify_value.test.ts
  • src/bundles/repl/src/config.ts
  • src/bundles/repl/src/functions.ts
  • src/bundles/repl/src/index.ts
  • src/bundles/repl/src/programmable_repl.ts
  • src/bundles/repl/src/protocol.ts
  • src/bundles/repl/src/rich_display.ts
  • src/bundles/repl/src/stringify_value.ts
  • src/bundles/repl/tsconfig.json
  • src/tabs/Repl/index.tsx
  • src/tabs/Repl/package.json
  • src/tabs/Repl/src/__tests__/index.test.ts
  • src/tabs/Repl/src/index.tsx
  • src/tabs/Repl/tsconfig.json
💤 Files with no reviewable changes (5)
  • src/bundles/repl/src/config.ts
  • src/bundles/repl/src/functions.ts
  • src/tabs/Repl/index.tsx
  • src/bundles/repl/src/tests/index.test.ts
  • src/bundles/repl/src/programmable_repl.ts

Comment thread src/bundles/repl/src/rich_display.ts
Comment thread src/bundles/repl/src/rich_display.ts
Comment thread src/bundles/repl/src/stringify_value.ts Outdated
Comment thread src/tabs/Repl/src/__tests__/index.test.ts Outdated
Comment thread src/tabs/Repl/src/index.tsx
Comment thread src/tabs/Repl/src/index.tsx
Comment thread src/tabs/Repl/src/index.tsx

@martin-henz martin-henz left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Overall this is a solid, faithful migration of repl to Conductor — the wire protocol is clean, the tab correctly separates the "raw channel" pattern from the RPC pattern used elsewhere (matrix/sound) with a good rationale in the doc comment, and the new unit tests for rich_display.ts/stringify_value.ts are thorough. One real functional bug and a pre-existing (not introduced here) security issue worth flagging below.

Correctness

set_background_image/set_font_size/set_program_text never trigger the lazy tab load (src/bundles/repl/src/index.ts)

__displayOutput (used by repl_display/rich_repl_display/__runCode) calls this.__loadReplTab(), but __displayEditorProps (used by set_background_image/set_font_size) and set_program_text do not:

private __displayEditorProps(): void {
  const message: ReplEditorPropsMessage = { type: 'editor_props', ...this.__editorProps };
  this.__latestEditorProps = message;
  if (this.__tabRequested) {
    this.__replChannel.send(message);
  }
}
async* set_program_text(text: TypedValue<DataType.CONST_STRING>): AsyncGenerator<...> {
  const message: ReplSetProgramTextMessage = { type: 'set_program_text', text: text.value };
  this.__latestProgramText = message;
  if (this.__tabRequested) {
    this.__replChannel.send(message);
  }
  return mVoid();
}

Neither calls __loadReplTab(). A program whose only interaction with the module is set_program_text(...) — a very plausible use case per its own doc comment ("Set program text in the Repl editor to the given string", i.e. pre-populating starter code for a student) — will never cause the Repl tab to actually open. The text is latched (__latestProgramText) and would only reach the tab if something else happens to open it later, which defeats the point of pre-populating starter code for the student to immediately see and run.

Suggest calling this.__loadReplTab() at the top of __displayEditorProps() and inside set_program_text(), matching __displayOutput's behavior and the established convention elsewhere in this codebase (e.g. matrix's get_matrix()/clear_matrix() both call this.__ensureTabLoaded(), sound's play()/init_record() etc. all do too) — every host-touching entry point calls the lazy-load helper, not just one of them.

Security (pre-existing, not introduced by this PR)

checkColorStringValidity in rich_display.ts uses an unanchored regex:

export function checkColorStringValidity(htmlColor: string): boolean {
  return /#[0-9a-f]{6}/u.test(htmlColor.toLowerCase());
}

The clrt/clrb colour value (colorHex = config_str.substring(4)) is validated with this and then interpolated directly into the style="..." attribute that the tab renders via dangerouslySetInnerHTML. Since the regex isn't anchored, a payload like:

rich_repl_display(pair("text", "clrt#ff0000\" onmouseover=\"alert(1)"));

passes validation (it contains # + 6 hex digits) and breaks out of the style attribute, injecting an arbitrary attribute/event handler — a DOM XSS via rich_repl_display. Note xssStringCheck (which blocks <, >, script, etc.) is only ever applied to the base/leaf text content, never to the tail/style string, so nothing else catches this either.

I confirmed this is byte-for-byte identical to the deleted programmable_repl.ts's checkColorStringValidity/processRichDisplayContent, so it's not introduced by this migration. But since this PR is the first time this file has been touched in a while, it might be worth either fixing it here (anchor the regex: /^#[0-9a-f]{6}$/u) or filing a follow-up issue so it doesn't get forgotten again.

Test coverage

src/tabs/Repl/src/__tests__/index.test.ts's 'an output message from the bundle is accumulated' test doesn't actually assert anything about the two emitted entries:

test('an output message from the bundle is accumulated', () => {
  channel.emit({ type: 'output', entry: { content: 'hello', ... } });
  channel.emit({ type: 'output', entry: { content: 'world', ... } });
  // Output isn't exposed via a getter - re-running the code and checking the channel traffic
  // exercises the same accumulation logic without reaching into private state.
  plugin.__runCode();
  expect(channel.sent).toContainEqual({ type: 'run', code: '' });
});

Since ReplViewState.outputs isn't exposed via any getter, this only proves __runCode() still works — it would pass even if the 'output' case in __handleMessage were completely broken (e.g. never pushed to outputs at all). Consider exposing a small test-only accessor (SoundTabPlugin.getStatus() is the precedent for this in the codebase) so the actual accumulated state can be asserted directly.

Minor / nits (both pre-existing, faithfully ported — not blocking)

  • set_font_size's doc comment says "@param font_size_px font size (in pixels)", but the tab applies it as `${state.fontSize}pt` (points, not pixels) via editorInstance.setOptions(...). 1pt ≈ 1.33px, so rendered size doesn't match the documented unit. Identical to the deleted pre-Conductor tab, so not new here.
  • In ReplView, the AceEditor ref callback is a fresh arrow function every render, so setOptions({fontSize}) re-runs on every re-render rather than just on mount/fontSize change. Harmless (idempotent) but redundant — react-ace's AceEditor also accepts a fontSize prop directly, which would sidestep the manual setOptions call entirely. Again, faithfully ported from the old implementation's pattern.

What's good

  • The doc comment explaining why default_js_slang can't work under Conductor (and pointing at the exact commons.ts line that marks js-slang* external) is excellent — this is exactly the kind of context a future reader needs.
  • Choosing a raw subscribe/send channel over makeRpc for this module, with a clear one-paragraph justification, shows real understanding of when each pattern in this codebase applies (matches pix_n_flix's frame channel precedent).
  • stringify_value.ts's recursion depth guard (and the test for it against a self-referential pair) is a nice defensive touch that the old implementation didn't need to worry about in the same way.
  • Backlog-replay semantics (__tabRequested gate, replaying full history vs. latest-only for props/program-text) are well thought through and well tested.

- rich_display.ts: anchor checkColorStringValidity's regex (was unanchored,
  letting a crafted clrt#/clrb# tail break out of the style="..." attribute
  the tab renders via dangerouslySetInnerHTML - a real DOM XSS via
  rich_repl_display); bound processRichDisplayContent's recursion so a
  cyclic pair (set_head/set_tail) can't recurse forever.
- stringify_value.ts: add a shared total-node budget alongside the existing
  depth cap - a pair whose head AND tail both cycle back branches at every
  level, so depth alone still allows up to 2^MAX_DEPTH calls.
- index.ts: guard __runCode against two overlapping runs (e.g. a fast
  double-click on Run) driving the same evaluator closure concurrently.
- tabs/Repl/index.tsx: guard localStorage access (throws in some
  private-browsing/quota-exceeded contexts); unsubscribe from the channel in
  destroy() so a discarded plugin instance stops reacting to messages; move
  fontSize application into an effect keyed on state.fontSize instead of
  only the AceEditor ref callback, and add a getOutputs() accessor for tests.
- Tests: add regression coverage for all of the above, strengthen two tests
  that weren't actually asserting what their names claimed, and switch two
  throttle-wait tests to fake timers instead of real 150ms sleeps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyGDAXkee7m9yRQSfHhVyZ
@martin-henz

Copy link
Copy Markdown
Member Author

Thanks for the thorough review — went through all 7 actionable comments (replied individually on each thread) plus the 4 nitpicks, in 4ae6507:

  • __runCode concurrency guard: added an __running flag, set before the evaluator closure call and reset in a finally, so two overlapping run messages (e.g. a fast double-click) can't drive the same evaluator closure concurrently. Couldn't add a unit test for this specifically — importing the bundle's index.ts directly in a test isn't possible (see the PR description's note on the vitest/decorator limitation), so this one is exercised by the existing set_evaluator/error-path coverage only, not a dedicated concurrency test.
  • checkColorStringValidity trailing-characters test: added, plus a companion processRichDisplayContent test using the exact attribute-break-out payload.
  • localStorage-restore test not verifying restored state: fixed — now calls __runCode() on the fresh instance and asserts the run message carries the cached code.
  • Real 150ms sleeps → fake timers: done for both throttle-wait tests (vi.useFakeTimers() / vi.advanceTimersByTime(150)), dropped the tab's test suite runtime from ~310ms to ~7ms.

Full rundown of the 7 actionable fixes is in my replies on each thread. tsc/lint/vitest/buildtools build all still green for both packages after these changes.

- index.ts: set_background_image/set_font_size/set_program_text now call
  __loadReplTab(), matching __displayOutput's behavior. Previously, a
  program whose only interaction with the module was one of these three -
  most plausibly set_program_text, used to pre-populate starter code for a
  student - would never cause the Repl tab to actually open, silently
  defeating the feature.
- tabs/Repl/index.tsx: apply font size in px, not pt, to actually match
  set_font_size's own doc comment and parameter name (font_size_px) -
  pre-existing mismatch carried over from the deleted pre-Conductor tab.

No corresponding bundle-side test: importing index.ts directly in a test
still hits the same vitest/decorator limitation noted elsewhere in this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyGDAXkee7m9yRQSfHhVyZ
@martin-henz

Copy link
Copy Markdown
Member Author

Handled both findings from this review, in 5ba98cd:

  • set_background_image/set_font_size/set_program_text never triggering the lazy tab load: fixed — both now call __loadReplTab(), matching __displayOutput's behavior. Couldn't add a bundle-side unit test for it though; same vitest/decorator-import limitation noted elsewhere in this PR applies here too.
  • set_font_size doc/param name says pixels, code applied points: fixed — now applies px, matching the font_size_px parameter name and its doc comment.

The security finding (unanchored colour regex) and the weak test-coverage finding from this same review were already handled as part of CodeRabbit's review (which flagged the same two issues independently) — see 4ae6507 and my replies on that review's threads.

@martin-henz
martin-henz merged commit 2381988 into master Aug 2, 2026
12 checks passed
@martin-henz
martin-henz deleted the conductor-migration-repl branch August 2, 2026 06:40
martin-henz added a commit that referenced this pull request Aug 2, 2026
set_evaluator() was the one repl function that gave a program a
reason to use the tab but never actually triggered the lazy tab
loader - every other producing/customizing function (set_program_text,
repl_display, set_background_image, ...) already calls
__loadReplTab(), but set_evaluator() was missed when the lazy-load
pattern was introduced in the Conductor migration (#865).

As a result, a program whose only interaction with the module is
set_evaluator() - exactly the module's own documented minimal usage
example - never opened the Repl tab at all.


Claude-Session: https://claude.ai/code/session_01B3LRoXRMQ5ADeYMdVTChCZ

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant