@illuma-ai/code-sandbox 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ type ErrorCallback = (error: SandboxError) => void;
24
24
  * - Write project files into the virtual filesystem
25
25
  * - Install npm dependencies from package.json
26
26
  * - Start the entry command (e.g., "node server.js")
27
- * - Track file modifications for git diffing
27
+ * - Track file modifications for diffing
28
28
  * - Provide preview URL for the iframe
29
29
  */
30
30
  export declare class NodepodRuntime {
package/dist/types.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @illuma-ai/code-sandbox — Type definitions
3
3
  *
4
4
  * Core types for the browser-native code sandbox.
5
- * Covers file maps, runtime states, git operations, and component props.
5
+ * Covers file maps, runtime states, imperative handle, and component props.
6
6
  */
7
7
  /** A flat map of file paths to their string contents */
8
8
  export type FileMap = Record<string, string>;
@@ -102,11 +102,9 @@ export interface RuntimeState {
102
102
  files: FileMap;
103
103
  /**
104
104
  * Baseline file snapshot — the files as they were before the latest
105
- * update (initial load, or the version before the last GitHub poll).
106
- *
107
- * Used by the diff viewer to show what changed. Empty on first load
108
- * (everything is "new"), populated after the first file update
109
- * (GitHub poll, updateFiles, etc.).
105
+ * update (via updateFiles). Used by the diff viewer to show what changed.
106
+ * Empty on first load (everything is "new"), populated after the first
107
+ * file update.
110
108
  */
111
109
  originalFiles: FileMap;
112
110
  /**
@@ -125,42 +123,101 @@ export interface RuntimeState {
125
123
  */
126
124
  errors: SandboxError[];
127
125
  }
128
- export interface GitHubRepo {
129
- owner: string;
130
- repo: string;
131
- branch?: string;
132
- path?: string;
133
- }
134
- export interface GitCommitRequest {
135
- owner: string;
136
- repo: string;
137
- /** Branch to create for the commit */
138
- branch: string;
139
- /** Base branch to fork from (default: 'main') */
140
- baseBranch?: string;
141
- /** Map of file paths to new contents (only changed files) */
142
- changes: FileMap;
143
- /** Commit message */
144
- message: string;
145
- }
146
- export interface GitPRRequest extends GitCommitRequest {
147
- /** PR title */
148
- title: string;
149
- /** PR body/description */
150
- body?: string;
151
- }
152
- export interface GitPRResult {
153
- number: number;
154
- url: string;
155
- branch: string;
126
+ /**
127
+ * Imperative handle exposed by CodeSandbox via React.forwardRef.
128
+ *
129
+ * This is the primary API for host applications (like Ranger) to
130
+ * control the sandbox programmatically. The sandbox is a "dumb renderer"
131
+ * — the host pushes files in and reads state out.
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * const ref = useRef<CodeSandboxHandle>(null);
136
+ *
137
+ * // Push files from any source
138
+ * ref.current?.updateFiles(fileMap);
139
+ *
140
+ * // Push a single file (e.g., agent modified one file)
141
+ * ref.current?.updateFile('server.js', newContent);
142
+ *
143
+ * // Read current state
144
+ * const files = ref.current?.getFiles();
145
+ * const changes = ref.current?.getChangedFiles();
146
+ * const errors = ref.current?.getErrors();
147
+ * ```
148
+ */
149
+ export interface CodeSandboxHandle {
150
+ /**
151
+ * Replace the entire file set. Diffs against current files, writes only
152
+ * changed files to Nodepod FS, and restarts the server if anything changed.
153
+ *
154
+ * The previous file set becomes `originalFiles` for diff tracking.
155
+ *
156
+ * @param files - Complete new file set
157
+ * @param options - Optional: restartServer (default true)
158
+ */
159
+ updateFiles: (files: FileMap, options?: {
160
+ restartServer?: boolean;
161
+ }) => Promise<void>;
162
+ /**
163
+ * Update a single file. Writes to Nodepod FS and updates state.
164
+ * Does NOT restart the server — call `restart()` manually if needed,
165
+ * or use `updateFiles()` for bulk updates with auto-restart.
166
+ *
167
+ * @param path - File path (e.g., "server.js", "public/index.html")
168
+ * @param content - New file content
169
+ */
170
+ updateFile: (path: string, content: string) => Promise<void>;
171
+ /**
172
+ * Force restart the server process. Kills the current process
173
+ * and re-runs the entry command.
174
+ */
175
+ restart: () => Promise<void>;
176
+ /**
177
+ * Get the current file map (all files including edits).
178
+ */
179
+ getFiles: () => FileMap;
180
+ /**
181
+ * Get only files that have been modified relative to originalFiles.
182
+ * Returns a FileMap containing only the changed files.
183
+ */
184
+ getChangedFiles: () => FileMap;
185
+ /**
186
+ * Get the per-file change status map (new/modified/deleted).
187
+ * Missing keys should be treated as "unchanged".
188
+ */
189
+ getFileChanges: () => Record<string, FileChangeStatus>;
190
+ /**
191
+ * Get all structured errors collected during this session.
192
+ */
193
+ getErrors: () => SandboxError[];
194
+ /**
195
+ * Get the current runtime state snapshot.
196
+ */
197
+ getState: () => RuntimeState;
156
198
  }
199
+ /**
200
+ * Props for the CodeSandbox component.
201
+ *
202
+ * The sandbox is a pure renderer — it receives files via props and
203
+ * exposes an imperative handle for programmatic updates. It does NOT
204
+ * interact with any storage backend directly.
205
+ *
206
+ * File sources (for initial load, pick ONE):
207
+ * - `files` — pass a FileMap directly
208
+ * - `template` — use a built-in template name (e.g., "fullstack-starter")
209
+ *
210
+ * For live updates after mount, use the imperative handle:
211
+ * ```tsx
212
+ * const ref = useRef<CodeSandboxHandle>(null);
213
+ * <CodeSandbox ref={ref} files={initialFiles} />
214
+ * // Later:
215
+ * ref.current.updateFiles(newFiles);
216
+ * ```
217
+ */
157
218
  export interface CodeSandboxProps {
158
- /** Pass files directly */
219
+ /** Pass files directly (from any source) */
159
220
  files?: FileMap;
160
- /** OR load from GitHub */
161
- github?: GitHubRepo;
162
- /** GitHub personal access token (for private repos and write-back) */
163
- gitToken?: string;
164
221
  /** OR use a built-in template name */
165
222
  template?: string;
166
223
  /** Command to start the dev server (default inferred from package.json or template) */
@@ -169,9 +226,9 @@ export interface CodeSandboxProps {
169
226
  port?: number;
170
227
  /** Environment variables */
171
228
  env?: Record<string, string>;
172
- /** Fires when a file is modified in the editor */
229
+ /** Fires when a file is modified in the editor by the user */
173
230
  onFileChange?: (path: string, content: string) => void;
174
- /** Fires when the dev server is ready */
231
+ /** Fires when the dev server is ready (initial boot or after restart) */
175
232
  onServerReady?: (port: number, url: string) => void;
176
233
  /** Fires on boot progress changes */
177
234
  onProgress?: (progress: BootProgress) => void;
@@ -187,6 +244,12 @@ export interface CodeSandboxProps {
187
244
  * Max 2 auto-fix attempts per error is recommended (see Ranger pattern).
188
245
  */
189
246
  onSandboxError?: (error: SandboxError) => void;
247
+ /**
248
+ * Fires after updateFiles() completes (files written + server restarted).
249
+ * Useful for the host to know when the sandbox has finished processing
250
+ * a file push.
251
+ */
252
+ onFilesUpdated?: (fileChanges: Record<string, FileChangeStatus>) => void;
190
253
  /** CSS class name for the root element */
191
254
  className?: string;
192
255
  /** Height of the sandbox (default: '100vh') */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illuma-ai/code-sandbox",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Illuma AI (https://github.com/illuma-ai)",
@@ -20,6 +20,8 @@ import React, {
20
20
  useEffect,
21
21
  useCallback,
22
22
  useRef,
23
+ useImperativeHandle,
24
+ forwardRef,
23
25
  } from "react";
24
26
  import { Allotment } from "allotment";
25
27
  import { motion, AnimatePresence } from "framer-motion";
@@ -32,7 +34,11 @@ import { Preview } from "./Preview";
32
34
  import { BootOverlay } from "./BootOverlay";
33
35
  import { ViewSlider, type WorkbenchView } from "./ViewSlider";
34
36
  import { useRuntime } from "../hooks/useRuntime";
35
- import type { CodeSandboxProps, FileChangeStatus } from "../types";
37
+ import type {
38
+ CodeSandboxProps,
39
+ CodeSandboxHandle,
40
+ FileChangeStatus,
41
+ } from "../types";
36
42
 
37
43
  /** Transition for view crossfade */
38
44
  const VIEW_TRANSITION = {
@@ -50,102 +56,142 @@ const VIEW_TRANSITION = {
50
56
  * A compact toolbar row contains the view toggle and (in preview mode)
51
57
  * a URL bar with refresh button.
52
58
  */
53
- export function CodeSandbox(props: CodeSandboxProps) {
54
- const { className = "", height = "100vh", port = 3000 } = props;
59
+ export const CodeSandbox = forwardRef<CodeSandboxHandle, CodeSandboxProps>(
60
+ function CodeSandbox(props, ref) {
61
+ const { className = "", height = "100vh", port = 3000 } = props;
55
62
 
56
- const {
57
- state,
58
- selectedFile,
59
- openFiles,
60
- handleFileChange,
61
- handleSelectFile,
62
- handleCloseFile,
63
- handleBrowserError,
64
- restart,
65
- } = useRuntime(props);
63
+ const runtime = useRuntime(props);
64
+ const {
65
+ state,
66
+ selectedFile,
67
+ openFiles,
68
+ handleFileChange,
69
+ handleSelectFile,
70
+ handleCloseFile,
71
+ handleBrowserError,
72
+ restart,
73
+ updateFiles,
74
+ updateFile,
75
+ getFiles,
76
+ getChangedFiles,
77
+ getFileChanges,
78
+ getErrors,
79
+ getState,
80
+ } = runtime;
66
81
 
67
- const fileTree = useMemo(() => buildFileTree(state.files), [state.files]);
68
- const isBooting = state.status !== "ready" && state.status !== "error";
82
+ // Expose imperative handle to parent via ref
83
+ useImperativeHandle(
84
+ ref,
85
+ () => ({
86
+ updateFiles,
87
+ updateFile,
88
+ restart,
89
+ getFiles,
90
+ getChangedFiles,
91
+ getFileChanges,
92
+ getErrors,
93
+ getState,
94
+ }),
95
+ [
96
+ updateFiles,
97
+ updateFile,
98
+ restart,
99
+ getFiles,
100
+ getChangedFiles,
101
+ getFileChanges,
102
+ getErrors,
103
+ getState,
104
+ ],
105
+ );
69
106
 
70
- // Active view: code or preview
71
- const [activeView, setActiveView] = useState<WorkbenchView>("code");
107
+ const fileTree = useMemo(() => buildFileTree(state.files), [state.files]);
108
+ const isBooting = state.status !== "ready" && state.status !== "error";
72
109
 
73
- // Auto-switch to preview when server becomes ready
74
- const hasPreview = !!state.previewUrl;
75
- useEffect(() => {
76
- if (hasPreview) {
77
- setActiveView("preview");
78
- }
79
- }, [hasPreview]);
110
+ // Active view: code or preview
111
+ const [activeView, setActiveView] = useState<WorkbenchView>("code");
80
112
 
81
- // Switch to code view when a file is selected from the tree
82
- const onFileSelect = useCallback(
83
- (path: string) => {
84
- handleSelectFile(path);
85
- setActiveView("code");
86
- },
87
- [handleSelectFile],
88
- );
113
+ // Auto-switch to preview when server becomes ready
114
+ const hasPreview = !!state.previewUrl;
115
+ useEffect(() => {
116
+ if (hasPreview) {
117
+ setActiveView("preview");
118
+ }
119
+ }, [hasPreview]);
89
120
 
90
- return (
91
- <div
92
- className={`sb-root relative w-full bg-sb-bg text-sb-text overflow-hidden flex flex-col ${className}`}
93
- style={{ height }}
94
- >
95
- {/* Boot overlay */}
96
- {isBooting && <BootOverlay progress={state.progress} />}
121
+ // Switch to code view when a file is selected from the tree
122
+ const onFileSelect = useCallback(
123
+ (path: string) => {
124
+ handleSelectFile(path);
125
+ setActiveView("code");
126
+ },
127
+ [handleSelectFile],
128
+ );
97
129
 
98
- {/* Toolbar — compact row with view toggle + URL bar */}
99
- <Toolbar
100
- activeView={activeView}
101
- onViewChange={setActiveView}
102
- previewUrl={state.previewUrl}
103
- port={port}
104
- onRefresh={restart}
105
- />
130
+ return (
131
+ <div
132
+ className={`sb-root relative w-full bg-sb-bg text-sb-text overflow-hidden flex flex-col ${className}`}
133
+ style={{ height }}
134
+ >
135
+ {/* Boot overlay */}
136
+ {isBooting && <BootOverlay progress={state.progress} />}
106
137
 
107
- {/* Viewsfull width, toggled */}
108
- <div className="flex-1 relative overflow-hidden min-h-0">
109
- {/* Code view */}
110
- <motion.div
111
- className="absolute inset-0"
112
- initial={false}
113
- animate={{
114
- opacity: activeView === "code" ? 1 : 0,
115
- pointerEvents: activeView === "code" ? "auto" : "none",
116
- }}
117
- transition={{ duration: 0.15 }}
118
- >
119
- <CodeView
120
- fileTree={fileTree}
121
- files={state.files}
122
- originalFiles={state.originalFiles}
123
- fileChanges={state.fileChanges}
124
- selectedFile={selectedFile}
125
- openFiles={openFiles}
126
- terminalOutput={state.terminalOutput}
127
- onSelectFile={onFileSelect}
128
- onCloseFile={handleCloseFile}
129
- onFileChange={handleFileChange}
130
- />
131
- </motion.div>
138
+ {/* Toolbarcompact row with view toggle + URL bar */}
139
+ <Toolbar
140
+ activeView={activeView}
141
+ onViewChange={setActiveView}
142
+ previewUrl={state.previewUrl}
143
+ port={port}
144
+ onRefresh={restart}
145
+ />
132
146
 
133
- {/* Preview view */}
134
- <motion.div
135
- className="absolute inset-0"
136
- initial={false}
137
- animate={{
138
- opacity: activeView === "preview" ? 1 : 0,
139
- pointerEvents: activeView === "preview" ? "auto" : "none",
140
- }}
141
- transition={{ duration: 0.15 }}
142
- >
143
- <Preview url={state.previewUrl} onBrowserError={handleBrowserError} />
144
- </motion.div>
147
+ {/* Views full width, toggled */}
148
+ <div className="flex-1 relative overflow-hidden min-h-0">
149
+ {/* Code view */}
150
+ <motion.div
151
+ className="absolute inset-0"
152
+ initial={false}
153
+ animate={{
154
+ opacity: activeView === "code" ? 1 : 0,
155
+ pointerEvents: activeView === "code" ? "auto" : "none",
156
+ }}
157
+ transition={{ duration: 0.15 }}
158
+ >
159
+ <CodeView
160
+ fileTree={fileTree}
161
+ files={state.files}
162
+ originalFiles={state.originalFiles}
163
+ fileChanges={state.fileChanges}
164
+ selectedFile={selectedFile}
165
+ openFiles={openFiles}
166
+ terminalOutput={state.terminalOutput}
167
+ onSelectFile={onFileSelect}
168
+ onCloseFile={handleCloseFile}
169
+ onFileChange={handleFileChange}
170
+ />
171
+ </motion.div>
172
+
173
+ {/* Preview view */}
174
+ <motion.div
175
+ className="absolute inset-0"
176
+ initial={false}
177
+ animate={{
178
+ opacity: activeView === "preview" ? 1 : 0,
179
+ pointerEvents: activeView === "preview" ? "auto" : "none",
180
+ }}
181
+ transition={{ duration: 0.15 }}
182
+ >
183
+ <Preview
184
+ url={state.previewUrl}
185
+ onBrowserError={handleBrowserError}
186
+ />
187
+ </motion.div>
188
+ </div>
145
189
  </div>
146
- </div>
147
- );
148
- }
190
+ );
191
+ },
192
+ );
193
+
194
+ CodeSandbox.displayName = "CodeSandbox";
149
195
 
150
196
  // ---------------------------------------------------------------------------
151
197
  // Toolbar