Skip to content

v1.2.0 #181

Open
NellowTCS wants to merge 12 commits into
mainfrom
v1.2.0
Open

v1.2.0 #181
NellowTCS wants to merge 12 commits into
mainfrom
v1.2.0

Conversation

@NellowTCS

@NellowTCS NellowTCS commented May 12, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • CI now produces a standalone single-file build artifact and a streamlined GitHub Pages publish.
  • Refactor

    • Migrated to a Vite-based toolchain with single-file and PWA support.
    • Editor/UI rewritten to a reactive multi-file tabbed experience with autosave, formatting, linting, search, terminal, and richer preview/console (clickable stack traces).
    • Added in‑browser virtual filesystem and lightweight Git integration.
  • Chores / Removed

    • Removed legacy build scripts, old bundler config, service worker/manifest, and legacy global styles.
  • Documentation

    • README and TODO updated for the new build flow and tooling.

Comment thread .github/workflows/single-file.yml Fixed
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

2

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'v1.2.0' is a version number and does not clearly summarize the primary changes in the pull request, which involve a major toolchain migration from Webpack to Vite, refactoring state management, and comprehensive architectural updates. Provide a descriptive title that highlights the main architectural changes, such as 'Migrate from Webpack to Vite and refactor state management' or 'Update to v1.2.0: Vite toolchain and reactive state architecture'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v1.2.0

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (1)
.github/workflows/single-file.yml (1)

13-41: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add explicit permissions block to the workflow.

The workflow does not specify permissions for GITHUB_TOKEN. As a security best practice, explicitly declare the minimum required permissions.

🔒 Proposed fix to add permissions
 jobs:
   deploy:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     steps:

As per coding guidelines, the CodeQL static analysis tool recommends setting an explicit permissions block with contents: read as a minimal starting point.

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

In @.github/workflows/single-file.yml around lines 13 - 41, The workflow lacks
an explicit permissions block for GITHUB_TOKEN; add a minimal permissions
declaration (e.g., permissions: contents: read) either at the workflow root or
scoped to the deploy job to limit token access; update the top-level or the
deploy job (job name "deploy") to include this permissions entry so the
GITHUB_TOKEN has only the required read access.
🧹 Nitpick comments (1)
.github/workflows/single-file.yml (1)

22-22: 💤 Low value

Inconsistent Node.js version across workflows.

This workflow uses Node.js 22 while .github/workflows/static.yml uses Node.js 20. Unless there's a specific reason for this difference, consider standardizing on a single Node.js version for consistency and easier maintenance.

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

In @.github/workflows/single-file.yml at line 22, The workflows are using
different Node.js versions (node-version: "22" in this workflow vs node-version:
"20" in the other workflow); pick one Node.js version to standardize on (e.g.,
"20" or "22") and update the node-version key in this workflow to match the
version used in the other workflow (change node-version: "22" to the chosen
version), and then run CI locally or via GitHub Actions to confirm no
compatibility issues; also search for any other occurrences of node-version in
workflow files and align them to the same value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/single-file.yml:
- Around line 8-10: The workflow's concurrency stanza currently uses group:
"pages", which collides with the static.yml workflow; update the concurrency
group value (the concurrency block and its group key) in the single-file
workflow to a unique name such as "single-file-build" (keep cancel-in-progress
as-is) so this workflow no longer blocks or is blocked by the Pages workflow.

In `@Build/pwa-assets.config.js`:
- Around line 1-27: The dependency constraint for `@vite-pwa/assets-generator` is
incorrect (1.0.1 does not exist); update the package version in your dependency
manifest so installs resolve deterministically — change the version specifier
from ^1.0.1 to ^1.0.2 (or to ^1.0.0 if you intend compatibility with the initial
1.0.0 release). Ensure the updated version is saved in package.json and any
lockfile is refreshed so the build uses the published release compatible with
the existing usage of defineConfig and minimal2023Preset.

In `@Build/src/appState.ts`:
- Around line 82-88: The code currently only type-checks
snapshot.activeTab/activeOutput before calling
activeTabState.set/activeOutputState.set, which lets invalid persisted strings
propagate; update this to whitelist values: define or import the canonical
allowed sets (e.g., TAB_KEYS or an enum used by the UI/editor and OUTPUT_KEYS)
and verify snapshot.activeTab and snapshot.activeOutput are members (e.g.,
allowedTabs.includes(snapshot.activeTab)) before calling
activeTabState.set/activeOutputState.set; if a value is invalid, skip setting
(or set a safe default) and optionally log a warning so downstream lookups
remain stable.
- Around line 48-54: The effect currently calls
localStorage.setItem("htmlRunnerState", JSON.stringify(createStateSnapshot()))
on every content mutation, causing sync hot-path writes; wrap this persistence
in a debounce/throttle so writes occur at a limited frequency (e.g., debounce
250–1000ms) or use requestIdleCallback to perform the write when idle, and
optionally skip writes if the new snapshot equals the last persisted snapshot.
Update the effect that references stateHydrated.get(), createStateSnapshot(),
and the localStorage.setItem call to schedule the serialized write via the
chosen debounce/throttle/idle mechanism and cancel any pending timer on cleanup.

In `@Build/src/defaultContent.ts`:
- Line 8: The default HTML in Build/src/defaultContent.ts references <script
src="main.js"></script> but the build/export emits "script.js", so update the
script tag in the default export to <script src="script.js"></script> (i.e.,
replace "main.js" with "script.js" in the default content string) so the
exported project loads the correct JS file out of the box.

In `@Build/src/runner.ts`:
- Around line 17-33: The cached promise prettierBundlePromise currently holds a
rejected promise if the dynamic imports fail, causing all future format attempts
to fail; modify the initialization in the import block so the
Promise.all(...).then(...) chain also has a .catch(err => {
prettierBundlePromise = undefined; throw err; }) (or similar reset) so that on
load failure the cached prettierBundlePromise is cleared and subsequent calls
retry the dynamic imports; reference the prettierBundlePromise variable and the
Promise.all([... import(...) ...]).then(...) chain in runner.ts when applying
this change.

In `@Build/vite.config.mjs`:
- Around line 7-25: The plugin inlineSvgFaviconPlugin is treating the file at
options.svg as UTF-8 SVG (transformIndexHtml reads it as text and runs
XML/whitespace regex) but the project supplies a PNG, corrupting the favicon;
fix by either (A) replacing the PNG with a real SVG and updating the favicon
reference to public/favicon.svg so inlineSvgFaviconPlugin can continue to read
UTF-8 SVG, or (B) update inlineSvgFaviconPlugin/transformIndexHtml to detect the
file type from options.svg (e.g., by extension or mime sniff), read binary for
non-SVG files (fs.readFileSync(options.svg) without "utf8"), skip the
XML/whitespace regex for binary images, base64-encode the raw buffer, and set
the correct type in faviconTag (image/png vs image/svg+xml) before inserting
into the head.

In `@TODO.md`:
- Line 12: Update the Auto Completion checklist item text to hyphenate
“built-in” in the phrase inside the markdown list item (the line containing "- [
] **Auto Completion** (using the built in CodeMirror tools)"); change "built in"
to "built-in" so the item reads "(using the built-in CodeMirror tools)".

---

Duplicate comments:
In @.github/workflows/single-file.yml:
- Around line 13-41: The workflow lacks an explicit permissions block for
GITHUB_TOKEN; add a minimal permissions declaration (e.g., permissions:
contents: read) either at the workflow root or scoped to the deploy job to limit
token access; update the top-level or the deploy job (job name "deploy") to
include this permissions entry so the GITHUB_TOKEN has only the required read
access.

---

Nitpick comments:
In @.github/workflows/single-file.yml:
- Line 22: The workflows are using different Node.js versions (node-version:
"22" in this workflow vs node-version: "20" in the other workflow); pick one
Node.js version to standardize on (e.g., "20" or "22") and update the
node-version key in this workflow to match the version used in the other
workflow (change node-version: "22" to the chosen version), and then run CI
locally or via GitHub Actions to confirm no compatibility issues; also search
for any other occurrences of node-version in workflow files and align them to
the same value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d116896b-483e-43ba-b136-24f76242b42e

📥 Commits

Reviewing files that changed from the base of the PR and between 2a4f534 and 431f6e1.

⛔ Files ignored due to path filters (2)
  • Build/package-lock.json is excluded by !**/package-lock.json
  • Build/public/favicon.png is excluded by !**/*.png
📒 Files selected for processing (28)
  • .github/workflows/single-file.yml
  • .github/workflows/static.yml
  • Build/Buildscripts/build-all.sh
  • Build/Buildscripts/build-inline.sh
  • Build/Buildscripts/build.sh
  • Build/index.html
  • Build/package.json
  • Build/pwa-assets.config.js
  • Build/src/appState.ts
  • Build/src/defaultContent.ts
  • Build/src/editor.ts
  • Build/src/global.d.ts
  • Build/src/index.html
  • Build/src/main.ts
  • Build/src/runner.ts
  • Build/src/state.ts
  • Build/src/types.ts
  • Build/src/ui.ts
  • Build/styles/styles.css
  • Build/tsconfig.json
  • Build/vite.config.mjs
  • Build/webpack.config.js
  • README.md
  • TODO.md
  • main.js
  • manifest.json
  • service-worker.js
  • styles.css
💤 Files with no reviewable changes (10)
  • Build/webpack.config.js
  • service-worker.js
  • Build/Buildscripts/build-all.sh
  • manifest.json
  • Build/src/index.html
  • Build/Buildscripts/build.sh
  • styles.css
  • Build/src/state.ts
  • Build/Buildscripts/build-inline.sh
  • Build/tsconfig.json

Comment thread .github/workflows/single-file.yml
Comment thread Build/pwa-assets.config.js
Comment thread Build/src/appState.ts
Comment thread Build/src/appState.ts Outdated
Comment thread Build/src/defaultContent.ts Outdated
Comment thread Build/src/runner.ts
Comment thread Build/vite.config.mjs
Comment thread TODO.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
Build/src/runner.ts (1)

107-109: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

load listener accumulates on the preview iframe across runs.

Every runCode() attaches a new load listener that closes over the current url. The listener is never removed, so each subsequent run adds another one (each invoked on its own matching load). Over a long session this leaks listeners and closures. Use { once: true } (or remove explicitly) so each listener is detached after firing.

🔒️ Proposed fix
     preview.src = url;
-    preview.addEventListener("load", () => URL.revokeObjectURL(url));
+    preview.addEventListener(
+      "load",
+      () => URL.revokeObjectURL(url),
+      { once: true }
+    );
     switchOutput("preview");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/runner.ts` around lines 107 - 109, The preview iframe's "load"
listener attached in runCode() captures the current url and is never removed,
causing accumulated listeners and closures; update the addEventListener call on
preview (the listener that calls URL.revokeObjectURL(url) and then
switchOutput("preview")) to be registered with the { once: true } option (or
alternatively store and remove the handler after it runs) so the handler
automatically detaches after firing and prevents leaks.
♻️ Duplicate comments (1)
.github/workflows/single-file.yml (1)

11-13: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Concurrency group still collides with static.yml.

group: "pages" continues to overlap with the Pages deploy workflow, so this artifact build can be queued/blocked unnecessarily. A workflow-specific group like "single-file-build" would isolate it.

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

In @.github/workflows/single-file.yml around lines 11 - 13, The concurrency
group "pages" in the workflow's concurrency block collides with the Pages deploy
workflow; change the concurrency group to a workflow-specific name (e.g.,
"single-file-build") by updating the concurrency.group value in
.github/workflows/single-file.yml so this workflow uses a unique group and no
longer blocks or is blocked by the Pages workflow.
🧹 Nitpick comments (8)
.github/workflows/single-file.yml (2)

16-16: 💤 Low value

Job name deploy is misleading.

This job builds and uploads an artifact; it does not deploy. Renaming to e.g. build improves log readability and avoids confusion with the Pages workflow.

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

In @.github/workflows/single-file.yml at line 16, The GitHub Actions job key
"deploy" is misleading because it only builds and uploads an artifact; rename
the job key from deploy to build to reflect its purpose (update the top-level
job identifier "deploy" to "build" in the workflow), and search/update any
references to that job key (e.g., other jobs' needs:, outputs, or step uses) so
they point to "build" instead of "deploy" to keep logs and dependencies
consistent.

22-29: ⚡ Quick win

Prefer npm ci and enable setup-node’s built-in cache.

In CI, npm ci is faster and deterministic against package-lock.json, and actions/setup-node@v4 can cache ~/.npm automatically. Both reduce build time and avoid surprise resolutions.

♻️ Proposed change
       - name: Use Node.js
         uses: actions/setup-node@v4
         with:
           node-version: "20"
+          cache: "npm"
+          cache-dependency-path: Build/package-lock.json

       - name: Install dependencies
         working-directory: Build
-        run: npm i
+        run: npm ci
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/single-file.yml around lines 22 - 29, Replace the
non-deterministic npm install step with a clean CI install and enable
setup-node’s npm cache: in the GitHub Actions job that uses
actions/setup-node@v4 (with node-version: "20"), add the cache: "npm" option to
its with block and change the Install dependencies run step (currently using
working-directory: Build and run: npm i) to run: npm ci (keeping the
working-directory: Build). This ensures deterministic installs via
package-lock.json and activates setup-node’s built-in npm caching to speed
subsequent runs.
Build/src/defaultContent.ts (1)

8-13: 💤 Low value

Unnecessary forward-slash escapes inside a JS template literal.

<\/script>, <\/body>, <\/html> are only needed when an HTML string is embedded inside a <script> block in HTML source. Inside a .ts template literal they have no effect and just hurt readability. Safe to drop.

♻️ Proposed cleanup
-<script src="script.js"><\/script>
+<script src="script.js"></script>
 <h1>Hello, HTMLRunner!</h1>
 <p>This is a demo page.</p>
 <button onclick="testFunction()">Click me!</button>
-<\/body>
-<\/html>`;
+</body>
+</html>`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/defaultContent.ts` around lines 8 - 13, The template literal in
defaultContent.ts contains unnecessary escaped forward slashes (e.g.,
"<\/script>", "<\/body>", "<\/html>") that were only needed inside an HTML
<script> block; remove the backslashes so the strings become "</script>",
"</body>", "</html>" in the template (locate the template literal that includes
the lines with "<\/script>" and replace those escaped sequences). Ensure any
variables or functions that reference this content still receive the corrected
unescaped HTML string.
Build/src/editor.ts (3)

3-16: 💤 Low value

Duplicate import from @codemirror/commands.

defaultKeymap, standardKeymap and toggleComment are imported in two separate statements from the same module. Merge them into a single import for cleanliness.

♻️ Proposed change
-import { defaultKeymap, standardKeymap } from "@codemirror/commands";
+import { defaultKeymap, standardKeymap, toggleComment } from "@codemirror/commands";
@@
-import { toggleComment } from "@codemirror/commands"; // Ensure toggleComment is imported
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/editor.ts` around lines 3 - 16, There are duplicate imports from
"@codemirror/commands" (defaultKeymap, standardKeymap and toggleComment);
consolidate them into a single import statement that includes defaultKeymap,
standardKeymap and toggleComment so only one import from "@codemirror/commands"
remains (update the import near the top of the file where
defaultKeymap/standardKeymap are currently declared).

69-75: 💤 Low value

No-op linter adds machinery for nothing.

The lint source unconditionally returns [], so linter(...) + lintGutter() only render an empty gutter and burn a 100ms timer per edit. Either drop both extensions or plug in a real linter for the language.

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

In `@Build/src/editor.ts` around lines 69 - 75, The current integration of
linter(...) and lintGutter() is a no-op because the lint source passed to linter
always returns an empty array, wasting a 100ms timer and rendering an empty
gutter; either remove both linter(...) and lintGutter() calls from the editor
extensions list or replace the stub lint source with a real diagnostic provider
(implement a function used by linter that returns proper Diagnostic objects for
the language and handles debouncing instead of relying on the unused 100ms
delay), updating the references to linter and lintGutter accordingly.

76-80: 💤 Low value

Remove standardKeymap from the spread; defaultKeymap already includes it.

defaultKeymap from @codemirror/commands concatenates additional bindings with standardKeymap, so spreading both creates duplicate key bindings.

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

In `@Build/src/editor.ts` around lines 76 - 80, The keymap configuration currently
spreads both defaultKeymap and standardKeymap, causing duplicate bindings;
remove standardKeymap from the array passed to keymap.of and only include
defaultKeymap and the custom binding (the object with key "Mod-/" and run:
toggleComment) so use keymap.of([...defaultKeymap, { key: "Mod-/", run:
toggleComment }]); update the array in the keymap.of call that currently
references defaultKeymap, standardKeymap, and the toggleComment binding.
Build/src/runner.ts (1)

60-60: 💤 Low value

Full-HTML detection regex misses a bare <html> with no attributes/whitespace before EOF.

/<html[\s>]|<!doctype html/i requires a whitespace or > after <html. That covers <html> and <html lang="…">, but not pathological cases like <html at EOF or <html/> (self-closed). Not a real-world concern for typical user input, but worth a glance if you want to be strict; otherwise safe to keep as-is.

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

In `@Build/src/runner.ts` at line 60, The isFullHtml detection currently using the
regex assigned to isFullHtml misses cases like a bare "<html" at EOF or
self-closed "<html/>"; update the regex used where isFullHtml is declared so it
also matches "<html" followed by end-of-string or a "/" before ">" (for example
use a lookahead or alternation such as matching "<html" when followed by
whitespace, ">", "/", or end-of-string) and keep the existing doctype check (the
variable name isFullHtml in Build/src/runner.ts identifies the spot to change).
Build/src/ui.ts (1)

112-135: 💤 Low value

toggleDarkMode and setPageDarkMode duplicate the same three-step sequence.

Both functions call setDarkMode(...), toggle the dark-mode body class, and refresh the theme icon. toggleDarkMode can just delegate to setPageDarkMode to keep the logic in one place.

♻️ Proposed change
 export function toggleDarkMode(): void {
-  const newDarkMode = !darkModeState.get();
-  setDarkMode(newDarkMode);
-  document.body.classList.toggle("dark-mode", newDarkMode);
-  updateThemeIcon();
+  setPageDarkMode(!darkModeState.get());
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/ui.ts` around lines 112 - 135, toggleDarkMode duplicates the same
three-step sequence as setPageDarkMode; change toggleDarkMode to compute the new
value (const newDarkMode = !darkModeState.get()) and delegate to
setPageDarkMode(newDarkMode) instead of calling setDarkMode, toggling
document.body, and calling updateThemeIcon itself so all state/class/icon logic
lives in setPageDarkMode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/single-file.yml:
- Around line 4-5: The workflow's push.branches glob currently uses branches:
["*"] which does not match branch names containing "/" so feature or dependabot
branches are skipped; update the push.branches setting (the push -> branches
entry) to use branches: ["**"] instead of ["*"] so all branches, including those
with slashes, trigger the workflow.

In `@Build/src/editor.ts`:
- Around line 35-43: The setAutoRun function only updates autoRunState but must
also reconfigure every editor's autoRunCompartment like setDarkMode does: update
autoRunState.set(value) then iterate all editors (the same collection used by
setDarkMode) and call editor.view.dispatch({effects:
autoRunCompartment.reconfigure(value ? autoRunOnExtension :
autoRunOffExtension)}) or the equivalent reconfigure call using
autoRunCompartment to apply the new setting; update setAutoRun to be the single
source of truth and remove the manual reconfiguration loops from the
toggleAutoRun implementations in ui.ts and main.ts so callers don’t need to
reconfigure editors themselves.

---

Outside diff comments:
In `@Build/src/runner.ts`:
- Around line 107-109: The preview iframe's "load" listener attached in
runCode() captures the current url and is never removed, causing accumulated
listeners and closures; update the addEventListener call on preview (the
listener that calls URL.revokeObjectURL(url) and then switchOutput("preview"))
to be registered with the { once: true } option (or alternatively store and
remove the handler after it runs) so the handler automatically detaches after
firing and prevents leaks.

---

Duplicate comments:
In @.github/workflows/single-file.yml:
- Around line 11-13: The concurrency group "pages" in the workflow's concurrency
block collides with the Pages deploy workflow; change the concurrency group to a
workflow-specific name (e.g., "single-file-build") by updating the
concurrency.group value in .github/workflows/single-file.yml so this workflow
uses a unique group and no longer blocks or is blocked by the Pages workflow.

---

Nitpick comments:
In @.github/workflows/single-file.yml:
- Line 16: The GitHub Actions job key "deploy" is misleading because it only
builds and uploads an artifact; rename the job key from deploy to build to
reflect its purpose (update the top-level job identifier "deploy" to "build" in
the workflow), and search/update any references to that job key (e.g., other
jobs' needs:, outputs, or step uses) so they point to "build" instead of
"deploy" to keep logs and dependencies consistent.
- Around line 22-29: Replace the non-deterministic npm install step with a clean
CI install and enable setup-node’s npm cache: in the GitHub Actions job that
uses actions/setup-node@v4 (with node-version: "20"), add the cache: "npm"
option to its with block and change the Install dependencies run step (currently
using working-directory: Build and run: npm i) to run: npm ci (keeping the
working-directory: Build). This ensures deterministic installs via
package-lock.json and activates setup-node’s built-in npm caching to speed
subsequent runs.

In `@Build/src/defaultContent.ts`:
- Around line 8-13: The template literal in defaultContent.ts contains
unnecessary escaped forward slashes (e.g., "<\/script>", "<\/body>", "<\/html>")
that were only needed inside an HTML <script> block; remove the backslashes so
the strings become "</script>", "</body>", "</html>" in the template (locate the
template literal that includes the lines with "<\/script>" and replace those
escaped sequences). Ensure any variables or functions that reference this
content still receive the corrected unescaped HTML string.

In `@Build/src/editor.ts`:
- Around line 3-16: There are duplicate imports from "@codemirror/commands"
(defaultKeymap, standardKeymap and toggleComment); consolidate them into a
single import statement that includes defaultKeymap, standardKeymap and
toggleComment so only one import from "@codemirror/commands" remains (update the
import near the top of the file where defaultKeymap/standardKeymap are currently
declared).
- Around line 69-75: The current integration of linter(...) and lintGutter() is
a no-op because the lint source passed to linter always returns an empty array,
wasting a 100ms timer and rendering an empty gutter; either remove both
linter(...) and lintGutter() calls from the editor extensions list or replace
the stub lint source with a real diagnostic provider (implement a function used
by linter that returns proper Diagnostic objects for the language and handles
debouncing instead of relying on the unused 100ms delay), updating the
references to linter and lintGutter accordingly.
- Around line 76-80: The keymap configuration currently spreads both
defaultKeymap and standardKeymap, causing duplicate bindings; remove
standardKeymap from the array passed to keymap.of and only include defaultKeymap
and the custom binding (the object with key "Mod-/" and run: toggleComment) so
use keymap.of([...defaultKeymap, { key: "Mod-/", run: toggleComment }]); update
the array in the keymap.of call that currently references defaultKeymap,
standardKeymap, and the toggleComment binding.

In `@Build/src/runner.ts`:
- Line 60: The isFullHtml detection currently using the regex assigned to
isFullHtml misses cases like a bare "<html" at EOF or self-closed "<html/>";
update the regex used where isFullHtml is declared so it also matches "<html"
followed by end-of-string or a "/" before ">" (for example use a lookahead or
alternation such as matching "<html" when followed by whitespace, ">", "/", or
end-of-string) and keep the existing doctype check (the variable name isFullHtml
in Build/src/runner.ts identifies the spot to change).

In `@Build/src/ui.ts`:
- Around line 112-135: toggleDarkMode duplicates the same three-step sequence as
setPageDarkMode; change toggleDarkMode to compute the new value (const
newDarkMode = !darkModeState.get()) and delegate to setPageDarkMode(newDarkMode)
instead of calling setDarkMode, toggling document.body, and calling
updateThemeIcon itself so all state/class/icon logic lives in setPageDarkMode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dda8132d-35b6-4148-84bc-a2e4a28c9698

📥 Commits

Reviewing files that changed from the base of the PR and between 431f6e1 and 5291829.

📒 Files selected for processing (9)
  • .github/workflows/single-file.yml
  • Build/src/appState.ts
  • Build/src/defaultContent.ts
  • Build/src/editor.ts
  • Build/src/runner.ts
  • Build/src/ui.ts
  • Build/vite.config.mjs
  • README.md
  • TODO.md
✅ Files skipped from review due to trivial changes (2)
  • TODO.md
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • Build/vite.config.mjs
  • Build/src/appState.ts

Comment thread .github/workflows/single-file.yml Outdated
Comment thread Build/src/editor.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
Build/src/main.ts (1)

292-301: ⚡ Quick win

Add error handling for dynamic import failures.

If file-saver or jszip fail to load (e.g., network issues, bundler misconfiguration), the unhandled rejection will crash the export flow without user feedback.

🛡️ Proposed fix to add try/catch
 async function exportAsZip() {
   const html = editors.html.view.state.doc.toString().trim();
   const css = editors.css.view.state.doc.toString().trim();
   const js = editors.js.view.state.doc.toString().trim();
 
   const files: { name: string; content: string }[] = [];
   if (html) files.push({ name: "index.html", content: html });
   if (css) files.push({ name: "styles.css", content: css });
   if (js) files.push({ name: "script.js", content: js });
 
   if (files.length === 0) {
     alert("Nothing to export!");
     return;
   }
 
+  try {
     if (files.length === 1) {
       const { saveAs } = await import("file-saver");
       const blob = new Blob([files[0].content], { type: "text/plain" });
       saveAs(blob, files[0].name);
       return;
     }
 
     const [{ default: JSZip }, { saveAs }] = await Promise.all([
       import("jszip"),
       import("file-saver"),
     ]);
 
     const zip = new JSZip();
     for (const file of files) {
       zip.file(file.name, file.content);
     }
 
     const content = await zip.generateAsync({ type: "blob" });
     saveAs(content, "htmlrunner-export.zip");
+  } catch (error) {
+    console.error("Export failed:", error);
+    alert("Failed to export files. Please try again.");
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/main.ts` around lines 292 - 301, The dynamic imports for
"file-saver" and "jszip" (the destructuring that produces JSZip and saveAs via
Promise.all and the earlier single import of saveAs) need try/catch around them
to prevent unhandled rejections; wrap the await import(...) calls (both the
single import branch that sets saveAs and the Promise.all branch that yields {
default: JSZip } and { saveAs }) in try/catch, log the error (console.error or
processLogger), show a user-facing error/notification, and return early if the
imports fail so the export flow does not proceed with undefined JSZip/saveAs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Build/src/console.ts`:
- Line 60: The current regex used in the line.match call (/:(\d+):(\d+)\)?$/) is
too narrow and misses common stack formats (e.g. "at fn (file.js:123:45)",
"fn@file.js:123:45", "file.js:123"). Replace the single regex with a more
flexible approach: try matching multiple patterns in order (e.g.,
/\(([^)]+):(\d+):(\d+)\)$/, /([^@()\s]+):(\d+):(\d+)$/, and
/([^@()\s]+):(\d+)$/) or use one combined regex that captures file, line, and
optional column; map captured groups to file, line, col (allowing col to be
undefined) and update the existing link-building logic that uses m so all common
trace formats become clickable.

In `@Build/src/main.ts`:
- Line 68: The sizes fallback uses the truthy || check so empty arrays or [0,0]
won't trigger the default; change the assignment that sets sizes (where you call
splitSizesState.get()) to validate the returned value: call
splitSizesState.get(), ensure it's a non-empty array of positive numbers (and
acceptable length), and if that validation fails use [50,50]; alternatively use
nullish coalescing (??) only if you also guard against empty arrays—update the
sizes property assignment accordingly.

---

Nitpick comments:
In `@Build/src/main.ts`:
- Around line 292-301: The dynamic imports for "file-saver" and "jszip" (the
destructuring that produces JSZip and saveAs via Promise.all and the earlier
single import of saveAs) need try/catch around them to prevent unhandled
rejections; wrap the await import(...) calls (both the single import branch that
sets saveAs and the Promise.all branch that yields { default: JSZip } and {
saveAs }) in try/catch, log the error (console.error or processLogger), show a
user-facing error/notification, and return early if the imports fail so the
export flow does not proceed with undefined JSZip/saveAs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2388e993-a0e1-4d8a-8478-bf6af3c30a28

📥 Commits

Reviewing files that changed from the base of the PR and between 5291829 and c8df0c8.

⛔ Files ignored due to path filters (1)
  • Build/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • .github/workflows/single-file.yml
  • Build/package.json
  • Build/src/console.ts
  • Build/src/defaultContent.ts
  • Build/src/editor.ts
  • Build/src/main.ts
  • Build/src/runner.ts
  • Build/src/ui.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • Build/src/defaultContent.ts
  • .github/workflows/single-file.yml
  • Build/src/ui.ts
  • Build/package.json
  • Build/src/editor.ts
  • Build/src/runner.ts

Comment thread Build/src/console.ts Outdated
Comment thread Build/src/main.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
Build/src/main.ts (1)

522-538: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Load persisted state before initializing Split.js.

initializeSplit() runs before loadState(), so the first render always uses the default panel sizes and ignores the saved splitSizesState until a later resize recreates the split instance.

Suggested fix
 document.addEventListener("DOMContentLoaded", async () => {
+  loadState();
   initializeEditors();
   initializeCopyButtons();
   initializeSplit();
   updateAutoRunStatus();
   initializeLogFilters();
   addGlobalSearchShortcuts();
@@
-  loadState();
   setPageDarkMode(darkModeState.get());
   updateAutoRunStatus();
   formatCode().catch((error) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/main.ts` around lines 522 - 538, The persisted UI state is loaded
after initializeSplit(), so Split.js gets default sizes; move loadState() to run
before initializeSplit() so initializeSplit() can read the persisted
splitSizesState and create the split with saved sizes (adjust the
DOMContentLoaded sequence to call loadState() — and any dependent calls like
setPageDarkMode(darkModeState.get()) — before calling initializeSplit()); ensure
initializeSplit() reads splitSizesState on creation so the saved sizes are
applied immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Build/eslint.config.mts`:
- Line 10: The ESLint block that sets { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
languageOptions: { globals: globals.browser } } applies browser globals to
Node-side files (e.g., Build/vite.config.mjs), causing incorrect linting; fix by
scoping globals.browser to only client/browser patterns (e.g., src/**, web/**,
or specific extensions) and add a separate config entry for Node-side patterns
(e.g., Build/**, scripts/**) that uses languageOptions: { globals: globals.node
} so Node scripts like Build/vite.config.mjs use node globals instead of
globals.browser.

In `@Build/src/console.ts`:
- Around line 29-30: The new console message handler (initializeConsole ->
handleConsoleMessage) is not being used because the app's entrypoint still
registers its own window.message listener; call initializeConsole() from the
application's startup (e.g., the main entrypoint initialization function) so the
clickable stack-line logic runs, and remove the duplicate
window.addEventListener("message", ...) registration in the entrypoint
(Build/src/main.ts) so initializeConsole/handleConsoleMessage is the single
source of truth and events aren't rendered twice.

In `@Build/src/main.ts`:
- Around line 482-489: The search input lookup currently uses
document.querySelector(...) inside window.requestAnimationFrame which can match
a hidden .cm-search from another tab; instead scope the query to the active
editor/search panel element you just opened (e.g., obtain the search panel or
active editor container node and call root.querySelector(selector) rather than
document.querySelector). Update the focus/select logic to use that scoped node
(replace the document.querySelector call that assigns field) so mode, selector
and field behavior stays the same but targets the visible panel for the active
editor.

---

Outside diff comments:
In `@Build/src/main.ts`:
- Around line 522-538: The persisted UI state is loaded after initializeSplit(),
so Split.js gets default sizes; move loadState() to run before initializeSplit()
so initializeSplit() can read the persisted splitSizesState and create the split
with saved sizes (adjust the DOMContentLoaded sequence to call loadState() — and
any dependent calls like setPageDarkMode(darkModeState.get()) — before calling
initializeSplit()); ensure initializeSplit() reads splitSizesState on creation
so the saved sizes are applied immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af0ecc02-950f-49e0-b593-9f8b16a44d1a

📥 Commits

Reviewing files that changed from the base of the PR and between c8df0c8 and 329d0ac.

⛔ Files ignored due to path filters (1)
  • Build/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • Build/eslint.config.mts
  • Build/package.json
  • Build/src/appState.ts
  • Build/src/console.ts
  • Build/src/defaultContent.ts
  • Build/src/editor.ts
  • Build/src/global.d.ts
  • Build/src/main.ts
  • Build/src/runner.ts
  • Build/src/ui.ts
  • Build/src/utils.ts
✅ Files skipped from review due to trivial changes (2)
  • Build/src/utils.ts
  • Build/src/defaultContent.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • Build/src/global.d.ts
  • Build/package.json
  • Build/src/editor.ts
  • Build/src/appState.ts
  • Build/src/ui.ts
  • Build/src/runner.ts

Comment thread Build/eslint.config.mts
Comment thread Build/src/console.ts Outdated
Comment thread Build/src/main.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@Build/src/appState.ts`:
- Around line 23-41: The initial signal values (darkModeInitial, autoRunInitial,
htmlInitial, cssInitial, jsInitial, activeTabInitial, activeOutputInitial,
splitSizesInitial, logFiltersInitial) are being seeded directly from
persistedState/localStorage without validation; update these initializers to
first safely parse localStorage (try/catch JSON.parse) and then validate each
value’s type and allowed values (e.g., html/css/js must be strings,
activeTab/activeOutput must be one of the accepted keys, splitSizes must be an
array of two numbers, logFilters must be string[]), falling back to the default
constants (defaultHtml/defaultCss/defaultJs, "html"/"preview", [50,50],
["log","error","warn","info"]) if validation fails; ensure loadState() and
applyStateSnapshot() are not relied on to fix invalid initial signals by
preventing bad values from being assigned in the darkModeInitial/... variables
in the module top-level code.

In `@Build/src/console.ts`:
- Around line 151-178: renderObject currently passes non-object nested
primitives (e.g., undefined, bigint, symbol) into the WeakSet logic and throws
TypeError; update renderObject to detect non-object/non-function values before
the visited.has/visited.add block (inside renderObject) and return a text node
for those primitives (use String(obj) or explicit "undefined"/bigint conversion)
so only actual objects/functions are checked/added to visited; reference the
renderObject function and the visited WeakSet usage around the visited.has(...)
and visited.add(...) calls.

In `@Build/src/ui.ts`:
- Around line 19-22: The showError function currently sets a new timeout every
call which lets an earlier timer clear a later error; fix this by tracking the
timeout id in a module-scoped variable (e.g., errorTimeoutId) and call
clearTimeout(errorTimeoutId) before scheduling a new setTimeout; then save the
returned id back to errorTimeoutId so the most recent message isn't prematurely
cleared. Reference: showError and errorMessage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90e68f91-2ba0-42d6-9599-0f88cdfb65ad

📥 Commits

Reviewing files that changed from the base of the PR and between 329d0ac and b4cdbeb.

📒 Files selected for processing (11)
  • Build/index.html
  • Build/src/appState.ts
  • Build/src/console.ts
  • Build/src/editor.ts
  • Build/src/main.ts
  • Build/src/runner.ts
  • Build/src/types.ts
  • Build/src/ui.ts
  • Build/src/utils.ts
  • Build/styles/styles.css
  • Build/tsconfig.json
✅ Files skipped from review due to trivial changes (1)
  • Build/styles/styles.css
🚧 Files skipped from review as they are similar to previous changes (4)
  • Build/src/utils.ts
  • Build/index.html
  • Build/src/editor.ts
  • Build/src/runner.ts

Comment thread Build/src/appState.ts
Comment thread Build/src/console.ts
Comment thread Build/src/ui.ts
Comment on lines 19 to +22
export function showError(message: string): void {
errorEl.textContent = message;
errorEl.style.display = "block";
setTimeout(() => (errorEl.style.display = "none"), 5000);
errorMessage.set(message);
// clear after a short timeout
setTimeout(() => errorMessage.set(""), 5000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear the previous dismiss timer before scheduling a new one.

Back-to-back showError() calls race here: the first timer can clear the second message almost immediately, so the latest error does not stay visible for the intended 5 seconds.

💡 Proposed fix
+let clearErrorTimeout: number | undefined;
+
 export function showError(message: string): void {
+  if (clearErrorTimeout) {
+    window.clearTimeout(clearErrorTimeout);
+  }
   errorMessage.set(message);
-  // clear after a short timeout
-  setTimeout(() => errorMessage.set(""), 5000);
+  clearErrorTimeout = window.setTimeout(() => {
+    errorMessage.set("");
+    clearErrorTimeout = undefined;
+  }, 5000);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/ui.ts` around lines 19 - 22, The showError function currently sets
a new timeout every call which lets an earlier timer clear a later error; fix
this by tracking the timeout id in a module-scoped variable (e.g.,
errorTimeoutId) and call clearTimeout(errorTimeoutId) before scheduling a new
setTimeout; then save the returned id back to errorTimeoutId so the most recent
message isn't prematurely cleared. Reference: showError and errorMessage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

♻️ Duplicate comments (1)
Build/src/appState.ts (1)

22-44: ⚠️ Potential issue | 🟠 Major

Sanitize persisted values before using them as initial signal state.

These initializers still copy raw localStorage JSON into filesState, activeOutputState, splitSizesState, and the other first-load values. If storage is corrupted, bad shapes/types are present before applyStateSnapshot() gets a chance to validate them, which can break the initial render and editor bootstrap.

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

In `@Build/src/appState.ts` around lines 22 - 44, Persisted values are used
directly into initial signals (filesInitial, activeFileInitial, darkModeInitial,
autoRunInitial, activeOutputInitial, splitSizesInitial, logFiltersInitial) which
can break startup if storage is corrupted; add small sanitizer/validator helpers
(e.g., sanitizeFiles, sanitizeActiveFile, sanitizeBooleans, sanitizeSplitSizes,
sanitizeLogFilters, sanitizeActiveOutput) that confirm types/shape (files is an
array of objects with required id/content/title, splitSizes is two numbers in
0-100, logFilters is array of allowed strings, activeOutput is one of allowed
values, darkMode/autoRun are booleans) and return safe defaults (defaultFiles,
["log","error","warn","info"], [50,50], "preview", etc.) then wire the
initializers to use these sanitizers instead of directly using persistedState so
the app always gets validated shapes before applyStateSnapshot runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Build/index.html`:
- Line 59: The preview iframe (<iframe id="preview" class="preview active">)
lacks a sandbox attribute allowing previewed scripts to access the parent; add a
sandbox attribute to harden isolation (e.g., sandbox="allow-scripts" for running
user scripts but preventing access to window.parent/storage) on the iframe with
id="preview" and avoid including permissions like allow-same-origin or
allow-top-navigation unless explicitly required.

In `@Build/package.json`:
- Line 12: The lint script in package.json currently silences ESLint failures by
appending "|| true" to the "lint" script; remove the "|| true" from the "lint"
script so ESLint errors cause a non-zero exit (i.e., change the "lint" script
value to "eslint \"src/**/*.{ts,js}\" --ext .ts,.js"), and if you need a
non-blocking helper add a separate script like "lint:ci-safe" or "lint:soft"
that includes "|| true".

In `@Build/src/biome.ts`:
- Around line 32-55: Concurrent calls to initBiome can race, so introduce an
in-flight promise (e.g. initBiomePromise: Promise<void> | null) and use it to
ensure single-flight initialization: at the start of initBiome check initialized
and return, then if initBiomePromise exists await and return; otherwise assign
initBiomePromise = (async () => { await initBiomeWasm(); workspace = new
Workspace(); const openRes = workspace.openProject({ path: "",
openUninitialized: true }); projectKey = openRes.projectKey;
workspace.updateSettings({ projectKey, configuration: { linter: { enabled: true,
rules: { recommended: true } } }, workspaceDirectory: "./", }); initialized =
true; })(); await initBiomePromise; finally clear initBiomePromise if you prefer
(or leave until process end) so all callers await the same promise and avoid
racing workspace/projectKey assignment.

In `@Build/src/editor.ts`:
- Around line 189-207: The effect currently only reconfigures
_languageCompartment when the file content changes, so renaming a file (e.g.,
script.js → styles.css) with identical content won't update the editor language;
fix by tracking the active filename (e.g., add a closure variable
lastActiveFileName) and, inside effect() along with the existing content check
on editor.view, also compare file.name to lastActiveFileName and dispatch the
_languageCompartment.reconfigure(getLanguageExtension(file.name)) when the name
changed (then set lastActiveFileName = file.name); ensure this runs regardless
of content equality but still respects the _suppressContentUpdate flow and only
triggers the reconfigure when the filename actually changed.

In `@Build/src/main.ts`:
- Around line 188-194: The map that builds console text uses JSON.stringify(d)
directly and can throw on circular or DOM objects; update the inner mapping for
ev.data inside the entries.map callback so each value (d) is serialized safely:
wrap JSON.stringify(d) in a try/catch (or use a safe replacer approach) and fall
back to String(d) or a non-throwing representation when stringify fails; ensure
the change is applied to the message construction inside the entries.map(ev => {
... }) block so a single bad value cannot break the whole "Copy All" output.
- Around line 323-349: renderFileTabs renders file tabs as non-interactive divs
so keyboard users cannot switch files; update renderFileTabs to make each tab
and the add/close controls keyboard-accessible by using focusable elements or
adding tabindex="0", proper ARIA roles/labels (e.g., role="tab" or role="button"
on the tab div, aria-selected when file.id === activeId, aria-label on closeBtn
and addBtn), and attach keydown handlers that activate the same logic as click
for Enter and Space (use the tab.dataset.file to identify the file and call the
same file-switch/close handlers). Also ensure the closeBtn and addBtn are
reachable via keyboard (tabindex or real <button>), and apply the same fixes to
the analogous rendering code referenced around lines 368-389 (the other tab/tree
render function) so keyboard activation and ARIA state are consistent.
- Around line 427-447: addFileAction is currently creating a FileTab with
id/name == fileName and unconditionally appending it to filesState, allowing
duplicate ids; update addFileAction to check filesState.get() for an existing
file with the same id or name before adding (compare against existing FileTab.id
and FileTab.name), and if a duplicate exists either reject the creation (abort
and show a prompt/alert) or generate a unique id (e.g., by appending a suffix)
before calling filesState.set(...) and activeFileState.set(...); ensure all
references to newFile use the final unique id so tab selection/close and VFS
sync remain unambiguous.
- Around line 696-705: The rename path only calls renameFileInVFS(fileName,
newName) and never updates the in-memory UI state (filesState and
activeFileState), causing the UI to show the old name; after
import("./vfs").then(vfs => vfs.renameFileInVFS(...)) also update the app state
by finding and replacing the filename in filesState (update the file object
key/name and any references) and, if activeFileState matches the old name, set
activeFileState to the trimmed newName; perform these updates after the VFS
promise resolves and handle failures by not mutating state if renameFileInVFS
throws or returns an error.

In `@Build/src/runner.ts`:
- Around line 82-98: The docContent assembly embeds raw css and js into <style>
and <script> tags (in the non-document fallback branch) which allows user
content containing literal "</style>" or "</script>" to break the HTML; update
the non-document fallback path that builds docContent to escape closing tags in
the css and js variables (same escaping used elsewhere in the codebase) before
inlining so that any occurrences of "</style>" and "</script>" are neutralized;
apply the same fix to the other fallback block referenced around the 118-123
range and ensure you update the variables used here (css, js,
consoleInterceptor, assembleFullHtml, hasDocTag) so both branches behave
consistently.
- Around line 51-57: getFileByExt only returns the first matching file, so
runCode's three lookups (for HTML, CSS, JS/MJS) drop additional files; change
the flow to collect and include all files of a given extension instead of one.
Update getFileByExt to return an array (e.g., getFilesByExt or similar) or add a
new getFilesByExt(files, ext) utility that finds all files where
f.name.split(".").pop() matches ext (optionally sort by name to preserve
deterministic order), then update runCode where it currently calls getFileByExt
(and the similar logic at the other referenced spots) to join/concatenate all
returned contents (with appropriate separators) for HTML, CSS and JS/MJS so
every source file is injected into the preview/export.

In `@Build/src/vfs.ts`:
- Around line 92-96: The catch block currently marks the VFS as ready even on
failure by calling _readyResolve() and setting _ready = true; instead, do not
resolve readiness on error—propagate the failure: remove the _ready = true and
_readyResolve() calls and reject the ready promise with the caught error (e.g.,
call an existing _readyReject(err) or create one when the readiness promise is
constructed), or rethrow the error so ready() returns a rejected promise; ensure
any consumers of ready() observe the failure rather than proceeding against a
broken VFS.
- Around line 138-148: The createFileInVFS function currently interpolates raw
names into `/sandbox/${name}` and appends entries blindly to filesState, so
validate and normalize the incoming name: reject or sanitize names containing
path separators (`/` or `\`) or parent segments (`..`) and use only the basename
(e.g., via path.basename-equivalent) to prevent sandbox escape; resolve the
target path and assert it is within `/sandbox` before writing. Also prevent
duplicate tabs by checking filesState for an existing FileTab.id/name and update
the entry instead of pushing a second one (keep existing content/language merge
logic), and apply the same validation/sanitization and duplicate-checking
pattern to the other VFS functions that write/read `/sandbox` (the same code
paths referenced in the review). Ensure you reference createFileInVFS,
filesState, activeFileState, FileTab, and getLanguageForExt when making the
changes.
- Around line 241-259: gitAddAll only stages files present in filesState so
deletions (removed by deleteFileInVFS which updates filesState) are never staged
and isomorphic-git requires git.remove for deletions; modify gitCommit (or
gitAddAll) to detect deleted paths and call git.remove for each before
committing. Specifically, after calling filesState.get() (or before commit
inside gitCommit) use isomorphic-git's statusMatrix() or git.status() to find
paths that are missing/deleted compared to HEAD, or track deletions from
deleteFileInVFS, then call git.remove({fs, dir: GIT_DIR, filepath:
<deletedPath>}) for each deleted file and only then proceed to call git.add for
existing files and git.commit; ensure functions referenced are gitAddAll,
gitCommit, deleteFileInVFS, filesState and use git.remove() to stage deletions.

---

Duplicate comments:
In `@Build/src/appState.ts`:
- Around line 22-44: Persisted values are used directly into initial signals
(filesInitial, activeFileInitial, darkModeInitial, autoRunInitial,
activeOutputInitial, splitSizesInitial, logFiltersInitial) which can break
startup if storage is corrupted; add small sanitizer/validator helpers (e.g.,
sanitizeFiles, sanitizeActiveFile, sanitizeBooleans, sanitizeSplitSizes,
sanitizeLogFilters, sanitizeActiveOutput) that confirm types/shape (files is an
array of objects with required id/content/title, splitSizes is two numbers in
0-100, logFilters is array of allowed strings, activeOutput is one of allowed
values, darkMode/autoRun are booleans) and return safe defaults (defaultFiles,
["log","error","warn","info"], [50,50], "preview", etc.) then wire the
initializers to use these sanitizers instead of directly using persistedState so
the app always gets validated shapes before applyStateSnapshot runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a846325-348b-45eb-88b0-11585272ccd8

📥 Commits

Reviewing files that changed from the base of the PR and between 329d0ac and 3e741de.

⛔ Files ignored due to path filters (1)
  • Build/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • Build/eslint.config.mts
  • Build/index.html
  • Build/package.json
  • Build/src/appState.ts
  • Build/src/biome.ts
  • Build/src/console.ts
  • Build/src/defaultContent.ts
  • Build/src/editor.ts
  • Build/src/git.worker.ts
  • Build/src/global.d.ts
  • Build/src/main.ts
  • Build/src/runner.ts
  • Build/src/types.ts
  • Build/src/ui.ts
  • Build/src/utils.ts
  • Build/src/vfs.ts
  • Build/styles/styles.css
  • Build/tsconfig.json
  • Build/vite.config.mjs
✅ Files skipped from review due to trivial changes (1)
  • Build/src/git.worker.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • Build/src/global.d.ts
  • Build/tsconfig.json
  • Build/src/utils.ts
  • Build/src/console.ts
  • Build/vite.config.mjs

Comment thread Build/index.html
<div class="tab" data-output="console">Console</div>
</div>
<div class="output-content">
<iframe id="preview" class="preview active"></iframe>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Sandbox the preview iframe.

This runner executes user code inside #preview. Without a sandbox attribute, previewed scripts can reach the parent app and its storage through window.parent, which breaks isolation.

🔒 Minimal hardening
-                    <iframe id="preview" class="preview active"></iframe>
+                    <iframe id="preview" class="preview active" sandbox="allow-scripts"></iframe>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/index.html` at line 59, The preview iframe (<iframe id="preview"
class="preview active">) lacks a sandbox attribute allowing previewed scripts to
access the parent; add a sandbox attribute to harden isolation (e.g.,
sandbox="allow-scripts" for running user scripts but preventing access to
window.parent/storage) on the iframe with id="preview" and avoid including
permissions like allow-same-origin or allow-top-navigation unless explicitly
required.

Comment thread Build/package.json
"build": "vite build",
"build:single": "cross-env SINGLE_FILE=true npm run build",
"preview": "vite preview",
"lint": "eslint \"src/**/*.{ts,js}\" --ext .ts,.js || true",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Let the lint script fail normally.

|| true makes npm run lint exit successfully even when ESLint reports errors, so CI/local checks can't block regressions anymore. If you want a non-blocking helper, keep that as a separate script instead of weakening the main lint target.

Suggested fix
-    "lint": "eslint \"src/**/*.{ts,js}\" --ext .ts,.js || true",
+    "lint": "eslint \"src/**/*.{ts,js}\" --ext .ts,.js",
📝 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
"lint": "eslint \"src/**/*.{ts,js}\" --ext .ts,.js || true",
"lint": "eslint \"src/**/*.{ts,js}\" --ext .ts,.js",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/package.json` at line 12, The lint script in package.json currently
silences ESLint failures by appending "|| true" to the "lint" script; remove the
"|| true" from the "lint" script so ESLint errors cause a non-zero exit (i.e.,
change the "lint" script value to "eslint \"src/**/*.{ts,js}\" --ext .ts,.js"),
and if you need a non-blocking helper add a separate script like "lint:ci-safe"
or "lint:soft" that includes "|| true".

Comment thread Build/src/biome.ts
Comment on lines +32 to +55
export async function initBiome(): Promise<void> {
if (initialized) return;

await initBiomeWasm();

workspace = new Workspace();

const openRes = workspace.openProject({ path: "", openUninitialized: true });
projectKey = openRes.projectKey;

workspace.updateSettings({
projectKey,
configuration: {
linter: {
enabled: true,
rules: {
recommended: true,
},
},
},
workspaceDirectory: "./",
});

initialized = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make Biome initialization single-flight.

Concurrent lintWithBiome() calls can both enter initBiome() before initialized becomes true, which races workspace and projectKey assignment. Cache the in-flight promise and await it from all callers.

🛠️ Minimal pattern
 let workspace: Workspace | null = null;
 let projectKey: number | null = null;
 let initialized = false;
+let initPromise: Promise<void> | null = null;

 export async function initBiome(): Promise<void> {
   if (initialized) return;
+  if (initPromise) return initPromise;
-
-  await initBiomeWasm();
-
-  workspace = new Workspace();
-
-  const openRes = workspace.openProject({ path: "", openUninitialized: true });
-  projectKey = openRes.projectKey;
-
-  workspace.updateSettings({
-    projectKey,
-    configuration: {
-      linter: {
-        enabled: true,
-        rules: {
-          recommended: true,
-        },
-      },
-    },
-    workspaceDirectory: "./",
-  });
-
-  initialized = true;
+  initPromise = (async () => {
+    await initBiomeWasm();
+
+    workspace = new Workspace();
+
+    const openRes = workspace.openProject({ path: "", openUninitialized: true });
+    projectKey = openRes.projectKey;
+
+    workspace.updateSettings({
+      projectKey,
+      configuration: {
+        linter: {
+          enabled: true,
+          rules: {
+            recommended: true,
+          },
+        },
+      },
+      workspaceDirectory: "./",
+    });
+
+    initialized = true;
+  })();
+
+  try {
+    await initPromise;
+  } catch (err) {
+    initPromise = null;
+    throw err;
+  }
 }
📝 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
export async function initBiome(): Promise<void> {
if (initialized) return;
await initBiomeWasm();
workspace = new Workspace();
const openRes = workspace.openProject({ path: "", openUninitialized: true });
projectKey = openRes.projectKey;
workspace.updateSettings({
projectKey,
configuration: {
linter: {
enabled: true,
rules: {
recommended: true,
},
},
},
workspaceDirectory: "./",
});
initialized = true;
let workspace: Workspace | null = null;
let projectKey: number | null = null;
let initialized = false;
let initPromise: Promise<void> | null = null;
export async function initBiome(): Promise<void> {
if (initialized) return;
if (initPromise) return initPromise;
initPromise = (async () => {
await initBiomeWasm();
workspace = new Workspace();
const openRes = workspace.openProject({ path: "", openUninitialized: true });
projectKey = openRes.projectKey;
workspace.updateSettings({
projectKey,
configuration: {
linter: {
enabled: true,
rules: {
recommended: true,
},
},
},
workspaceDirectory: "./",
});
initialized = true;
})();
try {
await initPromise;
} catch (err) {
initPromise = null;
throw err;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/biome.ts` around lines 32 - 55, Concurrent calls to initBiome can
race, so introduce an in-flight promise (e.g. initBiomePromise: Promise<void> |
null) and use it to ensure single-flight initialization: at the start of
initBiome check initialized and return, then if initBiomePromise exists await
and return; otherwise assign initBiomePromise = (async () => { await
initBiomeWasm(); workspace = new Workspace(); const openRes =
workspace.openProject({ path: "", openUninitialized: true }); projectKey =
openRes.projectKey; workspace.updateSettings({ projectKey, configuration: {
linter: { enabled: true, rules: { recommended: true } } }, workspaceDirectory:
"./", }); initialized = true; })(); await initBiomePromise; finally clear
initBiomePromise if you prefer (or leave until process end) so all callers await
the same promise and avoid racing workspace/projectKey assignment.

Comment thread Build/src/editor.ts
Comment on lines +189 to +207
effect(() => {
const files = filesState.get();
const activeId = activeFileState.get();
const file = files.find((f) => f.id === activeId);
if (file && editor.view) {
const currentDoc = editor.view.state.doc.toString();
if (currentDoc !== file.content && !_suppressContentUpdate) {
_suppressContentUpdate = true;
editor.view.dispatch({
changes: {
from: 0,
to: editor.view.state.doc.length,
insert: file.content,
},
});
editor.view.dispatch({
effects: _languageCompartment.reconfigure(getLanguageExtension(file.name)),
});
_suppressContentUpdate = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Renaming the active file can leave the editor in the wrong language mode.

The _languageCompartment reconfigure is gated by currentDoc !== file.content. If the active file is renamed from script.js to styles.css without changing content, the mode never updates until the next edit.

♻️ Minimal fix
 effect(() => {
   const files = filesState.get();
   const activeId = activeFileState.get();
   const file = files.find((f) => f.id === activeId);
   if (file && editor.view) {
+    editor.view.dispatch({
+      effects: _languageCompartment.reconfigure(getLanguageExtension(file.name)),
+    });
+
     const currentDoc = editor.view.state.doc.toString();
     if (currentDoc !== file.content && !_suppressContentUpdate) {
       _suppressContentUpdate = true;
       editor.view.dispatch({
         changes: {
           from: 0,
           to: editor.view.state.doc.length,
           insert: file.content,
         },
       });
-      editor.view.dispatch({
-        effects: _languageCompartment.reconfigure(getLanguageExtension(file.name)),
-      });
       _suppressContentUpdate = false;
     }
   }
 });
📝 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
effect(() => {
const files = filesState.get();
const activeId = activeFileState.get();
const file = files.find((f) => f.id === activeId);
if (file && editor.view) {
const currentDoc = editor.view.state.doc.toString();
if (currentDoc !== file.content && !_suppressContentUpdate) {
_suppressContentUpdate = true;
editor.view.dispatch({
changes: {
from: 0,
to: editor.view.state.doc.length,
insert: file.content,
},
});
editor.view.dispatch({
effects: _languageCompartment.reconfigure(getLanguageExtension(file.name)),
});
_suppressContentUpdate = false;
effect(() => {
const files = filesState.get();
const activeId = activeFileState.get();
const file = files.find((f) => f.id === activeId);
if (file && editor.view) {
editor.view.dispatch({
effects: _languageCompartment.reconfigure(getLanguageExtension(file.name)),
});
const currentDoc = editor.view.state.doc.toString();
if (currentDoc !== file.content && !_suppressContentUpdate) {
_suppressContentUpdate = true;
editor.view.dispatch({
changes: {
from: 0,
to: editor.view.state.doc.length,
insert: file.content,
},
});
_suppressContentUpdate = false;
}
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/editor.ts` around lines 189 - 207, The effect currently only
reconfigures _languageCompartment when the file content changes, so renaming a
file (e.g., script.js → styles.css) with identical content won't update the
editor language; fix by tracking the active filename (e.g., add a closure
variable lastActiveFileName) and, inside effect() along with the existing
content check on editor.view, also compare file.name to lastActiveFileName and
dispatch the _languageCompartment.reconfigure(getLanguageExtension(file.name))
when the name changed (then set lastActiveFileName = file.name); ensure this
runs regardless of content equality but still respects the
_suppressContentUpdate flow and only triggers the reconfigure when the filename
actually changed.

Comment thread Build/src/main.ts
Comment on lines +188 to +194
const text = entries
.map((ev) => {
const timestamp = new Date(ev.timestamp).toLocaleTimeString();
const message = ev.data
.map((d) => (typeof d === "object" ? JSON.stringify(d) : String(d)))
.join(" ");
return `${timestamp} ${message}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard console-copy serialization per value.

JSON.stringify(d) throws on circular objects and some DOM payloads. One such log entry will make “Copy All” fail for the entire console history.

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

In `@Build/src/main.ts` around lines 188 - 194, The map that builds console text
uses JSON.stringify(d) directly and can throw on circular or DOM objects; update
the inner mapping for ev.data inside the entries.map callback so each value (d)
is serialized safely: wrap JSON.stringify(d) in a try/catch (or use a safe
replacer approach) and fall back to String(d) or a non-throwing representation
when stringify fails; ensure the change is applied to the message construction
inside the entries.map(ev => { ... }) block so a single bad value cannot break
the whole "Copy All" output.

Comment thread Build/src/runner.ts Outdated
Comment thread Build/src/runner.ts Outdated
Comment on lines +82 to +98
const docContent = hasDocTag
? assembleFullHtml(html, css, js)
: [
'<!DOCTYPE html><html><head><meta charset="UTF-8">',
"<style>",
css,
"</style>",
"<script>",
consoleInterceptor,
"</script>",
"</head><body>",
html,
"</body>",
"<script>",
js,
"</script></html>",
].join("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape closing tags before inlining user CSS and JS.

Raw css and js are embedded inside <style> / <script> tags. If user code contains a literal </style> or </script>, the browser closes the tag early and the preview HTML becomes invalid.

Suggested fix
 function assembleFullHtml(html: string, css: string, js: string): string {
+  const safeCss = css.replace(/<\/style/gi, "<\\/style");
+  const safeJs = js.replace(/<\/script/gi, "<\\/script");
   const interceptorScript = `<script>${consoleInterceptor}<\/script>`;
-  const styleTag = css.trim() ? `<style>${css}<\/style>` : "";
-  const scriptTag = js.trim() ? `<script>${js}<\/script>` : "";
+  const styleTag = safeCss.trim() ? `<style>${safeCss}<\/style>` : "";
+  const scriptTag = safeJs.trim() ? `<script>${safeJs}<\/script>` : "";

Apply the same escaped values in the non-document fallback path as well.

Also applies to: 118-123

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

In `@Build/src/runner.ts` around lines 82 - 98, The docContent assembly embeds raw
css and js into <style> and <script> tags (in the non-document fallback branch)
which allows user content containing literal "</style>" or "</script>" to break
the HTML; update the non-document fallback path that builds docContent to escape
closing tags in the css and js variables (same escaping used elsewhere in the
codebase) before inlining so that any occurrences of "</style>" and "</script>"
are neutralized; apply the same fix to the other fallback block referenced
around the 118-123 range and ensure you update the variables used here (css, js,
consoleInterceptor, assembleFullHtml, hasDocTag) so both branches behave
consistently.

Comment thread Build/src/vfs.ts
Comment on lines +92 to +96
} catch (err) {
console.error("VFS init error:", err);
_ready = true;
_readyResolve();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't report the VFS as ready after init failure.

This catch resolves ready() as success even when mount or git initialization failed. Callers then proceed against a broken filesystem instead of surfacing the boot error.

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

In `@Build/src/vfs.ts` around lines 92 - 96, The catch block currently marks the
VFS as ready even on failure by calling _readyResolve() and setting _ready =
true; instead, do not resolve readiness on error—propagate the failure: remove
the _ready = true and _readyResolve() calls and reject the ready promise with
the caught error (e.g., call an existing _readyReject(err) or create one when
the readiness promise is constructed), or rethrow the error so ready() returns a
rejected promise; ensure any consumers of ready() observe the failure rather
than proceeding against a broken VFS.

Comment thread Build/src/vfs.ts Outdated
Comment on lines +138 to +148
export async function createFileInVFS(
name: string,
content = "",
): Promise<void> {
const ext = name.split(".").pop()?.toLowerCase() || "";
const language = getLanguageForExt(ext);
await fs.promises.writeFile(`/sandbox/${name}`, content, "utf-8");
const files = filesState.get();
const newFile: FileTab = { id: name, name, content, language };
filesState.set([...files, newFile]);
activeFileState.set(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate file names before using them as VFS paths.

These APIs interpolate raw names directly into /sandbox/${...}. Inputs containing /, \, or .. can escape the sandbox path, and duplicate names can desynchronize state from disk by appending a second tab entry for the same file.

🧱 Minimal guard
+function assertValidFileName(name: string, existingNames: string[] = []): void {
+  if (!name || name.includes("/") || name.includes("\\") || name.includes("..")) {
+    throw new Error("Invalid file name");
+  }
+  if (existingNames.includes(name)) {
+    throw new Error("File already exists");
+  }
+}
+
 export async function createFileInVFS(
   name: string,
   content = "",
 ): Promise<void> {
+  assertValidFileName(name, filesState.get().map((f) => f.name));
   const ext = name.split(".").pop()?.toLowerCase() || "";
   const language = getLanguageForExt(ext);
   await fs.promises.writeFile(`/sandbox/${name}`, content, "utf-8");
   const files = filesState.get();
   const newFile: FileTab = { id: name, name, content, language };
   filesState.set([...files, newFile]);
   activeFileState.set(name);
 }

Also applies to: 151-155, 166-167

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

In `@Build/src/vfs.ts` around lines 138 - 148, The createFileInVFS function
currently interpolates raw names into `/sandbox/${name}` and appends entries
blindly to filesState, so validate and normalize the incoming name: reject or
sanitize names containing path separators (`/` or `\`) or parent segments (`..`)
and use only the basename (e.g., via path.basename-equivalent) to prevent
sandbox escape; resolve the target path and assert it is within `/sandbox`
before writing. Also prevent duplicate tabs by checking filesState for an
existing FileTab.id/name and update the entry instead of pushing a second one
(keep existing content/language merge logic), and apply the same
validation/sanitization and duplicate-checking pattern to the other VFS
functions that write/read `/sandbox` (the same code paths referenced in the
review). Ensure you reference createFileInVFS, filesState, activeFileState,
FileTab, and getLanguageForExt when making the changes.

Comment thread Build/src/vfs.ts
Comment on lines +241 to +259
export async function gitAddAll(): Promise<void> {
const files = filesState.get();
for (const file of files) {
try {
await git.add({ fs, dir: GIT_DIR, filepath: file.name });
} catch (err) {
console.warn(`git add ${file.name} failed:`, err);
}
}
}

export async function gitCommit(message: string): Promise<string> {
await gitAddAll();
const sha = await git.commit({
fs,
dir: GIT_DIR,
message,
author: GIT_AUTHOR,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For isomorphic-git 1.38.x, does git.add({ fs, dir, filepath })stage deletions, or do deleted files needgit.remove()/statusMatrix()handling beforecommit?

💡 Result:

For isomorphic-git 1.38.x, git.add({ fs, dir, filepath }) does NOT reliably “stage deletions” the way native git add does. If the file is deleted in the working tree, you should handle it explicitly with git.remove() (and typically decide based on git.status / statusMatrix). Evidence from isomorphic-git issues and docs: - A reported bug/behavior shows that running git.add on a removed file can throw an error, and the suggested workaround is to use git.remove for deleted files instead of git.add [1]. - The isomorphic-git add docs describe adding a file to the git index (staging area) but do not state that it stages removals automatically; the existence of a separate remove command plus the workaround indicates deletions are not handled by add alone in this library version [2]. - A common implementation pattern is: check git.status for *deleted, and if so call git.remove({ fs, dir, filepath }) rather than git.add [1]. So, before commit, you generally need deletion-aware staging logic: - For added/modified paths: call git.add(...). - For deleted paths: call git.remove(...) (decided via git.status or statusMatrix). Practical decision snippet (from the issue’s workaround): - compute status; if status indicates deleted, call git.remove, else call git.add [1]. If you skip the explicit deletion handling and only call git.add for deleted paths, you risk either errors or the deletion not being staged, leading to incorrect commit results.

Citations:


🏁 Script executed:

# First, locate and examine the vfs.ts file
find . -name "vfs.ts" -type f

Repository: HTMLToolkit/HTMLRunner

Length of output: 85


🏁 Script executed:

# Read the specified lines and surrounding context to verify the code
sed -n '230,270p' Build/src/vfs.ts

Repository: HTMLToolkit/HTMLRunner

Length of output: 983


🏁 Script executed:

# Search for deleteFileInVFS implementation
rg "deleteFileInVFS" -A 10 -B 2

Repository: HTMLToolkit/HTMLRunner

Length of output: 1883


🏁 Script executed:

# Search for git.remove usage in the codebase
rg "git\.remove" -B 2 -A 2

Repository: HTMLToolkit/HTMLRunner

Length of output: 48


🏁 Script executed:

# Check how filesState is defined and if it tracks deleted files
rg "filesState" -B 3 -A 3 | head -80

Repository: HTMLToolkit/HTMLRunner

Length of output: 4350


Deleted files are never staged before commit.

gitAddAll() only processes files currently in filesState. When deleteFileInVFS() removes a file from disk, it also removes it from filesState with filesState.set(updated). Since the deleted file is no longer tracked in filesState, it never reaches git.add(), and the deletion is not staged.

Additionally, isomorphic-git 1.38.x does not handle deletions via git.add()—deleted files must be explicitly staged with git.remove(). This is never called in the codebase, so file deletions are completely ignored by gitCommit().

Handle deletion tracking explicitly, either by detecting file removals via git.status() / statusMatrix() and calling git.remove() for deleted paths before gitCommit(), or by maintaining a separate record of deleted files.

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

In `@Build/src/vfs.ts` around lines 241 - 259, gitAddAll only stages files present
in filesState so deletions (removed by deleteFileInVFS which updates filesState)
are never staged and isomorphic-git requires git.remove for deletions; modify
gitCommit (or gitAddAll) to detect deleted paths and call git.remove for each
before committing. Specifically, after calling filesState.get() (or before
commit inside gitCommit) use isomorphic-git's statusMatrix() or git.status() to
find paths that are missing/deleted compared to HEAD, or track deletions from
deleteFileInVFS, then call git.remove({fs, dir: GIT_DIR, filepath:
<deletedPath>}) for each deleted file and only then proceed to call git.add for
existing files and git.commit; ensure functions referenced are gitAddAll,
gitCommit, deleteFileInVFS, filesState and use git.remove() to stage deletions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
Build/src/console.ts (1)

57-86: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the parsed stack filename before jumping in the editor.

The regexes capture the source file, but the click handler ignores it and always selects a line in whatever tab is currently active. In the new multi-file model, a script.js stack while index.html is open jumps to the wrong document.

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

In `@Build/src/console.ts` around lines 57 - 86, The stack parsing extracts the
source filename in capture group m[1] but the click handler always reads the
current tab's document; update the code that builds the match (patterns loop) to
store the parsed filename (e.g., const fileName = m[1]) and inside the
lineEl.addEventListener("click", ...) ensure you switch/open that file in the
editor before reading editor.view.state.doc — e.g., call
editor.openFile(fileName) or the appropriate method to make that file the active
view, then compute maxLine/safeLine/lineObj/offset and dispatch the selection as
currently implemented.
♻️ Duplicate comments (11)
Build/src/vfs.ts (3)

73-77: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Initialization failures still resolve ready() as success.

The catch path sets _ready = true and resolves _readyPromise, so callers proceed against a broken mount after worker/configure/git init failed.

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

In `@Build/src/vfs.ts` around lines 73 - 77, The catch block in the initialization
path currently sets _ready = true and calls _readyResolve(), which incorrectly
signals success to callers of ready(); instead, preserve a failure state and
reject the ready promise: add or use an existing _readyReject (create one if
missing alongside _readyResolve/_readyPromise), do NOT set _ready = true in the
catch, and call _readyReject(err) (or throw/reject the promise with the error)
so ready() consumers receive the failure; update any initialization helpers (the
catch block around VFS init and the ready() consumer behavior) to propagate that
rejection rather than resolving as success.

110-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep VFS mutations inside /sandbox.

These helpers interpolate raw names directly into /sandbox/${...}. Nested folders are fine, but ../, leading slashes, or backslashes can still escape the sandbox and rename/delete .git or other mounted paths.

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

In `@Build/src/vfs.ts` around lines 110 - 126, Sanitize and validate the
user-supplied file names in createFileInVFS, deleteFileInVFS, and
renameFileInVFS so they cannot escape the `/sandbox` root: normalize the name
(replace backslashes, remove any leading slashes, and collapse `..` segments),
then resolve/join it with the sandbox base and assert the resulting path begins
with `/sandbox/`; if it does not, throw an error. Apply the same check for both
source and destination in renameFileInVFS and reuse/centralize the logic (e.g.,
a helper like ensureSandboxPath) before calling ensureParentDir,
fs.promises.writeFile, unlink, or rename.

207-226: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Deleted files still never reach commit staging.

gitAddAll() only iterates filesState.get(). Once a file is removed from state/VFS, gitCommit() has no path left to stage that deletion, so commits can succeed while silently keeping removed files in history.

Build/src/main.ts (2)

784-802: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject duplicate filenames before appending to filesState.

id and name are both keyed by fileName, but this path appends unconditionally. Adding the same name twice makes tab switching, rename, close, and VFS sync ambiguous because every lookup is name-based.

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

In `@Build/src/main.ts` around lines 784 - 802, In addFileAction, avoid appending
a FileTab with an id/name that already exists: before creating/setting newFile,
read filesState.get() and check for an existing entry where f.id === fileName ||
f.name === fileName; if a duplicate is found, abort (e.g., show an alert or
prompt) and do not call filesState.set(...) or change activeFileState; otherwise
proceed to push the new FileTab. Ensure you reference addFileAction, FileTab,
filesState, activeFileState and fileName when making this check so all
name-based lookups remain unambiguous.

560-587: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

File navigation is still mouse-only.

Tabs, file items, and folder toggles are rendered as plain div/span nodes with click handlers only. Keyboard users still cannot focus or activate file switches, close buttons, or folder expansion, which blocks basic editor navigation.

Also applies to: 674-733

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

In `@Build/src/main.ts` around lines 560 - 587, renderFileTabs renders tabs as
non-focusable divs which prevents keyboard users from switching files; update
renderFileTabs so each tab is an interactive element (preferably a <button> or a
div with tabindex="0" and role="tab") and add keyboard handlers for Enter/Space
to activate the tab, set aria-selected (true/false) and aria-controls or
aria-label on the element, ensure closeBtn keeps being a <button> (it already
is) and has an accessible label (e.g., aria-label="Close {file.name}"), and
ensure addBtn remains focusable with a clear aria-label; apply the same changes
to the corresponding file item and folder toggle renderer (the block referenced
at 674-733) so folder toggles are focusable, respond to Enter/Space, and expose
proper ARIA roles (e.g., button or treeitem/aria-expanded) for keyboard
navigation.
Build/src/runner.ts (1)

384-395: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Blob fallback still breaks on literal </style> / </script>.

inlineCssFiles() and inlineJsFiles() embed raw user content inside <style> / <script> tags. A literal closing tag terminates the wrapper early and corrupts the generated fallback HTML.

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

In `@Build/src/runner.ts` around lines 384 - 395, The inlineCssFiles and
inlineJsFiles functions embed raw user content inside <style> and <script> tags
which breaks when the content contains literal closing tags; sanitize user
content before wrapping by escaping any closing tag sequences (e.g. replace
occurrences of </style> and </script> with <\/style> and <\/script> respectively
or similar safe escape) so the wrapper tags are not terminated early; update
inlineCssFiles and inlineJsFiles to perform this replacement on f.content
(preserving trimming) before embedding.
Build/src/ui.ts (1)

18-20: ⚠️ Potential issue | 🟡 Minor

Cancel the previous error-clear timer.

A second showError() scheduled within 5 seconds can still be cleared by the first timer almost immediately, so the newest error does not remain visible for the intended interval.

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

In `@Build/src/ui.ts` around lines 18 - 20, The showError function currently sets
a timeout to clear errorMessage but doesn't cancel any prior timeout, so a
previously scheduled timer can clear a newly shown error early; fix this by
adding a module-scoped variable (e.g., errorClearTimeout) to hold the current
timeout id, call clearTimeout(errorClearTimeout) before creating a new
setTimeout in showError, assign the new timeout id to errorClearTimeout, and
keep the existing behavior of clearing errorMessage after 5000ms; reference the
showError function and the errorMessage state when making the change.
Build/src/biome.ts (1)

32-61: ⚠️ Potential issue | 🟠 Major

Make initBiome() single-flight.

Two overlapping lintWithBiome() calls can still pass the initialized check before the flag flips, so they race workspace / projectKey assignment and may leak an extra workspace.

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

In `@Build/src/biome.ts` around lines 32 - 61, initBiome currently races when
called concurrently because multiple calls can pass the boolean initialized
check; introduce a single-flight promise to serialize initialization: add a
module-level variable like initBiomePromise: Promise<void> | null and change
initBiome to return/await that promise when present, otherwise assign
initBiomePromise = (async () => { ... existing init logic ... })(); ensure the
promise is cleared (set to null) after success or failure and that initialized
is set inside the serialized block; reference initBiome, initialized, workspace,
projectKey and initBiomeWasm so you update those symbols within the new
single-flight wrapper.
Build/src/appState.ts (1)

22-44: ⚠️ Potential issue | 🟠 Major

Validate persisted state before it becomes live.

This reintroduces the earlier local-storage validation issue in the new files snapshot shape: the module-level initializers still trust raw persisted values, and applyStateSnapshot() only partially validates files. A malformed snapshot can seed invalid FileTabs or unsupported UI values before hydration, then get written back out as if it were valid.

Also applies to: 166-203

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

In `@Build/src/appState.ts` around lines 22 - 44, The module-level initializers
(filesInitial, activeFileInitial, darkModeInitial, autoRunInitial,
splitSizesInitial, logFiltersInitial) are trusting raw persistedState and can
seed invalid UI state; update the initializers and applyStateSnapshot() to fully
validate and sanitize persistedState before using it: validate that
persistedState.files is an array of valid FileTab objects (check required fields
and types) and otherwise fall back to defaultFiles, ensure activeFileInitial is
a string that matches a validated file id (otherwise pick defaultFiles[0].id),
coerce darkMode/autoRun only from explicit "true"/"false" strings or booleans,
validate splitSizes is an array of two finite numbers between 0–100 (fallback to
[50,50]), and sanitize logFilters to an array of allowed filter strings
(fallback to default list); apply the same validation logic in
applyStateSnapshot() (and in the code paths referenced around the other affected
block) so malformed snapshots never become live or get re-saved.
Build/src/editor.ts (1)

198-215: ⚠️ Potential issue | 🟡 Minor

Reconfigure the language compartment on rename, not only on content sync.

If the active file is renamed from script.js to styles.css without changing its contents, this effect never runs the reconfigure(), so the editor stays in the old mode until the next edit or file switch.

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

In `@Build/src/editor.ts` around lines 198 - 215, The effect currently only calls
_languageCompartment.reconfigure when content changes, so renames (e.g.,
script.js -> styles.css) don't update the language; fix by tracking the previous
active file name (e.g., add a outer-scope variable prevActiveFileName) and
inside the effect compare file.name to prevActiveFileName and call
_languageCompartment.reconfigure(getLanguageExtension(file.name)) when the name
changed (even if content is identical), then set prevActiveFileName = file.name;
keep the existing content-sync/_suppressContentUpdate logic and ensure you still
call reconfigure when content changes as before.
Build/src/console.ts (1)

153-180: ⚠️ Potential issue | 🟠 Major

Guard nested non-object primitives before the WeakSet path.

Values like undefined, bigint, or symbol inside logged objects still fall through to visited.add(...), which throws because WeakSet only accepts objects. One bad property breaks rendering of the whole console entry.

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

In `@Build/src/console.ts` around lines 153 - 180, The renderObject function can
call visited.add(asObj as object) for non-object primitives (undefined, bigint,
symbol, function) and crash; before treating obj as an object/array (before the
asObj cast, visited.has and visited.add calls), add a guard that checks typeof
obj !== "object" && typeof obj !== "function" (or equivalently ensure obj is an
object and not null) and return document.createTextNode(String(obj)) for those
primitives; update the logic around renderObject, the asObj cast, and the
visited.add(asObj as object) usage so only real objects/arrays are added to the
WeakSet.
🧹 Nitpick comments (1)
Build/vite.config.mjs (1)

78-78: ⚡ Quick win

Consider renaming the parameter for clarity.

The parameter is named svg but the actual file is public/favicon.png (PNG). Since the plugin now handles both SVG and non-SVG files, renaming the parameter to image or favicon would better reflect its actual purpose and reduce confusion.

♻️ Proposed refactor

In the plugin function signature:

-function inlineSvgFaviconPlugin(options) {
+function inlineFaviconPlugin(options) {
   return {
-    name: "inline-svg-favicon",
+    name: "inline-favicon",
     enforce: "post",
     transformIndexHtml(html) {
-      if (!fs.existsSync(options.svg)) return html;
-      const ext = path.extname(options.svg).toLowerCase();
+      if (!fs.existsSync(options.image)) return html;
+      const ext = path.extname(options.image).toLowerCase();
       let faviconTag = "";

       try {
         if (ext === ".svg") {
-          let svgContent = fs.readFileSync(options.svg, "utf8");
+          let svgContent = fs.readFileSync(options.image, "utf8");
           svgContent = svgContent
             .replace(/<\?xml[^>]*>\s*/g, "")
             .replace(/\s+/g, " ");
           const base64 = Buffer.from(svgContent).toString("base64");
           faviconTag = `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${base64}"/>\n`;
         } else {
           // Non-SVG (png, ico, etc.) — read binary and base64-encode
-          const buf = fs.readFileSync(options.svg);
+          const buf = fs.readFileSync(options.image);
           const base64 = buf.toString("base64");
           const mime = ext === ".png" ? "image/png" : ext === ".ico" ? "image/x-icon" : "application/octet-stream";
           faviconTag = `<link rel="icon" type="${mime}" href="data:${mime};base64,${base64}"/>\n`;
         }
       } catch (e) {
         // If reading fails, don't modify the HTML
         return html;
       }

       return html.replace(/<head>(.*?)/, `<head>$1\n  ${faviconTag}`);
     },
   };
 }

And update the call site:

-      isSingleFile && inlineSvgFaviconPlugin({ svg: "public/favicon.png" }),
+      isSingleFile && inlineFaviconPlugin({ image: "public/favicon.png" }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/vite.config.mjs` at line 78, The plugin call inlineSvgFaviconPlugin({
svg: "public/favicon.png" }) uses a misleading parameter name; update the plugin
function signature for inlineSvgFaviconPlugin to rename the parameter (e.g.,
from svg to image or favicon) and update all call sites (including this call in
vite.config.mjs) to pass the file under the new key (e.g., { image:
"public/favicon.png" } or { favicon: "public/favicon.png" }) so the name
reflects that non-SVG files are supported and avoids confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Build/src/appState.ts`:
- Around line 236-239: nextUntitledId is volatile and causes duplicate ids after
reload; change getNextUntitledId() so it derives the next untitled number from
the current file set instead of the module-scoped nextUntitledId counter. In
getNextUntitledId(), inspect the existing files collection (the same store/array
used elsewhere for file state), find the highest numeric suffix among ids
matching /^untitled-(\d+)\.html$/, and return `untitled-${max+1}.html` (fallback
to 1 if none); remove or stop using the module-level nextUntitledId to avoid
reset-on-reload collisions.

In `@Build/src/main.ts`:
- Around line 814-837: closeFileAction is currently deleting workspace files by
calling deleteFileInVFS after removing an entry from filesState; instead remove
only the UI/tab state and do not call deleteFileInVFS. Update closeFileAction
(and the analogous block around the 1028-1055 range) to only update filesState
and activeFileState and call switchToFile as needed, and remove or gate the
deleteFileInVFS(...) call so permanent deletion is performed only by a dedicated
delete action (e.g., deleteFileAction) or when an explicit "delete" flag is
passed.
- Around line 1288-1306: Startup currently hydrates filesState from the VFS and
then overwrites it with loadState(), causing race/stale writes; move the call to
loadState() to run before initializeVFS(worker) so persisted localStorage state
is applied first, then call initializeVFS(worker) which should merge (not
replace) with any existing filesState, and only then compute
initialFiles/initialActiveId and call createEditor(editorContainer,
initialFile); if initializeVFS currently blindly replaces filesState, update its
implementation to merge incoming VFS contents with existing filesState instead
of overwriting.
- Around line 1115-1128: deleteFolderAction currently only updates filesState
and never removes the corresponding files in the VFS, so deleted folder contents
reappear; after computing the new files list (and the deletedFiles as the
difference between previous filesState.get() and updated), remove those paths
from the VFS (e.g. call the VFS API to unlink/rm each deleted file or
recursively remove the folder under /sandbox) or alternatively clear the sandbox
VFS and re-run syncFilesToVFS(filesState.get()) to fully reconcile state; update
deleteFolderAction to perform that VFS removal step (use the same folderPath and
file.name values to locate VFS entries) before finishing and keep the existing
activeFileState/switchToFile logic.

In `@Build/src/runner.ts`:
- Around line 264-276: generatePreviewHtml currently always picks the first HTML
file, causing the preview to ignore the user's current active file; update it to
use the active preview entrypoint instead — either accept an activeFile
identifier (e.g., activeFileState.name) or detect an active flag on FileTab and
prefer that file when choosing htmlFile (fall back to first match if no active
file found). Modify the call site in Build/src/main.ts (where activeFileState is
updated and preview is rerun) to pass the active file name or ensure
FileTab.active is set so generatePreviewHtml uses that active file as the
entrypoint (keep existing hasDocTag logic and
injectIntoFullHtml/buildStandaloneHtml flow unchanged).
- Around line 247-260: populateSandboxCache is returning the preview URL before
cache.put() operations finish, so the iframe can navigate before the sandbox is
fully written; change the write loop and the index.html write to await the
cache.put() promises (e.g., collect promises from cache.put for each file and
the generated index.html and await Promise.all on them) so that
generatePreviewHtml/files loop and the htmlResp put (referencing the files
iteration, cache.put, generatePreviewHtml, and htmlResp) complete before
returning the preview URL to runCode().
- Around line 34-43: The inlineMarkdown function currently applies the link
replacement before the image replacement so markdown images match the link regex
and break; fix this by swapping the two .replace calls in inlineMarkdown so the
image regex (/!\[([^\]]*)\]\(([^)]+)\)/g) runs before the link regex
(/\[([^\]]+)\]\(([^)]+)\)/g), keeping the same callbacks that use safeUrl(url)
and preserve attributes (alt, loading, target, rel) to ensure images render
correctly.

In `@Build/src/sw.ts`:
- Around line 18-29: The worker currently hardcodes SANDBOX_PREFIX = "/sandbox/"
so requests under a non-root service-worker scope (e.g. /base/sandbox/) bypass
handleSandbox; change prefix derivation to compute it from the service worker
scope (use self.registration.scope), e.g. compute const scopePath = new
URL(self.registration.scope).pathname and set SANDBOX_PREFIX =
scopePath.replace(/\/?$/, "/") + "sandbox/"; then update the fetch listener to
use that computed SANDBOX_PREFIX when checking url.pathname.startsWith(...),
keeping handleSandbox(event.request, url) unchanged.

In `@Build/src/vfs.ts`:
- Around line 434-446: gitClone currently only calls removeGitDir which only
deletes /sandbox/.git, leaving other workspace files and causing clone failures
or stale files; update gitClone (or extend/removeGitDir) to remove the entire
target directory contents (dir) recursively before calling git.clone, then
recreate an empty directory at dir (ensure correct permissions) so git.clone
runs against an empty workspace; reference the gitClone function and
removeGitDir symbol when making this change.

In `@Build/vite.config.mjs`:
- Around line 52-54: src/sw.ts imports workbox-precaching, workbox-routing, and
workbox-strategies but Build/package.json doesn't list them as direct
dependencies; add "workbox-precaching", "workbox-routing", and
"workbox-strategies" to Build/package.json dependencies (matching compatible
versions used in package-lock.json) so the service worker file referenced by
filename: 'sw.ts' and the injectManifest strategy in vite.config.mjs won't break
if transitive packages change.

---

Outside diff comments:
In `@Build/src/console.ts`:
- Around line 57-86: The stack parsing extracts the source filename in capture
group m[1] but the click handler always reads the current tab's document; update
the code that builds the match (patterns loop) to store the parsed filename
(e.g., const fileName = m[1]) and inside the lineEl.addEventListener("click",
...) ensure you switch/open that file in the editor before reading
editor.view.state.doc — e.g., call editor.openFile(fileName) or the appropriate
method to make that file the active view, then compute
maxLine/safeLine/lineObj/offset and dispatch the selection as currently
implemented.

---

Duplicate comments:
In `@Build/src/appState.ts`:
- Around line 22-44: The module-level initializers (filesInitial,
activeFileInitial, darkModeInitial, autoRunInitial, splitSizesInitial,
logFiltersInitial) are trusting raw persistedState and can seed invalid UI
state; update the initializers and applyStateSnapshot() to fully validate and
sanitize persistedState before using it: validate that persistedState.files is
an array of valid FileTab objects (check required fields and types) and
otherwise fall back to defaultFiles, ensure activeFileInitial is a string that
matches a validated file id (otherwise pick defaultFiles[0].id), coerce
darkMode/autoRun only from explicit "true"/"false" strings or booleans, validate
splitSizes is an array of two finite numbers between 0–100 (fallback to
[50,50]), and sanitize logFilters to an array of allowed filter strings
(fallback to default list); apply the same validation logic in
applyStateSnapshot() (and in the code paths referenced around the other affected
block) so malformed snapshots never become live or get re-saved.

In `@Build/src/biome.ts`:
- Around line 32-61: initBiome currently races when called concurrently because
multiple calls can pass the boolean initialized check; introduce a single-flight
promise to serialize initialization: add a module-level variable like
initBiomePromise: Promise<void> | null and change initBiome to return/await that
promise when present, otherwise assign initBiomePromise = (async () => { ...
existing init logic ... })(); ensure the promise is cleared (set to null) after
success or failure and that initialized is set inside the serialized block;
reference initBiome, initialized, workspace, projectKey and initBiomeWasm so you
update those symbols within the new single-flight wrapper.

In `@Build/src/console.ts`:
- Around line 153-180: The renderObject function can call visited.add(asObj as
object) for non-object primitives (undefined, bigint, symbol, function) and
crash; before treating obj as an object/array (before the asObj cast,
visited.has and visited.add calls), add a guard that checks typeof obj !==
"object" && typeof obj !== "function" (or equivalently ensure obj is an object
and not null) and return document.createTextNode(String(obj)) for those
primitives; update the logic around renderObject, the asObj cast, and the
visited.add(asObj as object) usage so only real objects/arrays are added to the
WeakSet.

In `@Build/src/editor.ts`:
- Around line 198-215: The effect currently only calls
_languageCompartment.reconfigure when content changes, so renames (e.g.,
script.js -> styles.css) don't update the language; fix by tracking the previous
active file name (e.g., add a outer-scope variable prevActiveFileName) and
inside the effect compare file.name to prevActiveFileName and call
_languageCompartment.reconfigure(getLanguageExtension(file.name)) when the name
changed (even if content is identical), then set prevActiveFileName = file.name;
keep the existing content-sync/_suppressContentUpdate logic and ensure you still
call reconfigure when content changes as before.

In `@Build/src/main.ts`:
- Around line 784-802: In addFileAction, avoid appending a FileTab with an
id/name that already exists: before creating/setting newFile, read
filesState.get() and check for an existing entry where f.id === fileName ||
f.name === fileName; if a duplicate is found, abort (e.g., show an alert or
prompt) and do not call filesState.set(...) or change activeFileState; otherwise
proceed to push the new FileTab. Ensure you reference addFileAction, FileTab,
filesState, activeFileState and fileName when making this check so all
name-based lookups remain unambiguous.
- Around line 560-587: renderFileTabs renders tabs as non-focusable divs which
prevents keyboard users from switching files; update renderFileTabs so each tab
is an interactive element (preferably a <button> or a div with tabindex="0" and
role="tab") and add keyboard handlers for Enter/Space to activate the tab, set
aria-selected (true/false) and aria-controls or aria-label on the element,
ensure closeBtn keeps being a <button> (it already is) and has an accessible
label (e.g., aria-label="Close {file.name}"), and ensure addBtn remains
focusable with a clear aria-label; apply the same changes to the corresponding
file item and folder toggle renderer (the block referenced at 674-733) so folder
toggles are focusable, respond to Enter/Space, and expose proper ARIA roles
(e.g., button or treeitem/aria-expanded) for keyboard navigation.

In `@Build/src/runner.ts`:
- Around line 384-395: The inlineCssFiles and inlineJsFiles functions embed raw
user content inside <style> and <script> tags which breaks when the content
contains literal closing tags; sanitize user content before wrapping by escaping
any closing tag sequences (e.g. replace occurrences of </style> and </script>
with <\/style> and <\/script> respectively or similar safe escape) so the
wrapper tags are not terminated early; update inlineCssFiles and inlineJsFiles
to perform this replacement on f.content (preserving trimming) before embedding.

In `@Build/src/ui.ts`:
- Around line 18-20: The showError function currently sets a timeout to clear
errorMessage but doesn't cancel any prior timeout, so a previously scheduled
timer can clear a newly shown error early; fix this by adding a module-scoped
variable (e.g., errorClearTimeout) to hold the current timeout id, call
clearTimeout(errorClearTimeout) before creating a new setTimeout in showError,
assign the new timeout id to errorClearTimeout, and keep the existing behavior
of clearing errorMessage after 5000ms; reference the showError function and the
errorMessage state when making the change.

In `@Build/src/vfs.ts`:
- Around line 73-77: The catch block in the initialization path currently sets
_ready = true and calls _readyResolve(), which incorrectly signals success to
callers of ready(); instead, preserve a failure state and reject the ready
promise: add or use an existing _readyReject (create one if missing alongside
_readyResolve/_readyPromise), do NOT set _ready = true in the catch, and call
_readyReject(err) (or throw/reject the promise with the error) so ready()
consumers receive the failure; update any initialization helpers (the catch
block around VFS init and the ready() consumer behavior) to propagate that
rejection rather than resolving as success.
- Around line 110-126: Sanitize and validate the user-supplied file names in
createFileInVFS, deleteFileInVFS, and renameFileInVFS so they cannot escape the
`/sandbox` root: normalize the name (replace backslashes, remove any leading
slashes, and collapse `..` segments), then resolve/join it with the sandbox base
and assert the resulting path begins with `/sandbox/`; if it does not, throw an
error. Apply the same check for both source and destination in renameFileInVFS
and reuse/centralize the logic (e.g., a helper like ensureSandboxPath) before
calling ensureParentDir, fs.promises.writeFile, unlink, or rename.

---

Nitpick comments:
In `@Build/vite.config.mjs`:
- Line 78: The plugin call inlineSvgFaviconPlugin({ svg: "public/favicon.png" })
uses a misleading parameter name; update the plugin function signature for
inlineSvgFaviconPlugin to rename the parameter (e.g., from svg to image or
favicon) and update all call sites (including this call in vite.config.mjs) to
pass the file under the new key (e.g., { image: "public/favicon.png" } or {
favicon: "public/favicon.png" }) so the name reflects that non-SVG files are
supported and avoids confusion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1ca978f2-0c16-44a1-bdbe-e30f4e2d58ea

📥 Commits

Reviewing files that changed from the base of the PR and between 329d0ac and 598c48a.

⛔ Files ignored due to path filters (1)
  • Build/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • Build/dev-dist/registerSW.js
  • Build/eslint.config.mts
  • Build/index.html
  • Build/package.json
  • Build/src/appState.ts
  • Build/src/biome.ts
  • Build/src/console.ts
  • Build/src/defaultContent.ts
  • Build/src/editor.ts
  • Build/src/git.worker.ts
  • Build/src/global.d.ts
  • Build/src/main.ts
  • Build/src/runner.ts
  • Build/src/sw.ts
  • Build/src/terminal.ts
  • Build/src/types.ts
  • Build/src/ui.ts
  • Build/src/utils.ts
  • Build/src/vfs.ts
  • Build/styles/styles.css
  • Build/tsconfig.json
  • Build/vite.config.mjs
💤 Files with no reviewable changes (1)
  • Build/eslint.config.mts
✅ Files skipped from review due to trivial changes (2)
  • Build/dev-dist/registerSW.js
  • Build/styles/styles.css
🚧 Files skipped from review as they are similar to previous changes (5)
  • Build/tsconfig.json
  • Build/src/global.d.ts
  • Build/index.html
  • Build/package.json
  • Build/src/utils.ts

Comment thread Build/src/appState.ts
Comment on lines +236 to +239
let nextUntitledId = 1;
export function getNextUntitledId(): string {
return `untitled-${nextUntitledId++}.html`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Derive untitled ids from the current file set.

nextUntitledId resets to 1 on every reload, so a persisted untitled-1.html makes the next new file reuse the same id. Since tab selection and file updates key off id, duplicate ids can route edits or close actions to the wrong tab.

💡 Minimal fix
 let nextUntitledId = 1;
 export function getNextUntitledId(): string {
-  return `untitled-${nextUntitledId++}.html`;
+  const used = new Set(filesState.get().map((file) => file.id));
+  while (used.has(`untitled-${nextUntitledId}.html`)) {
+    nextUntitledId += 1;
+  }
+  return `untitled-${nextUntitledId++}.html`;
 }
📝 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
let nextUntitledId = 1;
export function getNextUntitledId(): string {
return `untitled-${nextUntitledId++}.html`;
}
let nextUntitledId = 1;
export function getNextUntitledId(): string {
const used = new Set(filesState.get().map((file) => file.id));
while (used.has(`untitled-${nextUntitledId}.html`)) {
nextUntitledId += 1;
}
return `untitled-${nextUntitledId++}.html`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/appState.ts` around lines 236 - 239, nextUntitledId is volatile and
causes duplicate ids after reload; change getNextUntitledId() so it derives the
next untitled number from the current file set instead of the module-scoped
nextUntitledId counter. In getNextUntitledId(), inspect the existing files
collection (the same store/array used elsewhere for file state), find the
highest numeric suffix among ids matching /^untitled-(\d+)\.html$/, and return
`untitled-${max+1}.html` (fallback to 1 if none); remove or stop using the
module-level nextUntitledId to avoid reset-on-reload collisions.

Comment thread Build/src/main.ts
Comment on lines +814 to +837
function closeFileAction(id: string): void {
const files = filesState.get();
if (files.length <= 1) return;

const idx = files.findIndex((f) => f.id === id);
const updated = files.filter((f) => f.id !== id);
filesState.set(updated);

const wasActive = activeFileState.get() === id;
if (wasActive && updated.length > 0) {
const nextIdx = Math.min(idx, updated.length - 1);
const newActive = updated[nextIdx].id;
activeFileState.set(newActive);
const file = updated[nextIdx];
if (file) switchToFile(file);
}

const file = files.find((f) => f.id === id);
if (file && isReady()) {
deleteFileInVFS(file.name).catch((err) =>
console.warn("VFS delete failed:", err),
);
if (messageElement) {
const logType = Array.from(messageElement.classList)
.find((cls) => cls.startsWith("console-"))
?.replace("console-", "");
if (logType && !activeFilters.has(logType)) {
entry.className = "console-entry filtered";
} else {
entry.className = "console-entry";
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

“Close” is currently a destructive delete.

These handlers remove files from filesState and then call deleteFileInVFS() for the same entries. In this PR filesState is the project source of truth, so closing one tab, other tabs, or all tabs permanently deletes workspace files instead of just hiding tabs.

Also applies to: 1028-1055

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

In `@Build/src/main.ts` around lines 814 - 837, closeFileAction is currently
deleting workspace files by calling deleteFileInVFS after removing an entry from
filesState; instead remove only the UI/tab state and do not call
deleteFileInVFS. Update closeFileAction (and the analogous block around the
1028-1055 range) to only update filesState and activeFileState and call
switchToFile as needed, and remove or gate the deleteFileInVFS(...) call so
permanent deletion is performed only by a dedicated delete action (e.g.,
deleteFileAction) or when an explicit "delete" flag is passed.

Comment thread Build/src/main.ts
Comment on lines +1115 to +1128
function deleteFolderAction(folderPath: string): void {
if (!confirm(`Delete folder "${folderPath}" and all files inside?`)) return;
const files = filesState.get();
const updated = files.filter((f) => {
const dir = getDirPath(f.name);
return dir !== folderPath && !dir.startsWith(folderPath + "/");
});
if (updated.length === files.length) return;
filesState.set(updated);
const activeId = activeFileState.get();
if (!updated.find((f) => f.id === activeId) && updated.length > 0) {
activeFileState.set(updated[0].id);
switchToFile(updated[0]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Folder deletion never touches the VFS.

This only filters filesState. Because syncFilesToVFS() is additive and there is no recursive VFS delete here, the folder contents remain under /sandbox and reappear after reload or later git/VFS refreshes.

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

In `@Build/src/main.ts` around lines 1115 - 1128, deleteFolderAction currently
only updates filesState and never removes the corresponding files in the VFS, so
deleted folder contents reappear; after computing the new files list (and the
deletedFiles as the difference between previous filesState.get() and updated),
remove those paths from the VFS (e.g. call the VFS API to unlink/rm each deleted
file or recursively remove the folder under /sandbox) or alternatively clear the
sandbox VFS and re-run syncFilesToVFS(filesState.get()) to fully reconcile
state; update deleteFolderAction to perform that VFS removal step (use the same
folderPath and file.name values to locate VFS entries) before finishing and keep
the existing activeFileState/switchToFile logic.

Comment thread Build/src/main.ts
Comment on lines +1288 to +1306
const worker = new GitWorker();
await initializeVFS(worker);

const editorContainer = document.getElementById("editor-container");
if (!editorContainer) {
console.error("Editor container not found");
return;
}

const initialFiles = filesState.get();
const initialActiveId = activeFileState.get();
const initialFile = initialFiles.find((f) => f.id === initialActiveId) ?? initialFiles[0];
if (initialFile) {
createEditor(editorContainer, initialFile);
}

initializeCopyButtons();

loadState();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Startup hydrates two different sources of truth in the wrong order.

initializeVFS() already loaded /sandbox into filesState, createEditor() opens that file, and then loadState() overwrites the state afterward. The editor/tree can start from different snapshots, and the later debounced sync can write stale localStorage data back over the mounted workspace.

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

In `@Build/src/main.ts` around lines 1288 - 1306, Startup currently hydrates
filesState from the VFS and then overwrites it with loadState(), causing
race/stale writes; move the call to loadState() to run before
initializeVFS(worker) so persisted localStorage state is applied first, then
call initializeVFS(worker) which should merge (not replace) with any existing
filesState, and only then compute initialFiles/initialActiveId and call
createEditor(editorContainer, initialFile); if initializeVFS currently blindly
replaces filesState, update its implementation to merge incoming VFS contents
with existing filesState instead of overwriting.

Comment thread Build/src/runner.ts
Comment on lines +34 to +43
.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(_m, text: string, url: string) =>
`<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer">${text}</a>`,
)
.replace(
/!\[([^\]]*)\]\(([^)]+)\)/g,
(_m, alt: string, url: string) =>
`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy">`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Render images before links in inlineMarkdown().

![alt](url) matches the link regex first, so it becomes !<a ...> and never reaches the image replacement. Every Markdown image preview is broken with the current replacement order.

🔧 Minimal fix
   return escaped
     .replace(/`([^`]+)`/g, "<code>$1</code>")
     .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
     .replace(/\*([^*]+)\*/g, "<em>$1</em>")
     .replace(/~~([^~]+)~~/g, "<del>$1</del>")
+    .replace(
+      /!\[([^\]]*)\]\(([^)]+)\)/g,
+      (_m, alt: string, url: string) =>
+        `<img src="${safeUrl(url)}" alt="${alt}" loading="lazy">`,
+    )
     .replace(
       /\[([^\]]+)\]\(([^)]+)\)/g,
       (_m, text: string, url: string) =>
         `<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer">${text}</a>`,
-    )
-    .replace(
-      /!\[([^\]]*)\]\(([^)]+)\)/g,
-      (_m, alt: string, url: string) =>
-        `<img src="${safeUrl(url)}" alt="${alt}" loading="lazy">`,
     );
📝 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
.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(_m, text: string, url: string) =>
`<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer">${text}</a>`,
)
.replace(
/!\[([^\]]*)\]\(([^)]+)\)/g,
(_m, alt: string, url: string) =>
`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy">`,
);
.replace(
/!\[([^\]]*)\]\(([^)]+)\)/g,
(_m, alt: string, url: string) =>
`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy">`,
)
.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(_m, text: string, url: string) =>
`<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer">${text}</a>`,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Build/src/runner.ts` around lines 34 - 43, The inlineMarkdown function
currently applies the link replacement before the image replacement so markdown
images match the link regex and break; fix this by swapping the two .replace
calls in inlineMarkdown so the image regex (/!\[([^\]]*)\]\(([^)]+)\)/g) runs
before the link regex (/\[([^\]]+)\]\(([^)]+)\)/g), keeping the same callbacks
that use safeUrl(url) and preserve attributes (alt, loading, target, rel) to
ensure images render correctly.

Comment thread Build/src/runner.ts
Comment on lines +247 to +260
for (const file of files) {
const url = SANDBOX_BASE + file.name;
const resp = new Response(file.content, {
headers: { "Content-Type": mimeType(file.name) },
});
cache.put(url, resp);
}

const html = generatePreviewHtml(files);
const htmlUrl = SANDBOX_BASE + "index.html";
const htmlResp = new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
cache.put(htmlUrl, htmlResp);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await sandbox cache writes before returning the preview URL.

cache.put() is async here, but populateSandboxCache() returns index.html immediately and runCode() navigates the iframe right away. On a cold cache the iframe can beat those writes and load a partially populated sandbox.

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

In `@Build/src/runner.ts` around lines 247 - 260, populateSandboxCache is
returning the preview URL before cache.put() operations finish, so the iframe
can navigate before the sandbox is fully written; change the write loop and the
index.html write to await the cache.put() promises (e.g., collect promises from
cache.put for each file and the generated index.html and await Promise.all on
them) so that generatePreviewHtml/files loop and the htmlResp put (referencing
the files iteration, cache.put, generatePreviewHtml, and htmlResp) complete
before returning the preview URL to runCode().

Comment thread Build/src/runner.ts
Comment on lines +264 to +276
function generatePreviewHtml(files: FileTab[]): string {
const htmlFile = files.find((f) => /\.html?$/i.test(f.name));
const cssFiles = files.filter((f) => /\.css$/i.test(f.name));
const jsFiles = files.filter((f) => /\.m?js$/i.test(f.name));

const rawHtml = htmlFile?.content ?? "";
const hasDocTag = /<html[\s>/]|<!doctype\s+html/i.test(rawHtml);

if (hasDocTag) {
return injectIntoFullHtml(rawHtml, cssFiles, jsFiles);
}

return buildStandaloneHtml(rawHtml, cssFiles, jsFiles);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the active HTML file as the preview entrypoint.

This always picks the first .html? file. In Build/src/main.ts, in-preview navigation updates activeFileState and reruns the preview, but generatePreviewHtml() still snaps back to the first document, so multipage projects never stay on the page the user navigated to.

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

In `@Build/src/runner.ts` around lines 264 - 276, generatePreviewHtml currently
always picks the first HTML file, causing the preview to ignore the user's
current active file; update it to use the active preview entrypoint instead —
either accept an activeFile identifier (e.g., activeFileState.name) or detect an
active flag on FileTab and prefer that file when choosing htmlFile (fall back to
first match if no active file found). Modify the call site in Build/src/main.ts
(where activeFileState is updated and preview is rerun) to pass the active file
name or ensure FileTab.active is set so generatePreviewHtml uses that active
file as the entrypoint (keep existing hasDocTag logic and
injectIntoFullHtml/buildStandaloneHtml flow unchanged).

Comment thread Build/src/sw.ts
Comment on lines +18 to +29
const SANDBOX_CACHE = "sandbox-v1";
const SANDBOX_PREFIX = "/sandbox/";

self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});

self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith(SANDBOX_PREFIX)) {
event.respondWith(handleSandbox(event.request, url));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Derive the sandbox prefix from the service-worker scope, not "/sandbox/".

Build/src/runner.ts builds sandbox URLs relative to the current page, but this worker only intercepts pathnames starting with /sandbox/. On any subpath deployment, /.../sandbox/... requests bypass handleSandbox() and lose the extensionless/index fallback logic.

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

In `@Build/src/sw.ts` around lines 18 - 29, The worker currently hardcodes
SANDBOX_PREFIX = "/sandbox/" so requests under a non-root service-worker scope
(e.g. /base/sandbox/) bypass handleSandbox; change prefix derivation to compute
it from the service worker scope (use self.registration.scope), e.g. compute
const scopePath = new URL(self.registration.scope).pathname and set
SANDBOX_PREFIX = scopePath.replace(/\/?$/, "/") + "sandbox/"; then update the
fetch listener to use that computed SANDBOX_PREFIX when checking
url.pathname.startsWith(...), keeping handleSandbox(event.request, url)
unchanged.

Comment thread Build/src/vfs.ts
Comment on lines +434 to +446
export async function gitClone(url: string, dir = "/sandbox"): Promise<void> {
await removeGitDir(dir);
await git.clone({
fs,
http,
dir,
url,
singleBranch: true,
depth: 1,
corsProxy: "https://cors.isomorphic-git.org",
onAuth,
onAuthFailure,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

gitClone() leaves the previous workspace on disk.

Only /sandbox/.git is removed here. Any other files from the current workspace survive the clone attempt, so a clone can either fail on a non-empty directory or leave stale local files mixed into the fetched repository.

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

In `@Build/src/vfs.ts` around lines 434 - 446, gitClone currently only calls
removeGitDir which only deletes /sandbox/.git, leaving other workspace files and
causing clone failures or stale files; update gitClone (or extend/removeGitDir)
to remove the entire target directory contents (dir) recursively before calling
git.clone, then recreate an empty directory at dir (ensure correct permissions)
so git.clone runs against an empty workspace; reference the gitClone function
and removeGitDir symbol when making this change.

Comment thread Build/vite.config.mjs
Comment on lines +52 to +54
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check package.json for required Workbox dependencies

# Look for workbox packages in package.json
rg -A 5 '"(workbox-precaching|workbox-routing|workbox-strategies)"' Build/package.json

Repository: HTMLToolkit/HTMLRunner

Length of output: 48


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all package.json files
fd package.json -t f . > /tmp/packagejson_paths.txt

echo "Package.json files found:"
cat /tmp/packagejson_paths.txt

echo
echo "Searching for required Workbox packages across all package.json files..."
rg -n --no-heading --with-filename '"workbox-(precaching|routing|strategies)"' $(cat /tmp/packagejson_paths.txt) || true

echo
echo "Searching for workbox packages by substring across all package.json files..."
rg -n --no-heading --with-filename 'workbox-(precaching|routing|strategies)' $(cat /tmp/packagejson_paths.txt) || true

Repository: HTMLToolkit/HTMLRunner

Length of output: 265


🌐 Web query:

Workbox injectManifest strategy expects what imports in service worker template and whether specific packages are required

💡 Result:

The Workbox injectManifest strategy is designed for developers who want to maintain full control over their service worker code while still leveraging Workbox's automated precaching capabilities [1][2]. Regarding the requirements for your service worker template: 1. Mandatory Injection Point: Your source service worker template must include the string self.WB_MANIFEST [2][3]. During the build process, the Workbox build tool (e.g., workbox-build or workbox-webpack-plugin) replaces this specific placeholder with the generated precache manifest (an array of URL and revision objects) [2][4][5]. If this variable is missing, the build process will throw an error [2][3]. (You can customize this variable name using the injectionPoint configuration option if needed) [2][3]. 2. Required Imports and Packages: - You are responsible for importing any Workbox modules you wish to use [2][3]. Unlike the generateSW strategy, injectManifest does not automatically bundle the Workbox runtime [2]. - To actually utilize the injected manifest, you typically need to import the precacheAndRoute method from the workbox-precaching package and pass the manifest to it [2][3][5]. - A minimal service worker template using precaching would look like this [3][6]: import {precacheAndRoute} from 'workbox-precaching'; precacheAndRoute(self.WB_MANIFEST); 3. Other Packages: You are not strictly required to use specific Workbox packages beyond what is necessary to handle the manifest (like workbox-precaching) [2][3]. Since you are writing your own service worker, you have the flexibility to import any other Workbox modules (e.g., workbox-routing, workbox-strategies) or use standard Web APIs as required by your application's logic [1][4]. In summary, while there is no rigid requirement for specific Workbox packages other than those needed to implement your own caching/routing logic, the self.WB_MANIFEST placeholder is the only mandatory element the build process explicitly expects to find in your template [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for any 'workbox-' packages in Build/package.json:"
rg -n --no-heading 'workbox-' Build/package.json || true

echo
echo "Searching for 'workbox' in Build/package.json:"
rg -n --no-heading 'workbox' Build/package.json || true

echo
echo "Relevant excerpt around dependencies/devDependencies keys (first 200 lines containing workbox if any):"
# Print small surrounding context if workbox is present; otherwise print dependency blocks
if rg -n 'workbox-' Build/package.json >/dev/null 2>&1; then
  rg -n --context 3 'workbox-' Build/package.json
else
  # Show dependency section headings to understand what's present
  rg -n --no-heading '"dependencies"| "devDependencies"' Build/package.json || true
  head -n 120 Build/package.json
fi

Repository: HTMLToolkit/HTMLRunner

Length of output: 2816


🌐 Web query:

Workbox injectManifest does it require workbox-precaching specifically, and what happens if package is missing

💡 Result:

The Workbox injectManifest tool does not strictly require the workbox-precaching package to function, but it is effectively required if you intend to use the injected manifest for its intended purpose: precaching assets [1][2]. Here is the breakdown of how they relate and what happens if pieces are missing: 1. Relationship: injectManifest is responsible for generating a list of URLs (the precache manifest) and injecting it into your service worker file by replacing the placeholder string self.WB_MANIFEST [3][1]. The workbox-precaching package provides the logic (such as precacheAndRoute) to consume that manifest and actually perform the caching within the service worker [1][2]. 2. If workbox-precaching is missing: If you use injectManifest but do not include the workbox-precaching library in your service worker code, the manifest will still be successfully injected into your swDest file (replacing self.WB_MANIFEST with the array of assets) [1][2]. However, the manifest will be a "dead" variable in your code. Without the precacheAndRoute method or similar logic from the workbox-precaching package, your service worker will not know how to process or cache those files, rendering the injection useless [2]. 3. If self.WB_MANIFEST is missing: If your source service worker (swSrc) does not contain the self.WB_MANIFEST placeholder, the injectManifest plugin will throw an error during the build process [1][4]. It specifically requires this string to know where to insert the generated manifest [3][1]. In summary, injectManifest provides the data, and workbox-precaching provides the engine to use that data [1]. If you do not want to use precaching, you do not need to use injectManifest at all; you can simply write your service worker without it [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Build/vite.config.mjs (injectManifest config area) =="
# Show around the referenced lines (roughly 40-90)
sed -n '35,90p' Build/vite.config.mjs | nl -ba

echo
echo "== Locate sw.ts template referenced by config =="
# Search for sw.ts in Build
fd -t f 'sw.ts' Build || true
fd -t f 'sw.ts' . || true

echo
echo "== Print relevant contents of found sw.ts files (up to 200 lines each) =="
# Print first 200 lines for each located sw.ts (if more than one, keep output small)
for f in $(fd -t f 'sw.ts' .); do
  echo "--- $f ---"
  sed -n '1,200p' "$f" | nl -ba
done

Repository: HTMLToolkit/HTMLRunner

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Build/vite.config.mjs lines ~40-90 =="
sed -n '40,90p' Build/vite.config.mjs

echo
echo "== sw.ts template candidates (fd) =="
fd -t f 'sw.ts' . || true
fd -t f 'sw.ts' Build || true

echo
echo "== Print sw.ts contents (up to 220 lines) =="
for f in $(fd -t f 'sw.ts' .); do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

Repository: HTMLToolkit/HTMLRunner

Length of output: 3400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Lockfiles present =="
fd -t f 'package-lock.json' . || true
fd -t f 'yarn.lock' . || true
fd -t f 'pnpm-lock.yaml' . || true

echo
echo "== Search for workbox packages in lockfiles (if any) =="
for f in $(fd -t f 'package-lock.json|yarn.lock|pnpm-lock.yaml' .); do
  echo "--- $f ---"
  rg -n --no-heading 'workbox-(precaching|routing|strategies)' "$f" || true
done

Repository: HTMLToolkit/HTMLRunner

Length of output: 1127


Declare the Workbox packages imported by src/sw.ts as direct dependencies

Build/src/sw.ts imports workbox-precaching, workbox-routing, and workbox-strategies, but Build/package.json does not list any of them. They do appear in Build/package-lock.json (installed transitively), so the current build may work—still, add these packages to Build/package.json to avoid breaking installs when transitive dependencies change.

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

In `@Build/vite.config.mjs` around lines 52 - 54, src/sw.ts imports
workbox-precaching, workbox-routing, and workbox-strategies but
Build/package.json doesn't list them as direct dependencies; add
"workbox-precaching", "workbox-routing", and "workbox-strategies" to
Build/package.json dependencies (matching compatible versions used in
package-lock.json) so the service worker file referenced by filename: 'sw.ts'
and the injectManifest strategy in vite.config.mjs won't break if transitive
packages change.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants