@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/components/Workbench.d.ts +3 -2
- package/dist/hooks/useRuntime.d.ts +24 -14
- package/dist/index.cjs +82 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4209 -4184
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +105 -13
- package/package.json +1 -1
- package/src/components/Workbench.tsx +133 -87
- package/src/hooks/useRuntime.ts +182 -216
- package/src/index.ts +1 -0
- package/src/types.ts +119 -14
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,
|
|
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 (
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
@@ -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 {
|
|
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
|
|
54
|
-
|
|
59
|
+
export const CodeSandbox = forwardRef<CodeSandboxHandle, CodeSandboxProps>(
|
|
60
|
+
function CodeSandbox(props, ref) {
|
|
61
|
+
const { className = "", height = "100vh", port = 3000 } = props;
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
107
|
+
const fileTree = useMemo(() => buildFileTree(state.files), [state.files]);
|
|
108
|
+
const isBooting = state.status !== "ready" && state.status !== "error";
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
[
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
{/* Toolbar — compact 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
|
-
{/*
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
CodeSandbox.displayName = "CodeSandbox";
|
|
149
195
|
|
|
150
196
|
// ---------------------------------------------------------------------------
|
|
151
197
|
// Toolbar
|