Add 'Reset demos' button to refresh canonical demo files

Existing accounts (including admin) seeded before new demos shipped
had no easy way to pull in the latest copies — the registration-time
seeder is intentionally non-destructive. The new badge action fetches
src/static/bundled-demos/manifest.json, confirms the overwrite, and
re-copies each canonical demo into code/. Open tabs of those files are
refreshed in place so the user sees the new content immediately.

src/static/bundled-demos/ ships the six canonical files plus the
manifest so this works in local mode and on a static-only host. The
Dockerfile now mirrors workspace/code/<demo>.py into bundled-demos/
during the image build, keeping the two locations in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 06:35:03 +12:00
parent 655f8b78fd
commit 76129469a1
10 changed files with 507 additions and 1 deletions

View File

@@ -192,6 +192,14 @@ class TextEditor {
importBtn.addEventListener('click', () => this.importWorkspaceZip());
badge.appendChild(importBtn);
const resetBtn = document.createElement('button');
resetBtn.type = 'button';
resetBtn.className = 'workspace-badge-action';
resetBtn.textContent = 'Reset demos';
resetBtn.title = 'Re-copy the bundled demos into code/ (overwrites your edits to those files)';
resetBtn.addEventListener('click', () => this.resetDemoFiles());
badge.appendChild(resetBtn);
const exit = document.createElement('button');
exit.type = 'button';
exit.className = 'workspace-badge-exit';
@@ -259,9 +267,107 @@ class TextEditor {
importBtn.addEventListener('click', () => this.importWorkspaceZip());
badge.appendChild(importBtn);
const resetBtn = document.createElement('button');
resetBtn.type = 'button';
resetBtn.className = 'workspace-badge-action';
resetBtn.textContent = 'Reset demos';
resetBtn.title = 'Re-copy the bundled demos into code/ (overwrites your edits to those files)';
resetBtn.addEventListener('click', () => this.resetDemoFiles());
badge.appendChild(resetBtn);
badge.classList.remove('hidden');
}
async resetDemoFiles() {
let manifest;
try {
const r = await fetch('/static/bundled-demos/manifest.json', { cache: 'no-store' });
if (!r.ok) throw new Error(`manifest fetch ${r.status}`);
manifest = await r.json();
} catch (err) {
this.showError(
`Could not load demo manifest: ${err && err.message ? err.message : err}`
);
return;
}
const names = Array.isArray(manifest && manifest.files) ? manifest.files.slice() : [];
if (!names.length) {
this.showError('No demos in bundle');
return;
}
if (!confirm(
`Reset ${names.length} demo file${names.length === 1 ? '' : 's'}?\n\n` +
names.map((n) => ` • code/${n}`).join('\n') +
'\n\nAny edits you made to these files will be overwritten. Other ' +
'files (main.py, your own scripts) are not touched.'
)) return;
let written = 0;
let failed = 0;
for (const name of names) {
try {
const r = await fetch(`/static/bundled-demos/${encodeURIComponent(name)}`, {
cache: 'no-store',
});
if (!r.ok) {
failed += 1;
continue;
}
const content = await r.text();
const w = await this.apiFetch(`/api/file/code/${encodeURIComponent(name)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
});
if (w && w.ok) {
written += 1;
} else {
failed += 1;
}
} catch (_err) {
failed += 1;
}
}
this.workspaceSourcesCache = null;
this.directoryCache.clear();
/* If a stale demo is currently open in a tab, refresh the editor
contents from disk so the user sees the new version. */
for (const name of names) {
const path = `code/${name}`;
if (this.findTab(path)) {
try {
const fr = await this.apiFetch(`/api/file/${encodeURIComponent(path)}`);
if (fr && fr.ok) {
const fd = await fr.json();
const tab = this.findTab(path);
if (tab) {
tab.content = typeof fd.content === 'string' ? fd.content : '';
tab.savedContent = tab.content;
if (this.activeTabPath === path) {
this.ignoreNextChange = true;
this.editor.dispatch({
changes: {
from: 0,
to: this.editor.state.doc.length,
insert: tab.content,
},
});
}
}
}
} catch (_err) {
// Skip refresh failure; user can re-open manually.
}
}
}
await this.loadInitialDirectoryState();
if (failed) {
this.showError(`Reset ${written} demo${written === 1 ? '' : 's'} (${failed} failed)`);
} else {
this.showSuccess(`Reset ${written} demo${written === 1 ? '' : 's'}`);
}
}
async pickLocalFolder() {
if (!this.localWorkspace) return;
try {