@illuma-ai/code-sandbox 1.1.0 → 1.2.0

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.
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
  /**
@@ -154,13 +152,101 @@ export interface GitPRResult {
154
152
  url: string;
155
153
  branch: string;
156
154
  }
155
+ /**
156
+ * Imperative handle exposed by CodeSandbox via React.forwardRef.
157
+ *
158
+ * This is the primary API for host applications (like Ranger) to
159
+ * control the sandbox programmatically. The sandbox is a "dumb renderer"
160
+ * — the host pushes files in and reads state out.
161
+ *
162
+ * @example
163
+ * ```tsx
164
+ * const ref = useRef<CodeSandboxHandle>(null);
165
+ *
166
+ * // Push files from any source (GitHub, DB, S3, agent output)
167
+ * ref.current?.updateFiles(fileMap);
168
+ *
169
+ * // Push a single file (e.g., agent modified one file)
170
+ * ref.current?.updateFile('server.js', newContent);
171
+ *
172
+ * // Read current state
173
+ * const files = ref.current?.getFiles();
174
+ * const changes = ref.current?.getChangedFiles();
175
+ * const errors = ref.current?.getErrors();
176
+ * ```
177
+ */
178
+ export interface CodeSandboxHandle {
179
+ /**
180
+ * Replace the entire file set. Diffs against current files, writes only
181
+ * changed files to Nodepod FS, and restarts the server if anything changed.
182
+ *
183
+ * The previous file set becomes `originalFiles` for diff tracking.
184
+ *
185
+ * @param files - Complete new file set
186
+ * @param options - Optional: restartServer (default true)
187
+ */
188
+ updateFiles: (files: FileMap, options?: {
189
+ restartServer?: boolean;
190
+ }) => Promise<void>;
191
+ /**
192
+ * Update a single file. Writes to Nodepod FS and updates state.
193
+ * Does NOT restart the server — call `restart()` manually if needed,
194
+ * or use `updateFiles()` for bulk updates with auto-restart.
195
+ *
196
+ * @param path - File path (e.g., "server.js", "public/index.html")
197
+ * @param content - New file content
198
+ */
199
+ updateFile: (path: string, content: string) => Promise<void>;
200
+ /**
201
+ * Force restart the server process. Kills the current process
202
+ * and re-runs the entry command.
203
+ */
204
+ restart: () => Promise<void>;
205
+ /**
206
+ * Get the current file map (all files including edits).
207
+ */
208
+ getFiles: () => FileMap;
209
+ /**
210
+ * Get only files that have been modified relative to originalFiles.
211
+ * Returns a FileMap containing only the changed files.
212
+ */
213
+ getChangedFiles: () => FileMap;
214
+ /**
215
+ * Get the per-file change status map (new/modified/deleted).
216
+ * Missing keys should be treated as "unchanged".
217
+ */
218
+ getFileChanges: () => Record<string, FileChangeStatus>;
219
+ /**
220
+ * Get all structured errors collected during this session.
221
+ */
222
+ getErrors: () => SandboxError[];
223
+ /**
224
+ * Get the current runtime state snapshot.
225
+ */
226
+ getState: () => RuntimeState;
227
+ }
228
+ /**
229
+ * Props for the CodeSandbox component.
230
+ *
231
+ * The sandbox is a pure renderer — it receives files via props and
232
+ * exposes an imperative handle for programmatic updates. It does NOT
233
+ * interact with any storage backend (GitHub, S3, DB) directly.
234
+ *
235
+ * File sources (for initial load, pick ONE):
236
+ * - `files` — pass a FileMap directly
237
+ * - `template` — use a built-in template name (e.g., "fullstack-starter")
238
+ *
239
+ * For live updates after mount, use the imperative handle:
240
+ * ```tsx
241
+ * const ref = useRef<CodeSandboxHandle>(null);
242
+ * <CodeSandbox ref={ref} files={initialFiles} />
243
+ * // Later:
244
+ * ref.current.updateFiles(newFiles);
245
+ * ```
246
+ */
157
247
  export interface CodeSandboxProps {
158
- /** Pass files directly */
248
+ /** Pass files directly (from any source — GitHub, DB, S3, agent, etc.) */
159
249
  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
250
  /** OR use a built-in template name */
165
251
  template?: string;
166
252
  /** Command to start the dev server (default inferred from package.json or template) */
@@ -169,9 +255,9 @@ export interface CodeSandboxProps {
169
255
  port?: number;
170
256
  /** Environment variables */
171
257
  env?: Record<string, string>;
172
- /** Fires when a file is modified in the editor */
258
+ /** Fires when a file is modified in the editor by the user */
173
259
  onFileChange?: (path: string, content: string) => void;
174
- /** Fires when the dev server is ready */
260
+ /** Fires when the dev server is ready (initial boot or after restart) */
175
261
  onServerReady?: (port: number, url: string) => void;
176
262
  /** Fires on boot progress changes */
177
263
  onProgress?: (progress: BootProgress) => void;
@@ -187,6 +273,12 @@ export interface CodeSandboxProps {
187
273
  * Max 2 auto-fix attempts per error is recommended (see Ranger pattern).
188
274
  */
189
275
  onSandboxError?: (error: SandboxError) => void;
276
+ /**
277
+ * Fires after updateFiles() completes (files written + server restarted).
278
+ * Useful for the host to know when the sandbox has finished processing
279
+ * a file push.
280
+ */
281
+ onFilesUpdated?: (fileChanges: Record<string, FileChangeStatus>) => void;
190
282
  /** CSS class name for the root element */
191
283
  className?: string;
192
284
  /** 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.0",
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