Skip to content

New in-browser code editor plugin#1659

Open
Ukonhattu wants to merge 15 commits intomasterfrom
dev-dan
Open

New in-browser code editor plugin#1659
Ukonhattu wants to merge 15 commits intomasterfrom
dev-dan

Conversation

@Ukonhattu
Copy link
Contributor

@Ukonhattu Ukonhattu commented Feb 20, 2026

Summary by CodeRabbit

  • New Features

    • Added an in-browser Python exercise editor with run/test controls, interactive stdin support, real-time stdout/stderr streaming, and test result display.
    • Built browser-side test execution with timeouts, worker-based runs, and clear run/test UI (buttons, output panel, reset confirmation, and icons).
    • Submission flow now surfaces pass/fail scoring and grading banners.
  • Localization

    • Added submission result messages in English, Finnish, Swedish, Arabic, and Ukrainian.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This PR adds a browser-based Python test execution system: new Web Workers and Pyodide integration, a modular AnswerBrowserExercise UI with hooks and adapters, utilities for building in-browser test scripts, state/interface updates for browser tests, and grading/public-spec adaptations. Also includes build/script changes for injecting Pyodide version and localization strings.

Changes

Cohort / File(s) Summary
Git & Build Config
/.gitignore, services/tmc/next.config.js, services/tmc/package.json, services/tmc/scripts/inject-pyodide-version.cjs, services/tmc/src/util/pyodide-version.json
Ignore local TLS certs; webpack now branches on isServer and disables pyodide fallback on client builds; build/dev scripts run injection script; pyodide version config added.
Browser Workers & Templates
services/tmc/public/runWorker.js, services/tmc/public/browserTestWorker.js, services/tmc/public/runWorkerTemplate.py
New browser worker scripts and Python runtime template to load Pyodide, run/instrument user code, stream stdout/stderr, handle stdin, and return JSON run results or errors.
Pyodide Loader & Config
services/tmc/src/util/pyodideConfig.ts, services/tmc/src/util/pyodideLoader.ts
Centralized Pyodide version/URLs and a browser loader exposing a cached getPyodide() with typed interface.
Test Script Builder & Helpers
services/tmc/src/tmc/browserTestScript.ts, services/tmc/src/tmc/browserTestScriptHelpers.ts
New logic to inline/patch test and tmc modules, assemble a single Python test script that emits JSON RunResult, plus fs helpers and escaping utilities.
Worker Protocol Types & Injection Script
services/tmc/src/util/pythonTestWorker.ts, services/tmc/scripts/inject-pyodide-version.cjs
Types for worker request/response; script injects computed PYODIDE_INDEX_URL into worker files at build/dev time.
AnswerBrowserExercise — Top-level & Types
services/tmc/src/components/AnswerBrowserExercise.tsx (removed), services/tmc/src/components/AnswerBrowserExercise/AnswerBrowserExercise.tsx, services/tmc/src/components/AnswerBrowserExercise/index.tsx, services/tmc/src/components/AnswerBrowserExercise/types.ts
Replaced monolithic component with a modular default export and typed props; added grading prop handling.
AnswerBrowserExercise — Subcomponents & Styles
services/tmc/src/components/AnswerBrowserExercise/components/*, services/tmc/src/components/AnswerBrowserExercise/styles.ts, services/tmc/src/components/AnswerBrowserExercise/icons.tsx
Added ActionButtons, EditorSection, OutputPanel, ResetConfirmDialog, RunOutputContent, TestResultCard, TestResultsContent, index re-exports, icons, and comprehensive styles.
AnswerBrowserExercise — Hooks & Utils
services/tmc/src/components/AnswerBrowserExercise/hooks/*, services/tmc/src/components/AnswerBrowserExercise/utils/*, services/tmc/src/components/AnswerBrowserExercise/utils/index.ts
New hooks: useEditorState, useRunOutput (worker orchestration, stdin/stdout streaming), useTestRun (adapters, timeout handling); utilities: extensionToLanguage, formatLogsForDisplay, isSupportedForBrowserTest.
Browser Test Adapter Infrastructure
services/tmc/src/components/AnswerBrowserExercise/browserTest/*
Adapter types, Python adapter implementation, timeout constant, and adapter dispatcher to pick runtime-specific adapters.
State Interfaces & UI Integration
services/tmc/src/util/stateInterfaces.ts, services/tmc/src/components/AnswerExercise.tsx, services/tmc/src/components/ViewSubmission.tsx, services/tmc/src/components/StateRenderer.tsx, services/tmc/src/app/iframe/page.tsx
Added BrowserTestSpec and browser_test? on PublicSpec; propagate grading/readOnly to AnswerBrowserExercise; normalize submission shape and adjust view-submission/iframe flows and file ordering.
API Routes
services/tmc/src/app/api/grade/route.ts, services/tmc/src/app/api/public-spec/route.ts
Grade endpoint now normalizes run/test outputs and returns results; public-spec endpoint builds browser test scripts and returns browser_test in spec; prepareStubDir refactored to return stub/template dirs and accept a log function.
Localization
shared-module/packages/common/src/locales/*/tmc.json (ar, en, fi, sv, uk)
Added submit-result-failed and submit-result-passed translation keys across locales.
CSP Header
shared-module/packages/common/src/utils/responseHeaders.js
Updated Content-Security-Policy to add worker-src 'self' blob: directive.
Misc
services/tmc/public/..., services/tmc/src/util/pyodide-version.json
Injection and worker URL wiring; minor packaging/dependency addition (pyodide dependency).

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Browser)
    participant Main as Main Thread / UI
    participant Worker as Web Worker
    participant Pyodide as Pyodide (WASM)

    User->>Main: Trigger Run/Test with user code
    Main->>Main: Build/prepare test script (adapter)
    Main->>Worker: postMessage({ script })
    Worker->>Worker: ensure Pyodide loaded
    Worker->>Pyodide: load/indexURL & configure stdio
    Worker->>Pyodide: runPythonAsync(script)
    loop during execution
        Pyodide->>Worker: stdout/stderr chunk
        Worker->>Main: postMessage(stdout/stderr)
        alt needs input
            Worker->>Main: postMessage(stdin_request)
            Main->>User: show input prompt
            User->>Main: provide input
            Main->>Worker: postMessage(stdin_line)
            Worker->>Pyodide: deliver stdin
        end
    end
    Pyodide-->>Worker: execution finished
    Worker->>Main: postMessage({ runResult } or { error })
    Main->>User: display results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • nygrenh

Poem

🐰 Whiskers twitching with delight,
Worker wakes and loads Pyodide light,
Code hops in, prints and waits for a line,
Results bounce back in JSON fine,
A little rabbit claps — tests passed, that's right! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'New in-browser code editor plugin' is a generic description that captures the high-level nature of the change but lacks specificity about the main technical contribution.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev-dan

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 and usage tips.

@Ukonhattu
Copy link
Contributor Author

Uhh need to fix some "translations"

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
services/tmc/next.config.js (2)

43-58: ⚠️ Potential issue | 🟠 Major

Missing Turbopack equivalent for the pyodide fallback — will break next dev --turbo.

In webpack, the resolve.fallback option is typically used to silence module-not-found errors; Turbopack offers a similar option via turbopack.resolveAlias. At the moment only the browser condition is supported for conditional aliasing. Because you need to tell Turbopack to load an empty module when the package is requested in the browser, the alias must point to an actual empty stub file — unlike webpack's fallback: false, you cannot alias directly to a boolean.

The fix requires two steps:

  1. Create a minimal empty stub (e.g., src/pyodideStub.js):
// Pyodide is loaded from CDN at runtime; this stub silences Turbopack resolution.
export default {}
  1. Add resolveAlias to the turbopack block:
⚙️ Proposed fix for Turbopack pyodide resolution
  turbopack: {
    rules: {
      "*.svg": {
        loaders: [
          {
            loader: "@svgr/webpack",
            options: {
              svgoConfig: svgoConfig,
              svgProps: { role: "presentation" },
            },
          },
        ],
        as: "*.js",
      },
    },
+   resolveAlias: {
+     // Pyodide is loaded from CDN at runtime; prevent Turbopack from bundling it.
+     pyodide: { browser: "./src/pyodideStub.js" },
+   },
  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/next.config.js` around lines 43 - 58, Add a Turbopack alias to
handle the Pyodide fallback and create a real empty stub so `next dev --turbo`
won't error: add a minimal stub file (e.g., src/pyodideStub.js) exporting an
empty object, then update the turbopack block in next.config.js to include
resolveAlias mapping the pyodide package name to that stub for the browser
condition (use turbopack.resolveAlias) so the bundler resolves imports to the
stub instead of failing; reference turbopack.resolveAlias and the new
src/pyodideStub.js when making the change.

43-58: ⚠️ Potential issue | 🟠 Major

Turbopack does not inherit webpack's resolve.fallback — add a resolveAlias for pyodide with a stub module.

With Next.js 15, next dev --turbo is stable and next build --turbopack is available. Turbopack processes its own module graph and ignores the resolve.fallback set in the webpack() function. If any client-side file that Turbopack traces imports pyodide (which is not installed as a package), the dev server or build will fail.

The Turbopack equivalent is resolveAlias with a conditional browser target. Create an empty stub file and alias pyodide to it:

Create `empty.js` in the project root:
module.exports = {}

Then update next.config.js:

Add `resolveAlias` to the turbopack config:
  turbopack: {
    rules: {
      "*.svg": {
        loaders: [
          {
            loader: "@svgr/webpack",
            options: {
              svgoConfig: svgoConfig,
              svgProps: { role: "presentation" },
            },
          },
        ],
        as: "*.js",
      },
    },
+   resolveAlias: {
+     // Pyodide is loaded from CDN at runtime; prevent Turbopack from trying to bundle it.
+     pyodide: { browser: "./empty.js" },
+   },
  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/next.config.js` around lines 43 - 58, Turbopack ignores webpack
resolve.fallback so create a tiny stub module named empty.js exporting an empty
object and then add a resolveAlias entry inside the existing turbopack config
block (the turbopack.rules section shown) to alias "pyodide" to that stub for
the browser target; specifically add a resolveAlias mapping (browser
conditional) that points "pyodide" -> "./empty.js" so any client-side import of
pyodide resolves to the stub during dev/build.
services/tmc/src/app/api/public-spec/route.ts (1)

203-204: ⚠️ Potential issue | 🟡 Minor

makeLog drops variadic arguments and leaks undefined into logs.

The inner function uses (message, optionalParams) instead of (message, ...optionalParams). Because the return type annotation uses rest params, TypeScript collapses all extra arguments into a single array for optionalParams. Passing that array as a single positional arg to log means it gets printed as [ 'a', 'b' ] instead of being spread. When called with no extra args (e.g., from lines 101/105), optionalParams is [] and log prints an empty array.

Proposed fix
 const makeLog = (requestId: string): ((message: string, ...optionalParams: unknown[]) => void) => {
-  return (message, optionalParams) => log(requestId, message, optionalParams)
+  return (message, ...optionalParams) => log(requestId, message, ...optionalParams)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/app/api/public-spec/route.ts` around lines 203 - 204, The
helper makeLog currently defines the inner function as (message, optionalParams)
which collapses variadic args into a single array and passes that array as one
positional arg to log, causing arrays or undefined to appear in logs; change the
inner function to accept rest parameters (message: string, ...optionalParams:
unknown[]) and call log(requestId, message, ...optionalParams) so arguments are
forwarded correctly; update the inner function signature to match the declared
return type and preserve types for requestId and log forwarding.
🟡 Minor comments (10)
services/tmc/SUBMISSION-PLAN.md-113-113 (1)

113-113: ⚠️ Potential issue | 🟡 Minor

Math.max(1, points.length) silently masks an empty or malformed test suite.

If points is empty because the pod output was malformed, a JSON parse error occurred, or the exercise genuinely has no test points, this floors score_maximum at 1 and produces a valid-looking 0/1 grading result rather than surfacing the underlying problem.

The plan should instead specify that points.length === 0 (after normalization and error handling) should result in an explicit error grading result or at minimum a logged warning, not a silent 1. Silently adjusting score_maximum makes broken exercises indistinguishable from correctly-graded ones in the data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/SUBMISSION-PLAN.md` at line 113, The current use of Math.max(1,
points.length) hides malformed or empty test suites; change the logic that
computes score_maximum from using Math.max to an explicit check of points.length
=== 0 (after your points normalization/error handling) and in that branch
produce an explicit error grading result (or at minimum emit a clear warning
log) instead of silently setting score_maximum to 1; update the code paths that
compute score_maximum and the grading/result construction so they detect
empty/malformed points, log the problem (include normalized input/context), and
return a failure/error grading object rather than a 0/1-looking score.
shared-module/packages/common/src/locales/ar/tmc.json-21-22 (1)

21-22: ⚠️ Potential issue | 🟡 Minor

New entries are untranslated — English strings in the Arabic locale file.

Both submit-result-failed and submit-result-passed carry English values while every other key in this file is properly translated into Arabic. Arabic-locale users will see raw English for submission results.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared-module/packages/common/src/locales/ar/tmc.json` around lines 21 - 22,
The Arabic locale file contains untranslated English strings for the keys
"submit-result-failed" and "submit-result-passed"; update the values for these
keys in shared-module/packages/common/src/locales/ar/tmc.json to their Arabic
translations, preserving the interpolation placeholders ({{score}} and {{max}})
and the intended meaning/format so Arabic users see localized messages for
submission results.
shared-module/packages/common/src/locales/fi/tmc.json-21-22 (1)

21-22: ⚠️ Potential issue | 🟡 Minor

Localization strings are in English instead of Finnish.

All other entries in this Finnish locale file are properly translated, but these two new keys contain English text. They should be translated to Finnish, e.g.:

  • "submit-result-failed": something like "Testit epäonnistuivat. Pisteet: {{score}}/{{max}}. Voit muokata koodiasi ja palauttaa uudelleen."
  • "submit-result-passed": something like "Kaikki testit läpäisty! Pisteet: {{score}}/{{max}}."

The same issue applies to the Swedish (sv/tmc.json) and Ukrainian (uk/tmc.json) locale files reviewed in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared-module/packages/common/src/locales/fi/tmc.json` around lines 21 - 22,
Two localization entries ("submit-result-failed" and "submit-result-passed") in
the Finnish locale (tmc.json) are still in English; update their values to
Finnish (e.g. translate to "Testit epäonnistuivat. Pisteet: {{score}}/{{max}}.
Voit muokata koodiasi ja palauttaa uudelleen." and "Kaikki testit läpäisty!
Pisteet: {{score}}/{{max}}.") and apply equivalent translations to the same keys
in the Swedish (sv/tmc.json) and Ukrainian (uk/tmc.json) locale files so all
three locale files contain proper translations for submit-result-failed and
submit-result-passed.
services/tmc/src/app/api/grade/route.ts-119-125 (1)

119-125: ⚠️ Potential issue | 🟡 Minor

Error message references exercise_spec.type but the validation failure is about submission_data.

When normalizeSubmissionData returns null, the error message on line 123 says unexpected submission type '${exercise_spec.type}', which is misleading — the issue is that submission_data is missing or has an unrecognized shape, not that exercise_spec.type is wrong. Consider referencing the actual submission data in the message.

Proposed fix
     if (!submission_data) {
       return badRequest(
-        `unexpected submission type '${exercise_spec.type}' (missing or invalid submission_data)`,
+        `missing or invalid submission_data (exercise_spec.type='${exercise_spec.type}')`,
       )
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/app/api/grade/route.ts` around lines 119 - 125, The error
message is misleading when normalizeSubmissionData(rawSubmissionData) returns
null; update the badRequest call in route.ts to reference the problematic
submission data rather than exercise_spec.type. Specifically, in the block using
rawSubmissionData / submission_data and calling normalizeSubmissionData, change
the error text in the badRequest to mention missing/invalid submission_data and
include identifying info (e.g., JSON.stringify(rawSubmissionData) or a short
descriptor) so the message reflects the actual validation failure in
normalizeSubmissionData.
services/tmc/src/components/AnswerBrowserExercise.refactor-plan.md-28-51 (1)

28-51: ⚠️ Potential issue | 🟡 Minor

Add language specifiers to fenced code blocks.

The code blocks on lines 28 and 182 lack language identifiers, which triggers markdownlint MD040. Adding a language (e.g., ```text or ```plaintext for directory trees) improves rendering and accessibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise.refactor-plan.md` around
lines 28 - 51, The markdown file AnswerBrowserExercise.refactor-plan.md contains
fenced code blocks (the directory tree blocks near the top and another at line
~182) without language specifiers which triggers markdownlint MD040; update
those fences to include a plain-text language identifier (e.g., change ``` to
```text or ```plaintext) so the directory trees render correctly and lint
passes, ensuring you modify both fenced blocks referenced in this file.
services/tmc/src/components/AnswerBrowserExercise.refactor-plan.md-214-216 (1)

214-216: ⚠️ Potential issue | 🟡 Minor

Comment uses # instead of // in a TypeScript code example.

Line 215 uses a Python/shell-style comment (# or ...) inside what's meant to be a TypeScript import example. This would be a syntax error in TS. Use // instead.

Proposed fix
-import AnswerBrowserExercise from "./AnswerBrowserExercise"  # or "./AnswerBrowserExercise/index"
+import AnswerBrowserExercise from "./AnswerBrowserExercise"  // or "./AnswerBrowserExercise/index"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise.refactor-plan.md` around
lines 214 - 216, The TypeScript import example uses a Python-style comment
symbol (#) which is invalid in TS; update the import line in
AnswerBrowserExercise.refactor-plan.md to use a TypeScript/JS single-line
comment (//) after the import (referencing the import statement `import
AnswerBrowserExercise from "./AnswerBrowserExercise"` or the alternative
`"./AnswerBrowserExercise/index"`) so the example is syntactically correct.
services/tmc/src/components/ViewSubmission.tsx-38-43 (1)

38-43: ⚠️ Potential issue | 🟡 Minor

<code> without <pre> will collapse whitespace and lose indentation.

Python code (or any indented code) rendered inside <code> alone will have its whitespace collapsed by the browser. Wrap it in <pre> to preserve formatting.

Proposed fix
         <div key={f.filepath}>
           <div>{f.filepath}</div>
-          <code>{f.contents}</code>
+          <pre><code>{f.contents}</code></pre>
         </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/ViewSubmission.tsx` around lines 38 - 43, The
rendered code blocks in ViewSubmission.tsx use <code>{f.contents}</code> which
collapses whitespace and loses indentation; update the filesToShow.map rendering
so the file contents are wrapped in a <pre> (e.g., <pre><code>...</code></pre>)
or otherwise use a preformatted element around f.contents so Python/indented
code preserves spacing and line breaks while keeping the existing keys
(f.filepath) and structure intact.
services/tmc/public/browserTestWorker.js-37-41 (1)

37-41: ⚠️ Potential issue | 🟡 Minor

No validation that lastLine is parseable JSON — cryptic error on malformed output.

If the test script produces no stdout or the last line isn't valid JSON, JSON.parse(lastLine) throws a generic syntax error. Consider wrapping in a try/catch with a descriptive message, and possibly validating the shape of the parsed result.

Proposed fix
       var lines = stdout.trim().split("\n")
       var lastLine = lines[lines.length - 1] ?? ""
-      var runResult = JSON.parse(lastLine)
-      self.postMessage({ runResult: runResult })
+      var runResult
+      try {
+        runResult = JSON.parse(lastLine)
+      } catch (parseErr) {
+        self.postMessage({ error: "Test output was not valid JSON: " + lastLine.slice(0, 200) })
+        return
+      }
+      self.postMessage({ runResult: runResult })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/browserTestWorker.js` around lines 37 - 41, The code
assumes the Python script's last stdout line is valid JSON and directly does
JSON.parse(lastLine); update the promise continuation after
pyodide.runPythonAsync to guard parsing: check stdout/lastLine isn't empty, wrap
JSON.parse(lastLine) in a try/catch, and on error post a descriptive error
object via self.postMessage (e.g., { error: "Invalid JSON from test script",
details: err.message, raw: lastLine }) instead of throwing; after successful
parse validate the expected shape of runResult before posting { runResult } to
avoid downstream cryptic syntax errors (referencing pyodide.runPythonAsync,
lastLine, runResult, self.postMessage).
services/tmc/src/components/AnswerBrowserExercise/styles.ts-145-145 (1)

145-145: ⚠️ Potential issue | 🟡 Minor

Invalid CSS: border shorthand does not accept four values.

border: 4px 4px 0 0; is invalid — the border shorthand expects <width> <style> <color>. This likely should be border-radius to round the top corners of the output container.

Proposed fix
-  border: 4px 4px 0 0;
+  border-radius: 4px 4px 0 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/styles.ts` at line 145,
Replace the invalid CSS shorthand "border: 4px 4px 0 0;" with a border-radius
declaration: locate the offending rule in
services/tmc/src/components/AnswerBrowserExercise/styles.ts (the styled element
containing that line) and change it to use "border-radius" with explicit units
(e.g. 4px 4px 0 0) so the top corners are rounded correctly.
services/tmc/src/components/AnswerBrowserExercise/AnswerBrowserExercise.tsx-47-49 (1)

47-49: ⚠️ Potential issue | 🟡 Minor

Add a fallback string to t("no-exercise-files").

Every other t() call in this PR includes a fallback string. Without one here, a missing or not-yet-loaded translation key renders the raw key "no-exercise-files" to the user.

🐛 Proposed fix
- return <div>{t("no-exercise-files")}</div>
+ return <div>{t("no-exercise-files", "No exercise files found.")}</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/AnswerBrowserExercise.tsx`
around lines 47 - 49, The UI returns t("no-exercise-files") when editorFiles is
empty; add a fallback string to that translation call in the
AnswerBrowserExercise component so a human-readable message is shown if the key
is missing or not loaded (update the t invocation inside the editorFiles.length
=== 0 return to include a second argument like "No exercise files available").
Ensure you modify the t(...) call used in the AnswerBrowserExercise component
(where editorFiles is checked) to include the fallback string.
🧹 Nitpick comments (22)
services/tmc/SUBMISSION-PLAN.md (1)

20-20: Remove --- section dividers — the ## headings already provide structure.

The document uses seven --- horizontal rules purely as visual separators between sections. The existing ##/### heading hierarchy already communicates section boundaries, making these dividers redundant noise.

♻️ Example removal (pattern repeats at lines 34, 56, 74, 86, 103, 117)
 ### Routes we must not break
 ...
 So we can change **internal** behavior of the grade route (e.g. how we prepare or score), but not the API contract.
-
----
-
 ## Why "submission still successful" when tests fail

As per coding guidelines, avoid banner-style section divider comments — "long separator bars, repeated dashes" — and prefer clearer naming and smaller focused sections instead.

Also applies to: 34-34, 56-56, 74-74, 86-86, 103-103, 117-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/SUBMISSION-PLAN.md` at line 20, Remove all standalone '---'
horizontal-rule lines from services/tmc/SUBMISSION-PLAN.md (they appear after
the headings at lines referenced in the review) so the document relies on the
existing '##' and '###' headings for structure; edit the file to delete each
'---' divider instance (the repeated three-dash lines) and ensure no additional
whitespace or leftover separators remain between heading sections.
services/tmc/src/components/AnswerBrowserExercise/utils/formatLogsForDisplay.ts (1)

1-6: Minor: spurious blank line in method chain and terse destructuring names.

The blank line between Object.entries(logs) and .map(...) is unusual and likely accidental. k/v could be key/value for a marginal readability gain.

♻️ Proposed fix
 export function formatLogsForDisplay(logs: Record<string, string>): string {
-  return Object.entries(logs)
-
-    .map(([k, v]) => `[${k}]\n${v}`)
+  return Object.entries(logs)
+    .map(([key, value]) => `[${key}]\n${value}`)
     .join("\n\n")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/utils/formatLogsForDisplay.ts`
around lines 1 - 6, In formatLogsForDisplay, remove the spurious blank line in
the method chain after Object.entries(logs) and rename the destructured
variables in the map from k and v to more descriptive names (e.g., key and
value) so the chain reads Object.entries(logs).map(([key, value]) =>
`[${key}]\n${value}`).join("\n\n") for improved readability.
services/tmc/src/components/AnswerBrowserExercise/utils/extensionToLanguage.ts (1)

3-3: Prefer strict equality === over ==.

lastIndexOf always returns a number, so this is functionally safe, but using === is the idiomatic TypeScript convention and avoids implicit type coercion.

♻️ Proposed fix
-  if (separator == -1) {
+  if (separator === -1) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/utils/extensionToLanguage.ts`
at line 3, Update the equality check in extensionToLanguage.ts to use strict
equality: replace the loose comparison "if (separator == -1)" with a strict
comparison "if (separator === -1)" so the code follows TypeScript/JavaScript
idioms and avoids implicit coercion; locate the check referencing the variable
"separator" in the function that parses the extension.
services/tmc/src/components/AnswerBrowserExercise/components/ActionButtons.tsx (1)

60-60: ?? undefined is redundant.

testUnavailableReason is already typed string | undefined, so ?? undefined is a no-op.

Proposed fix
-            title={p.testUnavailableReason ?? undefined}
+            title={p.testUnavailableReason}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/components/ActionButtons.tsx`
at line 60, The title prop in ActionButtons (where you set
title={p.testUnavailableReason ?? undefined}) uses a redundant nullish
coalescing to undefined; remove the "?? undefined" and pass the value directly
(title={p.testUnavailableReason}) in the component so the prop retains its
existing string | undefined type without the no-op operator.
services/tmc/src/components/AnswerBrowserExercise/hooks/useEditorState.ts (1)

3-3: Prefer a targeted lodash import for better tree-shaking.

import _ from "lodash" pulls in the entire lodash bundle. Since only cloneDeep is used, import it directly.

Proposed fix
-import _ from "lodash"
+import cloneDeep from "lodash/cloneDeep"

Then replace _.cloneDeep(...) with cloneDeep(...) throughout the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/hooks/useEditorState.ts` at
line 3, The file currently imports the entire lodash bundle via "import _ from
'lodash'"; replace that with a targeted import of cloneDeep (e.g., import {
cloneDeep } from 'lodash' or import cloneDeep from 'lodash/cloneDeep') and then
update all usages of _.cloneDeep(...) in useEditorState (the hook in
useEditorState.ts) to call cloneDeep(...) instead so tree-shaking can remove
unused lodash code.
services/tmc/src/components/ViewSubmission.tsx (1)

8-20: normalizeSubmission relies on an unsafe double cast — add a runtime guard.

Line 12 casts through unknown, which bypasses type safety entirely. The subsequent check on raw.private_spec.type is good, but if the input is something truly unexpected (e.g., null), accessing raw?.private_spec could still produce surprising results. Consider adding an explicit typeof submission === "object" guard before the cast, or documenting that this is intentionally defensive.

That said, the function is clearly a compatibility shim and the intent is well-documented by the comment on line 7 — this is a minor concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/ViewSubmission.tsx` around lines 8 - 20,
normalizeSubmission uses an unsafe double cast through unknown; add a runtime
guard to avoid accessing properties on non-objects: first check that submission
is an object and not null (e.g., typeof submission === "object" && submission
!== null) before treating it as having private_spec, then verify 'private_spec'
in submission and that private_spec is an object before checking
private_spec.type; update the logic inside normalizeSubmission (referencing the
normalizeSubmission function and the raw/private_spec checks) to perform these
guards and only then return raw.private_spec when its type is "browser" or
"editor", otherwise return the original submission.
services/tmc/src/components/StateRenderer.tsx (1)

66-84: The fake-state adapter is fragile and hard to follow — consider documenting the invariant.

setStateForViewSubmission synthesizes an "answer-exercise" state so that AnswerBrowserExercise and its hooks can operate transparently in a "view-submission" context. This is clever but creates a coupling where any hook that inspects view_type (like useEditorState) must produce a result that this adapter knows how to reverse-translate.

A brief comment at the declaration site explaining the contract (e.g., "The inner updater must return an answer-exercise state or null; other view_types are not expected") would help future maintainers avoid subtle breakage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/StateRenderer.tsx` around lines 66 - 84, The
adapter setStateForViewSubmission synthesizes a fake ExerciseIframeState with
view_type "answer-exercise" so AnswerBrowserExercise and hooks like
useEditorState can run inside a "view-submission" state; document this invariant
at the setStateForViewSubmission declaration: explain that the updater will
receive a fake "answer-exercise" state
(public_spec/user_answer/previous_submission) and must return either an
"answer-exercise" state (so the adapter will copy result.user_answer back into
prev.submission), null/undefined (no-op), or a non-"answer-exercise" state
(which will replace the state directly), and note that other view_type values
are not expected—add this concise comment so future maintainers know the
contract between setStateForViewSubmission, AnswerBrowserExercise, and
useEditorState.
services/tmc/src/components/AnswerBrowserExercise/components/EditorSection.tsx (1)

23-33: Deep-cloning the entire file array on every keystroke is costly.

_.cloneDeep(editorFiles) runs on each onChange event (i.e., every character typed). For exercises with many or large files, this could cause noticeable input lag. Consider a shallow clone with a targeted content update instead, since only one file's contents string changes.

Proposed fix
   const onChange = (newContents: string | undefined) => {
     if (readOnly || newContents === undefined) {
       return
     }
-    const newState = _.cloneDeep(editorFiles)
-    const changed = newState.find((ef) => ef.filepath === filepath)
-    if (changed) {
-      changed.contents = newContents
-    }
+    const newState = editorFiles.map((ef) =>
+      ef.filepath === filepath ? { ...ef, contents: newContents } : ef,
+    )
     setEditorState(newState)
   }

This avoids the lodash dependency in this file entirely and is O(n) shallow rather than O(n·m) deep.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/components/EditorSection.tsx`
around lines 23 - 33, onChange currently deep-clones editorFiles on every
keystroke (via _.cloneDeep(editorFiles)), which is expensive; instead perform a
shallow update: create a new array from editorFiles (e.g., [...editorFiles]),
find the target entry by filepath, and replace only that entry (or copy that
object and set its contents) before calling setEditorState; update the onChange
function to remove the _.cloneDeep call and avoid modifying existing objects
in-place so only the single file's contents changes while preserving referential
identity for other entries.
services/tmc/src/util/pyodideLoader.ts (1)

33-36: Existing <script> tag found does not guarantee the script has finished loading.

If loadScript is ever called outside the getPyodide singleton guard (or the guard is refactored), finding an existing <script> tag and resolving immediately can race against a still-loading script. Currently safe because getPyodide ensures a single call, but the helper is fragile if reused. Consider listening for the load event on the existing element before resolving.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/util/pyodideLoader.ts` around lines 33 - 36, The current
loadScript helper in pyodideLoader.ts finds an existing script element via
document.querySelector(`script[src="${src}"]`) and calls resolve immediately,
which can race if that script is still loading; change the logic in loadScript
so that when an existing element is found (the existing variable) you check its
load state and if it hasn't finished attach 'load' and 'error' listeners to that
element and only call resolve() on the load event (or reject on error); if the
element is already complete (use readyState/complete flag or a data-loaded
attribute), resolve immediately. Ensure you update any code paths in getPyodide
that rely on loadScript to still work with the new listener-based behavior.
services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts (1)

5-8: getRunWorkerUrl silently falls back to an empty base path.

If NEXT_PUBLIC_BASE_PATH is not set and the app is deployed under a subpath, the worker URL will be incorrect. This is likely fine for the common case, but consider logging a warning in development when the env var is absent so misconfigurations are caught early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts`
around lines 5 - 8, getRunWorkerUrl currently silently falls back to an empty
base when NEXT_PUBLIC_BASE_PATH is missing; change getRunWorkerUrl to detect
when process.env.NEXT_PUBLIC_BASE_PATH is undefined (and process.env.NODE_ENV
!== "production" / running in development) and emit a clear warning (e.g.,
console.warn) that the base path is missing so deployments under a subpath are
caught early, while preserving the existing return value `${base ??
""}/runWorker.js`.
services/tmc/src/tmc/browserTestScript.ts (1)

339-380: Generated Python code is dense — consider breaking the test-runner scaffold into a .py template file.

The inline Python in inlineAndPatchTestSources (particularly the ~40-line test code template and the long list comprehension on line 376) is difficult to read, lint, or unit-test as a single template string. Extracting it into a separate .py resource file loaded at build time would:

  • Allow Python linters/formatters to validate the template
  • Make the logic easier to maintain
  • Reduce risk of escaping bugs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/tmc/browserTestScript.ts` around lines 339 - 380, The inline
Python template assigned to testCode inside inlineAndPatchTestSources is too
large and should be moved into a standalone .py template file; update the code
so testCode is loaded from that resource (e.g., readFileSync of a template like
tmc_test_runner_template.py) and then programmatically inject dynamic fragments
using the existing helpers (stringifyPythonCode, patchTmcFile, patchTestSource)
before sending to the runner. Specifically: create the .py template containing
the scaffold (imports, __wrap_import, tmc_webeditor setup, TMCTestRunner
invocation and the results list comprehension) with placeholders for
__webeditor_user_code_b64 and the patched module/test blocks, replace the inline
template in browserTestScript.ts (the testCode string) to read the file and
perform placeholder substitutions for the outputs of
stringifyPythonCode(patchTmcFile(...)) and patchTestSource(test,...), and ensure
behavior around inspect.getsource and final JSON print remains identical.
services/tmc/src/util/pyodideConfig.ts (1)

1-8: Version duplication across workers undermines the "single source of truth" claim.

Both public/browserTestWorker.js and public/runWorker.js hardcode PYODIDE_CDN_VERSION = "0.29.3" independently. The comment in each file acknowledges the need to keep them in sync with util/pyodideConfig.ts, but this manual synchronization is error-prone and will eventually drift. Consider generating the worker files at build time (e.g., via a template or environment-variable injection) so the version is truly defined once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/util/pyodideConfig.ts` around lines 1 - 8, The worker files
duplicate the Pyodide version (PYODIDE_CDN_VERSION) instead of using the single
source PYODIDE_VERSION from pyodideConfig.ts; fix by removing the hardcoded
PYODIDE_CDN_VERSION in the workers and make them consume the canonical
PYODIDE_VERSION at build time (either by injecting process.env.PYODIDE_VERSION
into the worker bundle via your build tool or by generating the worker sources
from a template that substitutes PYODIDE_VERSION from util/pyodideConfig.ts),
update public worker code to construct PYODIDE_INDEX_URL/PYODIDE_SCRIPT_URL from
that injected PYODIDE_VERSION (or import the config if your bundler supports
it), and remove the manual sync comment so the version truly lives only in
PYODIDE_VERSION.
services/tmc/src/components/AnswerBrowserExercise/browserTest/pythonAdapter.ts (2)

23-23: Hardcoded English string in getCannotRunMessage.

"Only Python files can be tested." is a user-visible string. Since this is a plain function (not a React component), useTranslation can't be used directly here. Consider having the adapter return a translation key (e.g., "browser-test-python-only") and translating at the call site.

As per coding guidelines, "Make sure to use the useTranslation hook for all text that is visible to the user."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/browserTest/pythonAdapter.ts`
at line 23, Replace the hardcoded English message returned by
getCannotRunMessage with a translation key string (e.g.,
"browser-test-python-only") so the adapter returns a key rather than user-facing
text; update getCannotRunMessage in pythonAdapter.ts to return that key, and
ensure callers translate it with useTranslation (t("browser-test-python-only"))
at the UI/render site where the message is shown.

12-16: unescape() is deprecated; consider a modern replacement for UTF-8 to base64 conversion.

unescape(encodeURIComponent(…)) works but uses a deprecated function. However, the suggested TextEncoder + spread operator approach has a limitation: String.fromCharCode(...bytes) fails on large inputs (call stack exceeded). Use one of these instead:

Option 1: Chunk the byte array (works in all browsers):

const bytes = new TextEncoder().encode(userCode)
let base64Code = ""
const chunk = 0x8000
for (let i = 0; i < bytes.length; i += chunk) {
  base64Code += String.fromCharCode(...bytes.subarray(i, i + chunk))
}
const safeBase64 = btoa(base64Code).replace(/\+/g, "-").replace(/\//g, "_")

Option 2: Use Uint8Array.toBase64() (modern browsers only):

const base64Code = btoa(String.fromCharCode(...new TextEncoder().encode(userCode)))

Also, the escaping on line 14 (replace(/\\/g, …).replace(/"/g, …)) is a no-op since base64 output only contains A-Z, a-z, 0-9, +, /, and =. It can be safely removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/browserTest/pythonAdapter.ts`
around lines 12 - 16, The buildScript function uses deprecated
unescape(encodeURIComponent(...)) and unnecessary escaping of base64; replace
that with a UTF-8 -> base64 conversion using TextEncoder and chunking to avoid
call stack issues (encode userCode to a Uint8Array, convert in chunks to a
string with String.fromCharCode on subarrays, then btoa that result) and remove
the replace(/\\/g, ...) and replace(/"/g, ...) since base64 won't contain those
characters; update buildScript to produce __webeditor_user_code_b64 =
"<base64>"\n${specScript} using the new base64 output and ensure any URL-safe
replacement (e.g., '+' -> '-', '/' -> '_') only if required by downstream
consumers.
services/tmc/src/components/AnswerBrowserExercise/styles.ts (2)

83-97: ResetButton duplicates StyledButton base styles.

ResetButton re-declares the same margin, cursor, border, border-radius, padding, font-size, and font-weight that StyledButton already defines. Consider extending StyledButton to reduce duplication.

Proposed fix
-export const ResetButton = styled.button`
-  margin: 0.5em;
-  margin-left: 10px;
-  cursor: pointer;
-  border: none;
-  border-radius: 4px;
-  padding: 8px 16px;
-  font-size: 0.875rem;
-  font-weight: 500;
-  background-color: `#ebebeb`;
-  color: `#252525`;
+export const ResetButton = styled(StyledButton)`
+  margin-left: 10px;
+  background-color: `#ebebeb`;
+  color: `#252525`;
   &:hover {
     background-color: `#d5d5d5`;
   }
 `
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/styles.ts` around lines 83
- 97, ResetButton duplicates base styles already defined on StyledButton; change
ResetButton to extend StyledButton (e.g., styled(StyledButton)) and only
override the differing properties (background-color, color, margin-left and
:hover background-color) so you remove duplicated
margin/cursor/border/border-radius/padding/font-size/font-weight declarations
while keeping the same visual behavior; update the declaration that currently
exports ResetButton to use StyledButton as the base.

1-259: Prefer theme colors and fonts over hardcoded values.

This file has numerous hardcoded color hex values (#0275d8, #ff7518, #4caf50, #f44336, #fff, #252525, #e8e8e8, etc.) and repeated monospace font stacks. The coding guidelines recommend using baseTheme.colors and primaryFont from the theme where possible. Consider extracting at least the primary palette values (brand blue, warning orange, success green, error red, surface/text grays) into theme constants so they're consistent project-wide and easier to maintain.

As per coding guidelines, "If a component uses colors, use colors from the theme if possible." and "If a component uses fonts, use fonts from the theme if possible."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/styles.ts` around lines 1 -
259, The file hardcodes many colors and font stacks; update styled components to
use the theme's color palette and primary font instead of literals: import/use
the theme (p.theme.baseTheme.colors and p.theme.primaryFont) and replace hex
values in components like RunButton, StopButton, TestButton, ResetButton,
OutputHeader, StdinWaitingBanner, TestResultCard, TestResultHeader, OutputBody,
OutputPre, StdinSubmittedLine, StdinInput, etc., mapping each hardcoded semantic
role (brand/primary, warning, success, error, surface/text/gray) to the
appropriate baseTheme.colors key and replace all repeated monospace font-family
strings with p.theme.primaryFont (or a theme font stack) so components use theme
colors/fonts consistently.
services/tmc/src/components/AnswerBrowserExercise/components/TestResultsContent.tsx (1)

16-28: testResults.status is rendered as raw text—consider translating it.

When testResults.testResults is empty, the raw status string (e.g., "COMPILE_FAILED", "TESTRUN_INTERRUPTED") is shown directly to the user. Consider mapping these status values to translated, human-readable labels via useTranslation.

As per coding guidelines, "Make sure to use the useTranslation hook for all text that is visible to the user."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/components/TestResultsContent.tsx`
around lines 16 - 28, The component TestResultsContent currently renders raw
machine status strings (testResults.status) when testResults.testResults is
empty; update it to use the i18n hook by importing and calling useTranslation
and map status values to human-readable labels (e.g., via a small STATUS_LABELS
map or a switch inside TestResultsContent) and render t(STATUS_LABELS[status] ||
status) instead of the raw testResults.status; keep the existing logs rendering
(formatLogsForDisplay) unchanged and ensure the translation keys exist for
values like "COMPILE_FAILED" and "TESTRUN_INTERRUPTED".
services/tmc/src/components/AnswerBrowserExercise/components/TestResultCard.tsx (1)

14-18: User-visible "PASS"/"FAIL" strings should use useTranslation.

Line 17 renders hardcoded English text "PASS" / "FAIL" directly in the UI. Per coding guidelines, all user-visible text should go through the useTranslation hook.

Proposed fix
+import { useTranslation } from "react-i18next"
+
-export const TestResultCard: React.FC<TestResultCardProps> = (p) => (
-  <StyledCard passed={p.passed} data-cy="test-result">
-    <TestResultHeader passed={p.passed}>
-      {p.passed ? "PASS" : "FAIL"}: {p.name}
-    </TestResultHeader>
+export const TestResultCard: React.FC<TestResultCardProps> = (p) => {
+  const { t } = useTranslation()
+  return (
+    <StyledCard passed={p.passed} data-cy="test-result">
+      <TestResultHeader passed={p.passed}>
+        {p.passed ? t("test-pass") : t("test-fail")}: {p.name}
+      </TestResultHeader>

As per coding guidelines, "Make sure to use the useTranslation hook for all text that is visible to the user."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/components/TestResultCard.tsx`
around lines 14 - 18, The TestResultCard currently renders hardcoded
"PASS"/"FAIL"; import and use the useTranslation hook inside TestResultCard
(e.g., const { t } = useTranslation()) and replace the hardcoded strings in
TestResultHeader with t(...) calls (e.g., t('testResult.pass') and
t('testResult.fail') or whichever translation keys you choose), ensuring you
reference p.passed when selecting the key; update any related tests or add
translation entries for the chosen keys.
services/tmc/src/components/AnswerBrowserExercise/components/RunOutputContent.tsx (2)

84-86: Remove the redundant scrollIntoView options comment.

The /* scrollIntoView options: ... */ comment only labels the parameter types already visible in the code. It conveys no non-obvious intent, rationale, or constraint. The eslint-disable comment on line 85 already provides the needed context.

♻️ Proposed fix
  useEffect(() => {
    if (waitingForInput && inputRef.current) {
-     /* scrollIntoView options: ScrollBehavior and ScrollLogicalPosition (not user-facing) */
      // eslint-disable-next-line i18next/no-literal-string
      inputRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" })
    }
  }, [waitingForInput])

As per coding guidelines, "Do not add comments that simply restate what the code already says."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/components/RunOutputContent.tsx`
around lines 84 - 86, Remove the redundant comment above the scrollIntoView
call: delete the line "/* scrollIntoView options: ScrollBehavior and
ScrollLogicalPosition (not user-facing) */" and keep the existing eslint-disable
comment and the call inputRef.current.scrollIntoView({ behavior: "smooth",
block: "nearest" }) unchanged so the code remains clear and lint-suppressed
where intended.

67-73: Consider refocusing the input after stdin submission and on waiting state.

handleSubmit clears stdinValue but leaves focus elsewhere; useEffect only scrolls without calling .focus(). For programs with multiple sequential input() calls, the user must manually re-click the field each time.

✨ Proposed improvement
  const handleSubmit = () => {
    if (submitStdinLine == null) {
      return
    }
    submitStdinLine(stdinValue)
    setStdinValue("")
+   inputRef.current?.focus()
  }
  useEffect(() => {
    if (waitingForInput && inputRef.current) {
      inputRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" })
+     inputRef.current.focus()
    }
  }, [waitingForInput])

Also applies to: 82-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/tmc/src/components/AnswerBrowserExercise/components/RunOutputContent.tsx`
around lines 67 - 73, The input is not refocused after submission and while
waiting, making sequential input calls require manual clicks; update
handleSubmit to, after calling submitStdinLine and clearing via
setStdinValue(""), call focus on the input ref (e.g.,
stdinInputRef.current?.focus()) and ensure stdinInputRef is attached to the
input element, and also modify the existing useEffect that currently only
scrolls (and the analogous logic around the waiting state between lines ~82-88)
to call stdinInputRef.current?.focus() when the component is waiting for the
next stdin or when stdinValue is cleared so the field regains focus
automatically; guard all ref accesses for null to avoid runtime errors.
services/tmc/src/components/AnswerBrowserExercise/browserTest/index.ts (1)

10-12: Redundant explicit type annotations when function is already typed as GetBrowserTestAdapter.

Since getBrowserTestAdapter is typed via GetBrowserTestAdapter, the parameter type and return type annotations on lines 11–12 are inferred automatically.

♻️ Proposed simplification
- export const getBrowserTestAdapter: GetBrowserTestAdapter = (
-   runtime: BrowserTestSpec["runtime"],
- ): BrowserTestRunnerAdapter | null => {
+ export const getBrowserTestAdapter: GetBrowserTestAdapter = (runtime) => {
    switch (runtime) {
      case "python":
        return getPythonBrowserTestAdapter()
      default:
        return null
    }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/browserTest/index.ts`
around lines 10 - 12, The function getBrowserTestAdapter already has the
GetBrowserTestAdapter type, so remove the redundant explicit parameter and
return type annotations: drop the ": BrowserTestSpec[\"runtime\"]" on the
runtime parameter and the ": BrowserTestRunnerAdapter | null" return annotation
so the compiler can infer them from GetBrowserTestAdapter; keep the function
name getBrowserTestAdapter and the runtime parameter identifier unchanged to
preserve usage sites.
services/tmc/src/components/AnswerBrowserExercise/AnswerBrowserExercise.tsx (1)

82-112: Replace hardcoded hex colors in the grading banner with theme tokens.

Lines 88–90 use raw hex values (#dcfce7, #fef3c7, #166534, #92400e) instead of colours drawn from baseTheme. Importing the theme palette keeps colours consistent with the rest of the UI and makes future theming straightforward.

♻️ Proposed fix (example — adjust to actual token names)
+ import { baseTheme } from "@/shared-module/common/styles"
  className={css`
    margin-bottom: 1rem;
    padding: 0.75rem 1rem;
    border-radius: 0.5rem;
-   background-color: ${passed ? "#dcfce7" : "#fef3c7"};
-   color: ${passed ? "#166534" : "#92400e"};
+   background-color: ${passed ? baseTheme.colors.green[100] : baseTheme.colors.yellow[100]};
+   color: ${passed ? baseTheme.colors.green[700] : baseTheme.colors.yellow[800]};
    font-size: 0.9375rem;
  `}

As per coding guidelines, "If a component uses colors, use colors from the theme if possible. Example: import { baseTheme } from "@/shared-module/common/styles"."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/AnswerBrowserExercise.tsx`
around lines 82 - 112, The grading banner in AnswerBrowserExercise uses
hardcoded hex colors (background-color: `#dcfce7` / `#fef3c7` and color: `#166534` /
`#92400e`); import the theme (e.g. baseTheme from "@/shared-module/common/styles")
and replace those raw hex values with the appropriate theme tokens (use semantic
success/warning or green/yellow palette tokens) in the background-color and
color expressions so the banner uses baseTheme color tokens instead of literals,
updating the component where grading and passed are computed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (4)
services/tmc/src/util/pyodideLoader.ts (1)

47-65: Rejected-promise caching addressed — LGTM.

The previous concern about pyodidePromise permanently holding a rejected promise has been resolved. The catch block correctly resets pyodidePromise = null before re-throwing, allowing callers to retry after a transient failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/util/pyodideLoader.ts` around lines 47 - 65, The getPyodide
function correctly avoids caching a permanently rejected promise by resetting
the module-level pyodidePromise to null inside the catch block before
re-throwing; no code changes are required—leave getPyodide, the pyodidePromise
handling, and the catch/reset logic as implemented.
services/tmc/public/runWorker.js (2)

118-134: Template-based injection applied — LGTM.

The user script is now delivered via base64 (self.userScriptB64) and the Python template is fetched as a separate file rather than built through string concatenation. This resolves the fragility and double-escaping concerns from the prior review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorker.js` around lines 118 - 134, The current
post-run flush incorrectly treats stderr as stdout: in the promise handler after
pyodide.runPythonAsync, when handling flushErr from stderrDecoder you append it
to stdout and post it with type "stdout"; change this to append flushErr to the
stderr accumulator (or a new stderr variable) and post it with type "stderr"
using the same self.postMessage call pattern; update references around
stdoutDecoder, stderrDecoder, flushOut, flushErr, stdout, and self.postMessage
to reflect the correct stream handling.

99-116: TextDecoder fix applied — LGTM.

Multi-byte UTF-8 is now decoded correctly via TextDecoder with { stream: true }, and the decoder flush is called after the run completes. This resolves the previous mojibake concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorker.js` around lines 99 - 116, The stderr raw
handler currently decodes into stderrDecoder but incorrectly appends to the
stdout buffer and emits messages as type "stdout"; update the pyodide.setStderr
raw function (the raw callback that uses stderrDecoder) to append decoded chunks
to the stderr variable (not stdout) and postMessage with type "stderr"
(preserving the same chunk payload), mirroring the logic used in
pyodide.setStdout.
services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts (1)

96-105: Buffered stdout flushing applied — LGTM.

The per-byte re-render concern from the prior review is resolved. Chunks are accumulated in runOutputBufferRef and flushed at most every 50 ms (or eagerly on newlines and stdin requests), greatly reducing React update frequency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts`
around lines 96 - 105, The buffering implementation in useRunOutput's
handleMessage correctly accumulates stdout chunks into runOutputBufferRef and
only triggers flushStdout on newlines or the periodic timer, so no code change
is required; keep the current logic in handleMessage (referencing
runOutputBufferRef and flushStdout) to maintain reduced React re-render
frequency and ensure eager flushing on newline or stdin requests.
🧹 Nitpick comments (1)
services/tmc/public/runWorker.js (1)

9-10: PYODIDE_CDN_VERSION is duplicated from pyodideConfig.ts with no enforcement.

The comment on line 6 manually instructs keeping these in sync. In a plain Web Worker script that cannot import, this is understandable, but the risk of version drift between this file and pyodideConfig.ts is real (e.g., the TS module gets bumped but this file is overlooked).

Consider adding a build-time check (e.g., a lint rule, a CI script, or a simple grep-based assertion) that asserts both constants share the same version string, surfacing mismatches before they reach production.

Would you like me to generate a CI/lint script that verifies the two version constants are kept in sync?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorker.js` around lines 9 - 10, PYODIDE_CDN_VERSION
and INDEX_URL in runWorker.js are duplicated from pyodideConfig.ts and can
drift; add a build-time consistency check that fails CI if versions diverge by
extracting the PYODIDE_CDN_VERSION value from both runWorker.js and
pyodideConfig.ts (or the INDEX_URL in runWorker.js) and comparing them.
Implement this as a small script or CI step (e.g., grep/awk/node) that reads the
PYODIDE_CDN_VERSION constant in pyodideConfig.ts and the
PYODIDE_CDN_VERSION/INDEX_URL in services/tmc/public/runWorker.js and exits
non‑zero on mismatch, so the pipeline prevents merging when the two values
differ.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/tmc/public/runWorker.js`:
- Around line 38-44: The getPyodide function caches pyodidePromise even when
loadPyodide rejects, preventing recovery; update getPyodide so the Promise
assigned to pyodidePromise handles rejection by resetting pyodidePromise to null
(e.g., attach a .catch that sets pyodidePromise = null and rethrows the error)
before returning it, ensuring subsequent calls to getPyodide will retry
loadPyodide; refer to pyodidePromise, getPyodide, and loadPyodide in your
change.
- Around line 27-36: The getTemplate function currently assigns templatePromise
to the fetch chain and never clears it on failure, so a rejected fetch
permanently poisons templatePromise; modify getTemplate (and the fetch chain for
templatePromise) to check response.ok and, on any failure or thrown error, reset
templatePromise back to null before rethrowing the error so subsequent run calls
can retry; specifically update the fetch promise returned by getTemplate
(referencing templatePromise and getTemplate) to attach a .then that verifies
r.ok -> r.text() and a .catch (or finally on rejection) that sets
templatePromise = null and rethrows the original error.

In `@services/tmc/public/runWorkerTemplate.py`:
- Line 64: The local variable `t` from the tuple returned by sys.exc_info() is
unused in the exception handling in runWorkerTemplate.py; update both
occurrences where you unpack as `t, v, tb = sys.exc_info()` (the one at the
current spot and the identical pattern near line 40) to prefix the unused name
with an underscore (e.g., `_t, v, tb = sys.exc_info()`), leaving `v` and `tb`
unchanged so linters stop complaining while the code behavior remains the same.
- Around line 47-49: Inside execute(), setting __name__ = "__main__" only
creates a local variable and doesn't affect exec; change exec to run user_code
with a globals dict where '__name__' is set to "__main__" (e.g., build or copy a
globals dict, set globals_dict['__name__']="__main__", then call exec(user_code,
globals_dict)), so code guarded by if __name__ == "__main__": will execute;
update the call site in execute and ensure user_code is passed to exec with that
modified globals dict rather than relying on a local variable.
- Line 72: The create_task call currently discards the Task returned by
asyncio.create_task(wrap_execution()), which lets the task be garbage-collected
and cancelled; capture and retain that Task (e.g., assign it to a variable like
task = asyncio.create_task(wrap_execution()) or store it on a long-lived object)
so the Task returned by asyncio.create_task is strongly referenced until
wrap_execution completes; update the code around the
asyncio.create_task(wrap_execution()) invocation to keep that reference
(referencing the wrap_execution() call and the created Task variable).
- Around line 16-21: The generic_visit method in runWorkerTemplate.py is
inappropriately mutating all AST string constants via re.sub(r"[\n] ", "\n",
node.value), which corrupts legitimate user strings; remove the substitution and
the replacement ast.Constant creation/return so generic_visit simply delegates
to super() (i.e., stop altering ast.Constant string nodes in generic_visit), and
if the re import becomes unused after removing this code, delete the re import
as well; reference the generic_visit method and the ast.Constant handling in
your changes.
- Around line 32-35: The compiled AST can contain top-level await (PatchCode
rewrites input() calls to await input_impl()), so change the compile(...) call
that builds user_code to pass the ast.PyCF_ALLOW_TOP_LEVEL_AWAIT flag (i.e., use
compile(tree, " ", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)) so top-level
await expressions in the module are accepted; update the compile invocation near
the PatchCode().visit(tree) usage to include this flag.

In `@services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts`:
- Around line 160-167: The cleanup in the useEffect only clears
flushTimerRef.current but doesn't terminate workerRef.current, causing the
worker to keep running and handleMessage to reference stale closures; update the
effect cleanup to call the existing stopRun function (which properly terminates
workerRef.current, clears timers, and removes handlers) instead of manually
clearing flushTimerRef, or have the cleanup delegate to stopRun so teardown is
consistent and idempotent; reference stopRun, workerRef.current,
flushTimerRef.current, and handleMessage to locate and replace the current
cleanup logic.
- Around line 53-79: The runPython function can start a new run while a previous
one is still executing, causing the same worker to be reused and its message
handlers to be replaced which interleaves stdout/errors; fix by checking
runExecuting at the top of runPython and returning early (guard) OR explicitly
terminate and recreate the worker before starting a new run: if runExecuting is
true return (and ensure UI disables run trigger), or call
workerRef.current.terminate(), set workerRef.current = undefined, then new
Worker(getRunWorkerUrl()) and reassign handlers; ensure setRunExecuting is set
appropriately and existing finish() is called/used to clean up the previous run
state when terminating.

In `@services/tmc/src/util/pyodideLoader.ts`:
- Around line 27-45: The loadScript helper currently resolves immediately if it
finds an existing script tag, which breaks retries in getPyodide because the
existing tag may still be loading or previously errored; update loadScript to
check the existing element's load state and, if it isn't already successfully
loaded, attach onload and onerror handlers to that element (or inspect
script.readyState) so the returned Promise only resolves/rejects when that
script actually finishes; also ensure that when a previous load errored you
remove or replace the tag so subsequent calls can retry and that this behavior
coordinates with pyodidePromise and window.loadPyodide.

---

Duplicate comments:
In `@services/tmc/public/runWorker.js`:
- Around line 118-134: The current post-run flush incorrectly treats stderr as
stdout: in the promise handler after pyodide.runPythonAsync, when handling
flushErr from stderrDecoder you append it to stdout and post it with type
"stdout"; change this to append flushErr to the stderr accumulator (or a new
stderr variable) and post it with type "stderr" using the same self.postMessage
call pattern; update references around stdoutDecoder, stderrDecoder, flushOut,
flushErr, stdout, and self.postMessage to reflect the correct stream handling.
- Around line 99-116: The stderr raw handler currently decodes into
stderrDecoder but incorrectly appends to the stdout buffer and emits messages as
type "stdout"; update the pyodide.setStderr raw function (the raw callback that
uses stderrDecoder) to append decoded chunks to the stderr variable (not stdout)
and postMessage with type "stderr" (preserving the same chunk payload),
mirroring the logic used in pyodide.setStdout.

In `@services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts`:
- Around line 96-105: The buffering implementation in useRunOutput's
handleMessage correctly accumulates stdout chunks into runOutputBufferRef and
only triggers flushStdout on newlines or the periodic timer, so no code change
is required; keep the current logic in handleMessage (referencing
runOutputBufferRef and flushStdout) to maintain reduced React re-render
frequency and ensure eager flushing on newline or stdin requests.

In `@services/tmc/src/util/pyodideLoader.ts`:
- Around line 47-65: The getPyodide function correctly avoids caching a
permanently rejected promise by resetting the module-level pyodidePromise to
null inside the catch block before re-throwing; no code changes are
required—leave getPyodide, the pyodidePromise handling, and the catch/reset
logic as implemented.

---

Nitpick comments:
In `@services/tmc/public/runWorker.js`:
- Around line 9-10: PYODIDE_CDN_VERSION and INDEX_URL in runWorker.js are
duplicated from pyodideConfig.ts and can drift; add a build-time consistency
check that fails CI if versions diverge by extracting the PYODIDE_CDN_VERSION
value from both runWorker.js and pyodideConfig.ts (or the INDEX_URL in
runWorker.js) and comparing them. Implement this as a small script or CI step
(e.g., grep/awk/node) that reads the PYODIDE_CDN_VERSION constant in
pyodideConfig.ts and the PYODIDE_CDN_VERSION/INDEX_URL in
services/tmc/public/runWorker.js and exits non‑zero on mismatch, so the pipeline
prevents merging when the two values differ.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9aa527 and e6472af.

📒 Files selected for processing (8)
  • services/tmc/public/browserTestWorker.js
  • services/tmc/public/runWorker.js
  • services/tmc/public/runWorkerTemplate.py
  • services/tmc/src/components/AnswerBrowserExercise/components/RunOutputContent.tsx
  • services/tmc/src/components/AnswerBrowserExercise/hooks/useEditorState.ts
  • services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts
  • services/tmc/src/components/AnswerBrowserExercise/hooks/useTestRun.ts
  • services/tmc/src/util/pyodideLoader.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • services/tmc/src/components/AnswerBrowserExercise/hooks/useTestRun.ts
  • services/tmc/public/browserTestWorker.js
  • services/tmc/src/components/AnswerBrowserExercise/components/RunOutputContent.tsx
  • services/tmc/src/components/AnswerBrowserExercise/hooks/useEditorState.ts

@github-actions github-actions bot added the dependencies Pull requests that update a dependency file label Feb 25, 2026
@socket-security
Copy link

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​dompurify@​3.2.01001003950100
Added@​types/​supertest@​6.0.31001007181100
Added@​types/​katex@​0.16.71001007283100
Added@​types/​commonmark@​0.27.101001007281100
Added@​types/​luxon@​3.7.11001007783100
Addedcommonmark@​0.31.2961001008080
Addedpostcss@​8.5.6991008282100
Addeddompurify@​3.2.7981001008390
Addedkatex@​0.16.229410010098100

View full report

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (1)
services/tmc/public/browserTestWorker.js (1)

26-35: ⚠️ Potential issue | 🟡 Minor

Pipeline warning: stderr is captured but never forwarded or read.

stderr is accumulated in the setStderr batched handler but never posted to the main thread and never included in any message. Either forward it (like runWorker.js does with { type: "stderr" }) or remove the buffer to silence the warning and avoid misleading future readers.

💡 Proposed fix (forward stderr as a warning, consistent with runWorker.js)
       pyodide.setStderr({
         batched: function (msg) {
-          stderr += msg + "\n"
+          console.warn("[browserTestWorker stderr]", msg)
         },
       })

And remove var stderr = "" on line 26.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/browserTestWorker.js` around lines 26 - 35, The stderr
buffer is never used; remove the unused var stderr declaration and change the
pyodide.setStderr batched handler to forward messages to the main thread (e.g.,
postMessage({ type: "stderr" or "warning", message: msg })) instead of
accumulating them — update the pyodide.setStderr batched function in
browserTestWorker.js to mirror the runWorker.js pattern so stderr is emitted to
the main thread.
🧹 Nitpick comments (4)
shared-module/packages/common/src/utils/responseHeaders.js (1)

74-74: Remove the commented-out legacy CSP from Line 74.

Keeping the old CSP as an inline comment is noisy and can mislead future edits. Keep the active policy only, and retain the rationale comments above.

Suggested cleanup
-    value: "default-src * 'self' data: 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob:", // "default-src 'self'; connect-src 'self'; font-src 'self'; frame-src *; img-src 'self' data: blob: https://storage.googleapis.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline'",
+    value: "default-src * 'self' data: 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob:",

As per coding guidelines, "Prefer comment quality over comment quantity. Do not add comments that simply restate what the code already says..."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared-module/packages/common/src/utils/responseHeaders.js` at line 74,
Remove the commented-out legacy CSP from the 'value' property in
responseHeaders.js so only the active policy remains; locate the object with the
value: "default-src * 'self' data: 'unsafe-inline' 'unsafe-eval'; worker-src
'self' blob:" and delete the trailing commented legacy string (the old
"default-src 'self'..." content) while keeping any explanatory rationale
comments above intact.
services/tmc/public/runWorkerTemplate.py (2)

56-56: Use a descriptive filename in compile() for clearer tracebacks.

" " (a space) as the filename argument appears in user-facing tracebacks and error messages. A descriptive string like "<user-script>" improves debuggability.

💡 Proposed fix
-    user_code = compile(wrapped, " ", "exec")
+    user_code = compile(wrapped, "<user-script>", "exec")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorkerTemplate.py` at line 56, Replace the
non-descriptive filename used when compiling user code so tracebacks show a
meaningful source: change the compile call that creates user_code
(compile(wrapped, " ", "exec")) to use a descriptive filename like
"<user-script>" or "<user-code>" (referencing the variables wrapped and
user_code) so exceptions and tracebacks reference the user script name instead
of a blank space.

60-65: Broad except Exception is intentional here — acceptable for user-code sandboxing.

The catch is necessary to report any parse/transform error back through printError rather than crashing the worker. The Ruff BLE001 warning can be suppressed inline.

💡 Suppress the Ruff warning without changing behavior
-except Exception:
+except Exception:  # noqa: BLE001
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorkerTemplate.py` around lines 60 - 65, Update the
broad exception suppression to silence Ruff BLE001 without changing behavior:
leave the try/except and the existing error reporting (sys.exc_info,
traceback.extract_tb, printError, exit()) intact but add an inline ruff/noqa
comment to the except line (e.g., append a "# noqa: BLE001" or equivalent ruff
suppression) so the linter is suppressed while keeping the broad catch for
sandboxed user code.
services/tmc/package.json (1)

62-63: pyodide belongs in devDependencies, not dependencies.

Per next.config.js, Pyodide is explicitly excluded from the client/server bundle and loaded from a CDN at runtime. The npm package is used only at build time for the version JSON and TypeScript types. Listing it as a runtime dependency inflates the production install and may cause confusion.

💡 Proposed fix
-    "zstddec": "^0.1.0",
-    "pyodide": "^0.29.3"
+    "zstddec": "^0.1.0"
   },
   "devDependencies": {
+    "pyodide": "^0.29.3",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/package.json` around lines 62 - 63, Move the "pyodide" entry out
of dependencies into devDependencies in package.json: remove "pyodide":
"^0.29.3" from the dependencies block and add it under devDependencies so it's
only present at build time (per next.config.js which excludes Pyodide from the
runtime bundle); ensure package.json still contains "zstddec" in dependencies
and update any lockfile by running the appropriate install command afterwards.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/tmc/public/browserTestWorker.js`:
- Around line 13-19: getPyodide() currently caches a rejected promise in the
module-level variable pyodidePromise so any load failure blocks retries; change
the load logic so that when calling loadPyodide({ indexURL: PYODIDE_INDEX_URL })
you attach a .catch handler that sets pyodidePromise = null (so subsequent calls
retry) and then rethrows the error; update getPyodide (and the pyodidePromise
assignment) to use this fault-tolerant promise handling while keeping the
cached-success behavior.

In `@services/tmc/public/runWorker.js`:
- Around line 67-71: The parameter `tb` in the `self.printError` function is
unused; rename it to `_tb` (or prefix it with an underscore) in the
`self.printError = function (message, kind, line, tb)` signature to silence the
linter and signal intent, and ensure no other code relies on the original `tb`
name (update any callers if necessary); do not otherwise change the function
body that constructs `msg`, posts the `run_error` message, and sets
`runHadError`.
- Around line 73-75: The local accumulation buffer stderr is declared and
appended to but never read; remove the unused variable declaration "var stderr =
\"\"" and any assignments/reset of stderr (and the code that concatenates into
it) so only real-time postMessage({ type: "stderr", ... }) is relied upon;
search for the symbol stderr in runWorker.js and delete its declaration and any
writes (but do not change the postMessage calls or self.exit() behavior).

In `@services/tmc/public/runWorkerTemplate.py`:
- Around line 29-48: The ast.Module construction for variable wrapped is missing
the required type_ignores argument; update the ast.Module(...) call that wraps
the AsyncFunctionDef named "__run__" to include type_ignores=[] (i.e.,
ast.Module(body=[...], type_ignores=[])) so it conforms to Python 3.8+ AST API
before calling ast.fix_missing_locations(wrapped).

In `@services/tmc/scripts/inject-pyodide-version.cjs`:
- Line 27: The file ends without a trailing newline causing prettier to fail;
open the file containing the console.log statement console.log("Injected Pyodide
index URL:", indexUrl) and add a single newline character at the end of the file
(after that console.log line) so the file terminates with a newline.
- Around line 16-23: The injectIndexUrl function currently overwrites the file
even when the regex doesn't match; update it to first test for a match (e.g.,
use the same RegExp or compare content before/after) and only perform
fs.writeFileSync when a replacement actually occurred, and if no match is found
raise/throw an explicit error or log and exit so the build fails instead of
silently continuing; reference injectIndexUrl and indexUrlStr and the /var
PYODIDE_INDEX_URL = "[^"]*"/ pattern when making the change.
- Around line 4-5: The ESLint rule `@typescript-eslint/no-require-imports` is
incorrectly flagging require("fs") and require("path") in this CommonJS script;
add an override in the ESLint config for the *.cjs glob to disable that rule
(turn off `@typescript-eslint/no-require-imports` for .cjs files) so require() is
allowed in inject-pyodide-version.cjs rather than adding inline disables in the
file.

---

Duplicate comments:
In `@services/tmc/public/browserTestWorker.js`:
- Around line 26-35: The stderr buffer is never used; remove the unused var
stderr declaration and change the pyodide.setStderr batched handler to forward
messages to the main thread (e.g., postMessage({ type: "stderr" or "warning",
message: msg })) instead of accumulating them — update the pyodide.setStderr
batched function in browserTestWorker.js to mirror the runWorker.js pattern so
stderr is emitted to the main thread.

---

Nitpick comments:
In `@services/tmc/package.json`:
- Around line 62-63: Move the "pyodide" entry out of dependencies into
devDependencies in package.json: remove "pyodide": "^0.29.3" from the
dependencies block and add it under devDependencies so it's only present at
build time (per next.config.js which excludes Pyodide from the runtime bundle);
ensure package.json still contains "zstddec" in dependencies and update any
lockfile by running the appropriate install command afterwards.

In `@services/tmc/public/runWorkerTemplate.py`:
- Line 56: Replace the non-descriptive filename used when compiling user code so
tracebacks show a meaningful source: change the compile call that creates
user_code (compile(wrapped, " ", "exec")) to use a descriptive filename like
"<user-script>" or "<user-code>" (referencing the variables wrapped and
user_code) so exceptions and tracebacks reference the user script name instead
of a blank space.
- Around line 60-65: Update the broad exception suppression to silence Ruff
BLE001 without changing behavior: leave the try/except and the existing error
reporting (sys.exc_info, traceback.extract_tb, printError, exit()) intact but
add an inline ruff/noqa comment to the except line (e.g., append a "# noqa:
BLE001" or equivalent ruff suppression) so the linter is suppressed while
keeping the broad catch for sandboxed user code.

In `@shared-module/packages/common/src/utils/responseHeaders.js`:
- Line 74: Remove the commented-out legacy CSP from the 'value' property in
responseHeaders.js so only the active policy remains; locate the object with the
value: "default-src * 'self' data: 'unsafe-inline' 'unsafe-eval'; worker-src
'self' blob:" and delete the trailing commented legacy string (the old
"default-src 'self'..." content) while keeping any explanatory rationale
comments above intact.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6472af and fc905c8.

⛔ Files ignored due to path filters (1)
  • services/tmc/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • services/tmc/package.json
  • services/tmc/public/browserTestWorker.js
  • services/tmc/public/runWorker.js
  • services/tmc/public/runWorkerTemplate.py
  • services/tmc/scripts/inject-pyodide-version.cjs
  • services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts
  • services/tmc/src/util/pyodide-version.json
  • services/tmc/src/util/pyodideConfig.ts
  • services/tmc/src/util/pyodideLoader.ts
  • shared-module/packages/common/src/utils/responseHeaders.js
✅ Files skipped from review due to trivial changes (1)
  • services/tmc/src/util/pyodide-version.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • services/tmc/src/components/AnswerBrowserExercise/hooks/useRunOutput.ts
  • services/tmc/src/util/pyodideLoader.ts

Comment on lines +13 to +19
function getPyodide() {
if (pyodidePromise !== null) {
return pyodidePromise
}
pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL })
return pyodidePromise
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

pyodidePromise is permanently cached on rejection — worker cannot recover.

Unlike runWorker.js (which resets pyodidePromise = null in a .catch), this getPyodide() permanently holds a rejected promise. Any CDN or load failure makes all subsequent test runs fail immediately with no retry.

🐛 Proposed fix
 function getPyodide() {
   if (pyodidePromise !== null) {
     return pyodidePromise
   }
-  pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL })
-  return pyodidePromise
+  pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL }).catch(function (err) {
+    pyodidePromise = null
+    throw err
+  })
+  return pyodidePromise
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getPyodide() {
if (pyodidePromise !== null) {
return pyodidePromise
}
pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL })
return pyodidePromise
}
function getPyodide() {
if (pyodidePromise !== null) {
return pyodidePromise
}
pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL }).catch(function (err) {
pyodidePromise = null
throw err
})
return pyodidePromise
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/browserTestWorker.js` around lines 13 - 19, getPyodide()
currently caches a rejected promise in the module-level variable pyodidePromise
so any load failure blocks retries; change the load logic so that when calling
loadPyodide({ indexURL: PYODIDE_INDEX_URL }) you attach a .catch handler that
sets pyodidePromise = null (so subsequent calls retry) and then rethrows the
error; update getPyodide (and the pyodidePromise assignment) to use this
fault-tolerant promise handling while keeping the cached-success behavior.

Comment on lines +67 to +71
self.printError = function (message, kind, line, tb) {
var msg = kind + " on line " + line + ": " + message
self.postMessage({ type: "run_error", message: msg, output: stdout })
runHadError = true
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pipeline warning: unused tb parameter in printError.

tb (traceback frames) is accepted but never used. Prefix it with _ to silence the linter and signal intent.

💡 Proposed fix
-self.printError = function (message, kind, line, tb) {
+self.printError = function (message, kind, line, _tb) {
🧰 Tools
🪛 GitHub Actions: Code Style

[warning] 67-67: eslint: warning: 'tb' is defined but never used. (no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorker.js` around lines 67 - 71, The parameter `tb` in
the `self.printError` function is unused; rename it to `_tb` (or prefix it with
an underscore) in the `self.printError = function (message, kind, line, tb)`
signature to silence the linter and signal intent, and ensure no other code
relies on the original `tb` name (update any callers if necessary); do not
otherwise change the function body that constructs `msg`, posts the `run_error`
message, and sets `runHadError`.

Comment on lines +73 to +75
var runHadError = false
var stdout = ""
var stderr = ""
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pipeline warning: stderr accumulation buffer is never read.

stderr is collected byte-by-byte (line 125) but the accumulated string is never consumed — neither in self.exit() nor in error messages. The individual chunks are already forwarded in real-time via postMessage({ type: "stderr" }), so the buffer is redundant. Remove the variable and its reset assignments to fix the linter warning and reduce confusion.

💡 Proposed fix
 var runHadError = false
 var stdout = ""
-var stderr = ""
       stdout = ""
-      stderr = ""
       var stdoutDecoder = new TextDecoder("utf-8", { fatal: false })
       var stderrDecoder = new TextDecoder("utf-8", { fatal: false })
           if (chunk.length > 0) {
-            stderr += chunk
             self.postMessage({ type: "stderr", chunk: chunk })
           }
🧰 Tools
🪛 GitHub Actions: Code Style

[warning] 75-75: eslint: warning: 'stderr' is assigned a value but never used. (no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorker.js` around lines 73 - 75, The local
accumulation buffer stderr is declared and appended to but never read; remove
the unused variable declaration "var stderr = \"\"" and any assignments/reset of
stderr (and the code that concatenates into it) so only real-time postMessage({
type: "stderr", ... }) is relied upon; search for the symbol stderr in
runWorker.js and delete its declaration and any writes (but do not change the
postMessage calls or self.exit() behavior).

Comment on lines +29 to +48
wrapped = ast.Module(
body=[
ast.AsyncFunctionDef(
name="__run__",
args=ast.arguments(
posonlyargs=[],
args=[],
vararg=None,
kwonlyargs=[],
kw_defaults=[],
kwarg=None,
defaults=[],
),
body=tree.body,
decorator_list=[],
returns=None,
),
],
)
ast.fix_missing_locations(wrapped)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Python 3.13 ast.Module type_ignores required argument

💡 Result:

In Python 3.13, ast.Module’s constructor signature is ast.Module(body, type_ignores) (no default shown), so creating it with only body can raise:

TypeError: Module() missing required argument 'type_ignores' (docs.python.org)

Fix

Pass an explicit empty list (or your collected TypeIgnore nodes):

import ast

mod = ast.Module(body=[ast.Pass()], type_ignores=[])

type_ignores is the list of # type: ignore comments in the module (otherwise it’s typically an empty list). (docs.python.org)

Citations:


🌐 Web query:

Python 3.8 ast.Module constructor type_ignores parameter required

💡 Result:

In Python 3.8, ast.Module gained a new, required field: type_ignores. Its constructor signature effectively became:

  • ast.Module(body, type_ignores) (where type_ignores is a list of ast.TypeIgnore nodes) [1].

So code that worked in 3.7 like:

ast.Module(body=[...])

can fail in 3.8+ with:

TypeError: required field "type_ignores" missing from Module [2][3].

Fix when constructing AST manually

Pass an explicit list (usually empty):

import ast

mod = ast.Module(body=[...], type_ignores=[])

What type_ignores is for

When parsing with type_comments=True, the parser records # type: ignore comment locations into Module.type_ignores; otherwise it’s an empty list [1]. (So ast.parse(...) will produce a Module(..., type_ignores=...) automatically; the issue is mainly with manual construction.)

Sources: Python 3.8 AST grammar / docs showing Module(stmt* body, type_ignore* type_ignores) and how # type: ignore becomes Module.type_ignores [1]; common error reports and fixes in real projects (e.g., Werkzeug/Flask, IPython/Jupyter contexts) [2][3].

[1] https://docs.activestate.com/activepython/3.8/python/library/ast.html
[2] https://stackoverflow.com/questions/63609384/typeerror-required-field-type-ignores-missing-from-module
[3] https://lightrun.com/answers/pallets-werkzeug-astmodule-signature-change-in-python-380a3-causes-typeerror


🌐 Web query:

ast.fix_missing_locations Python ast.Module type_ignores

💡 Result:

ast.fix_missing_locations(node) only fills in missing source-location attributes (lineno, col_offset, and optionally end_lineno, end_col_offset) so the tree can be passed to compile(). It does not add/repair required structural fields on nodes. [1]

In Python 3.8+, ast.Module’s shape is:

  • ast.Module(body, type_ignores) where type_ignores is a list of TypeIgnore nodes (i.e., # type: ignore comments). [2]

So if you manually construct a module and then call fix_missing_locations, you still must provide type_ignores yourself (usually []), e.g.:

m = ast.Module(body=[...], type_ignores=[])
m = ast.fix_missing_locations(m)
code = compile(m, filename="<ast>", mode="exec")

type_ignores is populated by parsing when type comments are enabled; otherwise it’s an empty list. [3]

Sources: Python ast docs on fix_missing_locations [1] and ast.Module(..., type_ignores) / type_ignores meaning [2][3].

[1] https://docs.python.org/3.12/library/ast.html#ast.fix_missing_locations
[2] https://docs.python.org/3.12/library/ast.html#ast.Module
[3] https://docs.python.org/3.12/library/ast.html#ast.parse


ast.Module is missing the required type_ignores=[] argument.

In Python 3.8+, including Python 3.13 used by Pyodide, ast.Module requires a type_ignores parameter. Without it, the code will immediately raise TypeError: Module() missing required argument 'type_ignores' when executed.

Proposed fix
     wrapped = ast.Module(
         body=[
             ast.AsyncFunctionDef(
                 name="__run__",
                 args=ast.arguments(
                     posonlyargs=[],
                     args=[],
                     vararg=None,
                     kwonlyargs=[],
                     kw_defaults=[],
                     kwarg=None,
                     defaults=[],
                 ),
                 body=tree.body,
                 decorator_list=[],
                 returns=None,
             ),
         ],
+        type_ignores=[],
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorkerTemplate.py` around lines 29 - 48, The
ast.Module construction for variable wrapped is missing the required
type_ignores argument; update the ast.Module(...) call that wraps the
AsyncFunctionDef named "__run__" to include type_ignores=[] (i.e.,
ast.Module(body=[...], type_ignores=[])) so it conforms to Python 3.8+ AST API
before calling ast.fix_missing_locations(wrapped).

Comment on lines +4 to +5
const fs = require("fs")
const path = require("path")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Pipeline failure: require() is forbidden by ESLint — fix by overriding the rule for .cjs files.

The @typescript-eslint/no-require-imports rule fires on both require("fs") and require("path") because the ESLint config applies globally. Since this is a CommonJS script (.cjs), require() is the only correct import mechanism — the rule should be disabled for this file or for the *.cjs glob in the ESLint config.

💡 Suggested fix (add override to ESLint config, not in this file)
// eslint.config.js (or .eslintrc equivalent)
+  {
+    files: ["**/*.cjs"],
+    rules: {
+      "@typescript-eslint/no-require-imports": "off",
+    },
+  },

Alternatively, add a file-level disable comment as a quick fix:

+/* eslint-disable `@typescript-eslint/no-require-imports` */
 const fs = require("fs")
 const path = require("path")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fs = require("fs")
const path = require("path")
/* eslint-disable `@typescript-eslint/no-require-imports` */
const fs = require("fs")
const path = require("path")
🧰 Tools
🪛 ESLint

[error] 4-4: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 5-5: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)

🪛 GitHub Actions: Code Style

[error] 4-4: ESLint: A require() style import is forbidden. (no-require-imports)


[error] 5-5: ESLint: A require() style import is forbidden. (no-require-imports)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/scripts/inject-pyodide-version.cjs` around lines 4 - 5, The
ESLint rule `@typescript-eslint/no-require-imports` is incorrectly flagging
require("fs") and require("path") in this CommonJS script; add an override in
the ESLint config for the *.cjs glob to disable that rule (turn off
`@typescript-eslint/no-require-imports` for .cjs files) so require() is allowed in
inject-pyodide-version.cjs rather than adding inline disables in the file.

Comment on lines +16 to +23
function injectIndexUrl(filePath) {
let content = fs.readFileSync(filePath, "utf8")
content = content.replace(
/var PYODIDE_INDEX_URL = "[^"]*"/,
`var PYODIDE_INDEX_URL = ${indexUrlStr}`,
)
fs.writeFileSync(filePath, content)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent no-op when the regex doesn't match — add a guard to detect stale/missing placeholder.

String.prototype.replace returns the original string unchanged when the pattern has no match. If var PYODIDE_INDEX_URL = "..." is absent or already substituted with a different quote style, the worker file is overwritten with its original content and the build log still prints "Injected…", making the failure invisible.

🛡️ Proposed fix
 function injectIndexUrl(filePath) {
   let content = fs.readFileSync(filePath, "utf8")
-  content = content.replace(
-    /var PYODIDE_INDEX_URL = "[^"]*"/,
-    `var PYODIDE_INDEX_URL = ${indexUrlStr}`,
-  )
+  const pattern = /var PYODIDE_INDEX_URL = "[^"]*"/
+  if (!pattern.test(content)) {
+    throw new Error(`PYODIDE_INDEX_URL placeholder not found in ${filePath}`)
+  }
+  content = content.replace(pattern, `var PYODIDE_INDEX_URL = ${indexUrlStr}`)
   fs.writeFileSync(filePath, content)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function injectIndexUrl(filePath) {
let content = fs.readFileSync(filePath, "utf8")
content = content.replace(
/var PYODIDE_INDEX_URL = "[^"]*"/,
`var PYODIDE_INDEX_URL = ${indexUrlStr}`,
)
fs.writeFileSync(filePath, content)
}
function injectIndexUrl(filePath) {
let content = fs.readFileSync(filePath, "utf8")
const pattern = /var PYODIDE_INDEX_URL = "[^"]*"/
if (!pattern.test(content)) {
throw new Error(`PYODIDE_INDEX_URL placeholder not found in ${filePath}`)
}
content = content.replace(pattern, `var PYODIDE_INDEX_URL = ${indexUrlStr}`)
fs.writeFileSync(filePath, content)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/scripts/inject-pyodide-version.cjs` around lines 16 - 23, The
injectIndexUrl function currently overwrites the file even when the regex
doesn't match; update it to first test for a match (e.g., use the same RegExp or
compare content before/after) and only perform fs.writeFileSync when a
replacement actually occurred, and if no match is found raise/throw an explicit
error or log and exit so the build fails instead of silently continuing;
reference injectIndexUrl and indexUrlStr and the /var PYODIDE_INDEX_URL =
"[^"]*"/ pattern when making the change.


injectIndexUrl(runWorkerPath)
injectIndexUrl(browserTestWorkerPath)
console.log("Injected Pyodide index URL:", indexUrl) No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Pipeline failure: prettier requires a trailing newline at end of file.

Add a newline character after line 27.

🧰 Tools
🪛 ESLint

[error] 27-27: Insert

(prettier/prettier)

🪛 GitHub Actions: Code Style

[error] 27-27: prettier/prettier: Insert

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/scripts/inject-pyodide-version.cjs` at line 27, The file ends
without a trailing newline causing prettier to fail; open the file containing
the console.log statement console.log("Injected Pyodide index URL:", indexUrl)
and add a single newline character at the end of the file (after that
console.log line) so the file terminates with a newline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant