Support demos/ in the local-mode workspace client

Treat demos as a writable top-level folder alongside code/, seed it on
first open, and ship browser_fetch.py with bundled lib stubs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 22:34:45 +12:00
parent d38f819c49
commit 8c45097ec5

View File

@@ -91,7 +91,7 @@ function isLibPath(path) {
function isWritablePath(path) {
if (!path) return false;
const root = path.split('/')[0];
return root === 'code';
return root === 'code' || root === 'demos';
}
function splitParts(path) {
@@ -367,6 +367,7 @@ class FileSystemBackend {
}
async ensureSeed() {
await this._resolveDir(['demos'], true);
const codeDir = await this._resolveDir(['code'], true);
let mainExists = false;
for await (const [name] of codeDir.entries()) {
@@ -543,20 +544,22 @@ class FileSystemBackend {
async listAllPyFiles() {
const out = {};
let codeDir;
try {
codeDir = await this._resolveDir(['code']);
} catch (_e) {
return out;
}
const collected = [];
await this._readAllRecursive(['code'], codeDir, collected);
for (const { path, handle } of collected) {
if (!path.toLowerCase().endsWith('.py')) continue;
for (const rootSeg of ['code', 'demos']) {
let dir;
try {
out[path] = await (await handle.getFile()).text();
dir = await this._resolveDir([rootSeg]);
} catch (_e) {
// Skip unreadable files.
continue;
}
const collected = [];
await this._readAllRecursive([rootSeg], dir, collected);
for (const { path, handle } of collected) {
if (!path.toLowerCase().endsWith('.py')) continue;
try {
out[path] = await (await handle.getFile()).text();
} catch (_e) {
// Skip unreadable files.
}
}
}
return out;
@@ -564,19 +567,21 @@ class FileSystemBackend {
async listAllUserFiles() {
const out = {};
let codeDir;
try {
codeDir = await this._resolveDir(['code']);
} catch (_e) {
return out;
}
const collected = [];
await this._readAllRecursive(['code'], codeDir, collected);
for (const { path, handle } of collected) {
for (const rootSeg of ['code', 'demos']) {
let dir;
try {
out[path] = await (await handle.getFile()).text();
dir = await this._resolveDir([rootSeg]);
} catch (_e) {
// Skip unreadable files (e.g. binaries the editor can't open).
continue;
}
const collected = [];
await this._readAllRecursive([rootSeg], dir, collected);
for (const { path, handle } of collected) {
try {
out[path] = await (await handle.getFile()).text();
} catch (_e) {
// Skip unreadable files (e.g. binaries the editor can't open).
}
}
}
return out;
@@ -700,7 +705,7 @@ export class LocalWorkspaceClient {
/* --- Lib bundle ------------------------------------------------- */
async _tryLoadStaticBundledLib() {
const names = ['machine.py', 'neopixel.py'];
const names = ['machine.py', 'neopixel.py', 'browser_fetch.py'];
const map = {};
for (const name of names) {
const r = await fetch(`/static/bundled-lib/${encodeURIComponent(name)}`, {
@@ -770,6 +775,9 @@ export class LocalWorkspaceClient {
if (!filtered.some((row) => row.name === 'code')) {
filtered.push({ name: 'code', is_directory: true, size: null });
}
if (!filtered.some((row) => row.name === 'demos')) {
filtered.push({ name: 'demos', is_directory: true, size: null });
}
if (this.libFiles && Object.keys(this.libFiles).length) {
filtered.push({ name: 'lib', is_directory: true, size: null });
}
@@ -810,7 +818,7 @@ export class LocalWorkspaceClient {
const path = normalizePath(rawPath);
if (!path) return jsonResponse(400, { detail: 'Empty path' });
if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable (lib is read-only)' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable (lib is read-only)' });
const content = body && typeof body.content === 'string' ? body.content : '';
await this.backend.writeFile(path, content);
return jsonResponse(200, { filename: basename(path) });
@@ -821,7 +829,7 @@ export class LocalWorkspaceClient {
const path = normalizePath(rawPath);
if (!path) return jsonResponse(400, { detail: 'Empty path' });
if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' });
const r = await this.backend.deleteFile(path);
if (!r.ok) {
if (r.reason === 'missing') return jsonResponse(404, { detail: 'File not found' });
@@ -836,7 +844,7 @@ export class LocalWorkspaceClient {
const path = normalizePath(rawPath);
if (!path) return jsonResponse(400, { detail: 'Empty path' });
if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' });
const r = await this.backend.createFolder(path);
if (!r.ok) {
if (r.reason === 'exists') return jsonResponse(400, { detail: 'Folder already exists' });
@@ -850,7 +858,7 @@ export class LocalWorkspaceClient {
const path = normalizePath(rawPath);
if (!path) return jsonResponse(400, { detail: 'Empty path' });
if (isLibPath(path)) return jsonResponse(403, { detail: 'lib is read-only' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ is writable' });
if (!isWritablePath(path)) return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' });
const r = await this.backend.deleteFolder(path);
if (!r.ok) {
if (r.reason === 'missing') return jsonResponse(404, { detail: 'Folder not found' });
@@ -867,7 +875,7 @@ export class LocalWorkspaceClient {
if (!source) return jsonResponse(400, { detail: 'Missing source_path' });
if (isLibPath(source) || isLibPath(dest)) return jsonResponse(403, { detail: 'lib is read-only' });
if (!isWritablePath(source) || (dest && !isWritablePath(dest))) {
return jsonResponse(403, { detail: 'Only code/ is writable' });
return jsonResponse(403, { detail: 'Only code/ and demos/ are writable' });
}
const r = await this.backend.movePath(source, dest);
if (!r.ok) {