@illuma-ai/code-sandbox
v1.34.0
Published
Browser-native code sandbox with file tree, editor, terminal, and live preview powered by Nodepod
Readme
@illuma-ai/code-sandbox
A browser-native code sandbox with file tree, Monaco editor, terminal, and live preview — powered by Nodepod.
Run Express + React applications entirely in the browser. No server-side containers required.
Features
- Full IDE workbench: file tree, code editor, terminal, live preview
- Runs Node.js in the browser via Nodepod (SharedArrayBuffer + Service Worker)
- Monaco editor with syntax highlighting, diff viewer, and file tabs
- Structured error reporting with file path, line number, and source context
- Hot reload for CSS/HTML/static asset changes (no server restart)
- Imperative API for programmatic control (designed for AI agent integration)
- Built-in project templates (Express + React full-stack apps)
- Customizable via CSS custom properties
- Dark theme by default, with shadcn-compatible theme variable bridge
Installation
npm install @illuma-ai/code-sandboxPeer Dependencies
These must be installed by the consuming application:
npm install react react-dom @monaco-editor/react monaco-editor allotment framer-motion| Peer Dependency | Version |
| ---------------------- | ----------------------------------- |
| react | ^18.0.0 \|\| ^19.0.0 |
| react-dom | ^18.0.0 \|\| ^19.0.0 |
| @monaco-editor/react | ^4.6.0 |
| monaco-editor | >=0.40.0 |
| allotment | ^1.20.0 |
| framer-motion | ^10.0.0 \|\| ^11.0.0 \|\| ^12.0.0 |
Prerequisites
COOP/COEP Headers
Nodepod requires SharedArrayBuffer, which is only available in cross-origin isolated contexts. Your dev server must send these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentiallessVite example:
// vite.config.ts
export default defineConfig({
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "credentialless",
},
},
});Service Worker
Copy the Nodepod service worker file to your public/ directory so it's served at /__sw__.js:
cp node_modules/@illuma-ai/nodepod/dist/sw.js public/__sw__.jsQuick Start
import { useRef } from "react";
import { CodeSandbox } from "@illuma-ai/code-sandbox";
import type { CodeSandboxHandle } from "@illuma-ai/code-sandbox";
import "@illuma-ai/code-sandbox/styles.css";
function App() {
const ref = useRef<CodeSandboxHandle>(null);
return (
<CodeSandbox
ref={ref}
template="fullstack-starter"
height="100vh"
onServerReady={(port, url) => console.log(`Ready on port ${port}`)}
onSandboxError={(err) => console.error(err.category, err.message)}
/>
);
}With Custom Files
const files = {
"package.json": JSON.stringify(
{
name: "my-app",
dependencies: { express: "^4.18.0" },
},
null,
2,
),
"server.js": `
const express = require("express");
const app = express();
app.get("/", (req, res) => res.send("<h1>Hello World</h1>"));
app.listen(3000, () => console.log("Running on port 3000"));
`,
};
<CodeSandbox
ref={ref}
files={files}
entryCommand="node server.js"
port={3000}
height="100vh"
/>;AI Agent Integration
The sandbox is designed as a "dumb renderer" — the host application pushes files in and reads state out. The user does not edit code in the sandbox; an AI agent writes code, and the host pushes it via the imperative API.
const ref = useRef<CodeSandboxHandle>(null);
// Agent starts editing — enable diff view + lock editor
ref.current.setDiffMode(true);
// Agent generates new files → push them into the sandbox
await ref.current.updateFiles(agentGeneratedFiles);
// Agent modifies a single file (surgical — no server restart)
await ref.current.updateFile("server.js", newServerCode);
// Agent deletes a file
await ref.current.deleteFile("old-route.js");
// Read errors for the agent to self-correct
const errors = ref.current.getErrors();
// errors[0] → { category: "process-stderr", message: "...", filePath: "server.js", line: 42, sourceContext: "..." }
// Agent done — disable diff view + unlock editor
ref.current.setDiffMode(false);API Reference
<CodeSandbox> Component
The main component. Renders the full workbench (file tree, editor, terminal, preview). Uses React.forwardRef to expose the imperative handle.
<CodeSandbox
ref={sandboxRef}
files={fileMap} // OR template="fullstack-starter"
entryCommand="node server.js"
port={3000}
env={{ NODE_ENV: "development" }}
height="100vh"
className="my-sandbox"
onFileChange={(path, content) => {}}
onServerReady={(port, url) => {}}
onProgress={(progress) => {}}
onError={(message) => {}}
onSandboxError={(error) => {}}
onFilesUpdated={(changes) => {}}
/>Props (CodeSandboxProps)
| Prop | Type | Default | Description |
| -------------- | ------------------------ | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| files | FileMap | — | Initial file set. Pass a flat Record<string, string> mapping file paths to contents. |
| template | string | — | Use a built-in template instead of files. One of: "express-react", "fullstack-starter". |
| entryCommand | string | Inferred from package.json or "node server.js" | Shell command to start the dev server. |
| port | number | 3000 | Port the server listens on. |
| env | Record<string, string> | — | Environment variables passed to Node.js processes. |
| height | string | "100vh" | CSS height of the sandbox root element. |
| className | string | — | CSS class name for the root element. |
| diffMode | boolean | — | Externally controlled diff mode. When true, the editor shows Monaco's side-by-side DiffEditor for files that have changes. When undefined, the user controls diff via the built-in Diff button. |
| readOnly | boolean | false | Make the editor read-only. When true, only the agent can modify files via the imperative handle. Typical pattern: set both readOnly and diffMode to true while the agent is editing, then flip both off when done. |
Callback Props
| Callback | Signature | Description |
| ---------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| onFileChange | (path: string, content: string) => void | Fires when a file is modified in the editor. |
| onServerReady | (port: number, url: string) => void | Fires when the dev server is ready (initial boot or after restart). |
| onProgress | (progress: BootProgress) => void | Fires on boot progress changes. |
| onError | (message: string) => void | Fires on errors (legacy string format). |
| onSandboxError | (error: SandboxError) => void | Fires when a structured error is detected. Primary error channel for AI agent integration. |
| onFilesUpdated | (changes: Record<string, FileChangeStatus>) => void | Fires after updateFiles() completes (files written + server restarted). |
CodeSandboxHandle (Imperative API)
Access via a React ref. This is the primary API for host applications to control the sandbox programmatically.
const ref = useRef<CodeSandboxHandle>(null);
<CodeSandbox ref={ref} ... />Methods
updateFiles(files, options?)
Replace the entire file set. Diffs against current files, writes only changed files to the virtual FS, and restarts the server if anything changed.
By default, the previous file set becomes originalFiles for diff tracking. Use keepBaseline to preserve the original baseline during incremental updates (e.g., streaming AI agent edits).
// Standard update — resets diff baseline
await ref.current.updateFiles(newFileMap);
// Skip server restart
await ref.current.updateFiles(newFileMap, { restartServer: false });
// Incremental update — preserves diff baseline for accumulated diffs
await ref.current.updateFiles(newFileMap, { keepBaseline: true, restartServer: false });| Param | Type | Default | Description |
| ----------------------- | --------- | ------- | ------------------------------------------------------------- |
| files | FileMap | — | Complete new file set. |
| options.restartServer | boolean | true | Whether to restart the server after writing. |
| options.keepBaseline | boolean | false | When true, preserves originalFiles so diffs accumulate against the initial state rather than resetting on each update. Ideal for streaming AI edits where you want to show total changes. |
Hot reload vs cold restart: If ALL changed files are hot-reloadable (CSS, HTML, images, fonts), the sandbox refreshes the preview iframe without restarting the Node.js server. If ANY changed file is JS/TS/JSON, the server process is killed and re-run.
AI agent editing pattern:
// 1. Agent starts editing — switch to code view, enable diff mode
ref.current.setView("code");
// 2. Agent edits files one at a time — push each surgically
// updateFile() does NOT restart the server, diffs accumulate
await ref.current.updateFile("routes/todos.js", newContent);
ref.current.openFile("routes/todos.js", 42); // scroll to changed line
// 2b. Agent deletes a file
await ref.current.deleteFile("routes/deprecated.js");
// 3. Agent finishes — reconciliation with restart
await ref.current.updateFiles(finalFiles); // resets baseline, restarts server
ref.current.setView("preview"); // switch back to see the running appupdateFile(path, content)
Update a single file. Writes to the virtual FS and updates state. Does not restart the server — call restart() manually if needed, or use updateFiles() for bulk updates with auto-restart.
await ref.current.updateFile("public/styles.css", newCss);deleteFile(path)
Delete a file from the sandbox. Removes it from the virtual FS and marks it as "deleted" in fileChanges for diff tracking. If the file was open in the editor, it is automatically closed.
await ref.current.deleteFile("routes/old-api.js");
// fileChanges → { "routes/old-api.js": "deleted" }Files that existed in originalFiles are tracked as "deleted" (shown with red indicator in file tree). Files that were added after boot and then deleted are simply removed from tracking.
restart()
Force restart the server process. Kills the current process and re-runs the entry command.
await ref.current.restart();getFiles()
Returns the current FileMap (all files including edits).
const files: FileMap = ref.current.getFiles();getChangedFiles()
Returns a FileMap containing only files that have been modified relative to originalFiles.
const changed: FileMap = ref.current.getChangedFiles();getFileChanges()
Returns the per-file change status map. Missing keys should be treated as "unchanged".
const changes: Record<string, FileChangeStatus> = ref.current.getFileChanges();
// { "server.js": "modified", "public/new-page.html": "new" }getErrors()
Returns all structured errors collected during this session. The array grows monotonically — new errors are appended.
const errors: SandboxError[] = ref.current.getErrors();getState()
Returns the current RuntimeState snapshot.
const state: RuntimeState = ref.current.getState();
// state.status, state.previewUrl, state.terminalOutput, etc.setDiffMode(enabled)
Enable or disable diff mode in the editor. When enabled, files with changes show Monaco's side-by-side DiffEditor comparing originalFiles (left) with current files (right).
Use this to show diffs in real-time while the AI agent is editing, then disable when the agent finishes.
// Agent starts editing
ref.current.setDiffMode(true);
// Push agent edits — user sees diffs live
await ref.current.updateFiles(agentEditedFiles);
// Agent done — back to normal editor
ref.current.setDiffMode(false);Tip: Combine with the
readOnlyprop to lock the editor during agent edits:<CodeSandbox ref={ref} readOnly={agentEditing} diffMode={agentEditing} ... />
setView(view)
Programmatically switch between "code", "preview", and "swagger" views.
ref.current.setView("code"); // Show editor + file tree
ref.current.setView("preview"); // Show live preview iframe
ref.current.setView("swagger"); // Show auto-generated API documentationBoth views are always mounted (no destroy/recreate). Switching uses opacity + pointer-events for instant toggling without losing state.
openFile(path)
Open a specific file in the editor and switch to code view. Useful for highlighting agent-edited files.
ref.current.openFile("routes/todos.js");
// → File opens in editor, code view activatedgetConsoleLogs()
Returns all console log entries captured from the preview iframe (console.log, info, warn, error).
const logs: ConsoleEntry[] = ref.current.getConsoleLogs();clearConsoleLogs()
Clear all captured console log entries.
ref.current.clearConsoleLogs();SandboxError
Structured error type designed for AI agent consumption. Includes enough context for the agent to construct a targeted fix prompt.
interface SandboxError {
id: string; // Unique ID for deduplication
category: SandboxErrorCategory; // Error type
message: string; // Human-readable error message
stack?: string; // Full stack trace
filePath?: string; // File path (relative to workdir)
line?: number; // Line number (1-indexed)
column?: number; // Column number (1-indexed)
timestamp: string; // ISO 8601 timestamp
sourceContext?: string; // ~10 surrounding source lines
}Error Categories (SandboxErrorCategory)
| Category | Description |
| ----------------------------- | -------------------------------------------------------------- |
| process-stderr | Output written to stderr by the Node.js process |
| process-exit | Process exited with a non-zero code |
| runtime-exception | Uncaught exception in the Node.js runtime |
| browser-error | JavaScript error in the preview iframe (window.onerror) |
| browser-unhandled-rejection | Unhandled promise rejection in the iframe |
| browser-console-error | console.error() calls in the iframe |
| compilation | Syntax/import errors at module load time |
| network | Failed HTTP requests from the preview (fetch/XHR) |
| boot | Error during Nodepod boot, file writing, or dependency install |
Source Context Format
When available, sourceContext contains lines formatted as "lineNum: content":
38: const express = require("express");
39: const app = express();
40: const PORT = 3000;
41:
42: app.get("/api/data", (req, res) => {
43: res.json(undefinedVariable); // ← error here
44: });
45: app.listen(PORT);RuntimeState
The full reactive state exposed by the runtime.
interface RuntimeState {
status: BootStage;
progress: BootProgress;
previewUrl: string | null;
terminalOutput: string[];
files: FileMap;
originalFiles: FileMap;
fileChanges: Record<string, FileChangeStatus>;
previewReloadKey: number;
error: string | null;
errors: SandboxError[];
}| Field | Type | Description |
| ------------------ | ---------------------------------- | --------------------------------------------------------------------------- |
| status | BootStage | Current lifecycle stage. |
| progress | BootProgress | Current boot progress (stage, message, percent). |
| previewUrl | string \| null | URL for the preview iframe. null until server is ready. |
| terminalOutput | string[] | All terminal output lines (stdout + stderr). |
| files | FileMap | Current files in the virtual FS (including edits). |
| originalFiles | FileMap | Baseline files before the latest updateFiles(). Used for diffs. |
| fileChanges | Record<string, FileChangeStatus> | Per-file change status. Missing keys = "unchanged". |
| previewReloadKey | number | Incremented on hot reload — triggers iframe refresh without server restart. |
| error | string \| null | Error message if status is "error". |
| errors | SandboxError[] | All structured errors (grows monotonically). |
BootStage
type BootStage =
| "initializing" // Nodepod.boot() in progress
| "writing-files" // Writing project files to virtual FS
| "installing" // npm install running
| "starting" // Entry command executing
| "ready" // Server is listening, preview iframe can load
| "error"; // Something failedBootProgress
interface BootProgress {
stage: BootStage;
message: string;
percent: number; // 0-100, approximate
}FileChangeStatus
type FileChangeStatus = "new" | "modified" | "deleted" | "unchanged";Hot Reload
When updateFiles() is called, the sandbox inspects which files changed:
- Hot-reloadable extensions:
.css,.html,.htm,.svg,.png,.jpg,.jpeg,.gif,.webp,.ico,.woff,.woff2,.ttf,.eot - If all changed files have hot-reloadable extensions → the preview iframe is soft-reloaded (
location.reload()) without restarting the Node.js server process. - If any changed file is JS/TS/JSON/etc. → the server process is killed and re-run (cold restart).
This makes CSS-only iterations instant.
Built-in Templates
Use template="name" to bootstrap with a pre-built project.
express-react
A lightweight Express + React app using JSONPlaceholder as a data source. React is loaded from CDN (no build step). Demonstrates posts/users CRUD views.
- Entry command:
node server.js - Port: 3000
- Files: 9 (server.js, public/index.html, styles, 5 React components)
fullstack-starter
A comprehensive full-stack template with authentication, CRUD, charts, and theming.
- Entry command:
node server.js - Port: 3000
- Files: 35 (Express + sql.js + React + React Router + Auth + CRUD + Chart.js + shadcn theme variables)
- Features: Login/register, todo CRUD with SQLite (sql.js), doughnut chart, light/dark theme, responsive layout
Template API
import { getTemplate, listTemplates } from "@illuma-ai/code-sandbox";
const names = listTemplates();
// ["express-react", "fullstack-starter"]
const tpl = getTemplate("fullstack-starter");
// { files: FileMap, entryCommand: string, port: number }Theming
Sandbox Chrome (CSS Custom Properties)
The sandbox UI is themed via --sb-* CSS custom properties. Override them in your CSS to match your application:
:root {
--sb-bg: #1e1e1e;
--sb-bg-alt: #252526;
--sb-bg-hover: #2a2d2e;
--sb-bg-active: #37373d;
--sb-sidebar: #252526;
--sb-editor: #1e1e1e;
--sb-terminal: #0d1117;
--sb-terminal-header: #161b22;
--sb-preview: #ffffff;
--sb-border: #3c3c3c;
--sb-text: #cccccc;
--sb-text-muted: #858585;
--sb-text-active: #ffffff;
--sb-accent: #007acc;
--sb-accent-hover: #1a8ad4;
--sb-success: #3fb950;
--sb-warning: #d29922;
--sb-error: #f85149;
--sb-info: #58a6ff;
--sb-scrollbar-thumb: rgba(255, 255, 255, 0.12);
--sb-scrollbar-thumb-hover: rgba(255, 255, 255, 0.25);
--sb-scrollbar-track: transparent;
--sb-scrollbar-width: 6px;
}Theme Variable Bridge
The sandbox CSS also defines shadcn-compatible design tokens (--text-primary, --surface-primary, --border-light, --chart-1 through --chart-5, etc.) in both light and dark variants. These are available inside the preview iframe content for seamless visual integration with your application's UI.
Add the .dark class to an ancestor element to activate dark mode tokens.
Advanced Usage
Individual Components
All sub-components are exported individually for custom layouts:
import {
FileTree,
buildFileTree,
CodeEditor,
Terminal,
Preview,
BootOverlay,
ViewSlider,
} from "@illuma-ai/code-sandbox";<FileTree>
Renders a collapsible file tree with per-extension SVG icons (19 distinct types: JS, TS, JSX, TSX, JSON, HTML, CSS, SCSS, MD, PY, RB, GO, RS, YML, ENV, SH, SQL, SVG, Lock) and change status indicators.
import { FileTree, buildFileTree } from "@illuma-ai/code-sandbox";
const tree = buildFileTree(fileMap);
<FileTree
files={tree}
selectedFile="server.js"
onSelectFile={(path) => setSelected(path)}
fileChanges={{ "server.js": "modified" }}
/>;<CodeEditor>
Monaco-based code editor with file tabs, diff mode toggle, and change status indicators on tabs.
<CodeEditor
files={fileMap}
originalFiles={baselineFiles}
fileChanges={changes}
activeFile="server.js"
openFiles={["server.js", "public/index.html"]}
onSelectFile={(path) => {}}
onCloseFile={(path) => {}}
onFileChange={(path, content) => {}}
readOnly={false}
diffMode={false}
/><Terminal>
Clean monochrome terminal output display with minimize/expand toggle and line count badge.
<Terminal
output={terminalLines}
minimized={false}
onToggleMinimize={() => setMinimized(!minimized)}
/><Preview>
Live preview iframe with JavaScript error capture (injects window.onerror, unhandledrejection, and console.error listeners). URL bar displays friendly localhost:3000/ instead of internal paths.
<Preview
url={previewUrl}
onRefresh={() => runtime.restart()}
onBrowserError={(error) => handleError(error)}
reloadKey={state.previewReloadKey}
/><BootOverlay>
Loading overlay with staged progress animation.
<BootOverlay
progress={{
stage: "installing",
message: "Installing express...",
percent: 55,
}}
/><ViewSlider>
Icon-based tab toggle for Code, Preview, and API Docs views with Framer Motion animation. Icons expand to show labels on hover.
import type { WorkbenchView } from "@illuma-ai/code-sandbox";
// WorkbenchView = "code" | "preview" | "swagger"
<ViewSlider
selected={view}
onSelect={(v: WorkbenchView) => setView(v)}
/>;NodepodRuntime Service
The framework-agnostic runtime service (no React dependency). Use directly for custom integrations.
import { NodepodRuntime } from "@illuma-ai/code-sandbox";
const runtime = new NodepodRuntime({
files: myFiles,
entryCommand: "node server.js",
port: 3000,
workdir: "/app",
env: { NODE_ENV: "production" },
});
runtime.setProgressCallback((progress) => console.log(progress));
runtime.setOutputCallback((line) => console.log(line));
runtime.setServerReadyCallback((port, url) => console.log("Ready:", url));
runtime.setErrorCallback((error) => console.error(error));
await runtime.boot();
// File operations
await runtime.writeFile("server.js", newContent);
await runtime.deleteFile("old-route.js");
const content = await runtime.readFile("server.js");
// State
const files = runtime.getCurrentFiles();
const changed = runtime.getChangedFiles();
const errors = runtime.getErrors();
const status = runtime.getStatus();
const previewUrl = runtime.getPreviewUrl();
// Lifecycle
await runtime.restart();
runtime.teardown();useRuntime Hook
React hook wrapping NodepodRuntime. Powers the CodeSandbox component internally. Use for fully custom React UIs.
import { useRuntime } from "@illuma-ai/code-sandbox";
const {
state, // RuntimeState
selectedFile, // string | null
openFiles, // string[]
handleFileChange, // (path, content) => void
handleSelectFile, // (path) => void
handleCloseFile, // (path) => void
handleBrowserError, // (error: SandboxError) => void
restart, // () => Promise<void>
updateFiles, // (files, options?) => Promise<void>
updateFile, // (path, content) => Promise<void>
deleteFile, // (path) => Promise<void>
getFiles, // () => FileMap
getChangedFiles, // () => FileMap
getFileChanges, // () => Record<string, FileChangeStatus>
getErrors, // () => SandboxError[]
getState, // () => RuntimeState
} = useRuntime(props);Nodepod Constraints
The sandbox runs on Nodepod, a browser-native Node.js runtime. Key constraints:
- No build tools: Vite, Webpack, Next.js, etc. will not work. Use CDN-loaded libraries instead.
express.static()does not work. Serve files manually viafs.readFileSync()+res.send().res.sendFile()does not work. Same — usefs.readFileSync()+res.send().- Module wrapper hides browser globals:
document,window,locationareundefinedin server-side code (as expected in Node.js). - Preview URL rewriting: Nodepod returns
/__virtual__/{port}URLs but the Service Worker expects/__preview__/{port}/. The sandbox handles this transparently.
All Exports
// Main component
export { CodeSandbox } from "@illuma-ai/code-sandbox";
// Individual components
export { FileTree, buildFileTree } from "@illuma-ai/code-sandbox";
export { CodeEditor } from "@illuma-ai/code-sandbox";
export { Terminal } from "@illuma-ai/code-sandbox";
export { Preview } from "@illuma-ai/code-sandbox";
export { BootOverlay } from "@illuma-ai/code-sandbox";
export { ViewSlider } from "@illuma-ai/code-sandbox";
// Services & hooks
export { NodepodRuntime } from "@illuma-ai/code-sandbox";
export { useRuntime } from "@illuma-ai/code-sandbox";
// Templates
export { getTemplate, listTemplates } from "@illuma-ai/code-sandbox";
// Types
export type {
FileMap,
FileNode,
FileChangeStatus,
SandboxError,
SandboxErrorCategory,
SandboxErrorSeverity,
ConsoleEntry,
ConsoleLevel,
BootStage,
BootProgress,
RuntimeConfig,
RuntimeState,
CodeSandboxProps,
CodeSandboxHandle,
FileTreeProps,
CodeEditorProps,
TerminalProps,
PreviewProps,
BootOverlayProps,
WorkbenchView,
} from "@illuma-ai/code-sandbox";License
MIT - See LICENSE for details.
