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