Add tutorial route; gate lib workspace for superusers; Py worker v4

- Serve /tutorial and add tutorial.html/tutorial.js assets
- Fetch auth role; hide lib from non-superusers in tree and restored tabs
- Cache workspace Python sources briefly for Py worker
- Pyodide worker and home/index links/styling tweaks

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 01:14:51 +12:00
parent 7d682cce8d
commit 6fc651ad72
8 changed files with 770 additions and 16 deletions

View File

@@ -31,6 +31,8 @@ class TextEditor {
this.completionRequestId = 0;
this.diagnosticsRequestId = 0;
this.diagnosticsTimer = null;
this.workspaceSourcesCache = null;
this.workspaceSourcesCacheAt = 0;
this.draggedItemPath = null;
this.draggedItemIsDirectory = false;
this.dragHoverExpandTimer = null;
@@ -43,10 +45,27 @@ class TextEditor {
this.lastLedFrame = null;
this.ledPanelWindow = null;
this.workspaceUserId = null;
this.isSuperuser = false;
this.init();
}
async getWorkspacePythonSources() {
const now = Date.now();
if (this.workspaceSourcesCache && (now - this.workspaceSourcesCacheAt) < 1200) {
return { ...this.workspaceSourcesCache };
}
const response = await this.apiFetch('/api/workspace/py-sources');
if (!response.ok) {
throw new Error('Failed to load workspace sources');
}
const payload = await response.json().catch(() => ({}));
const files = payload && typeof payload.files === 'object' ? payload.files : {};
this.workspaceSourcesCache = files;
this.workspaceSourcesCacheAt = now;
return { ...files };
}
init() {
try {
const params = new URLSearchParams(window.location.search);
@@ -69,7 +88,22 @@ class TextEditor {
this.setLspStatus('LSP: n/a', 'Open a Python file for diagnostics');
this.updateWorkspaceBanner();
this.prewarmPyWorker();
this.loadInitialDirectoryState().then(() => this.restoreSessionTabs());
this.fetchViewerRole()
.finally(() => this.loadInitialDirectoryState().then(() => this.restoreSessionTabs()));
}
async fetchViewerRole() {
try {
const me = await fetch('/api/auth/me', { credentials: 'include' });
if (!me.ok) {
this.isSuperuser = false;
return;
}
const data = await me.json().catch(() => ({}));
this.isSuperuser = Boolean(data && data.user && data.user.is_superuser);
} catch (_error) {
this.isSuperuser = false;
}
}
updateWorkspaceBanner() {
@@ -152,7 +186,7 @@ class TextEditor {
ensurePyWorker() {
if (!this.pyWorker) {
const worker = new Worker('/static/pyodide-worker.js?v=3');
const worker = new Worker('/static/pyodide-worker.js?v=4');
this.pyWorker = worker;
worker.onmessage = (event) => this.handlePyWorkerMessage(event);
}
@@ -255,6 +289,9 @@ class TextEditor {
return;
}
for (const path of session.openTabPaths) {
if (!this.isSuperuser && typeof path === 'string' && path.startsWith('lib/')) {
continue;
}
await this.openFile(path);
}
if (session.activeTabPath && this.findTab(session.activeTabPath)) {
@@ -290,7 +327,7 @@ class TextEditor {
}
getVisibleTopLevelNames() {
return new Set(['code', 'lib']);
return this.isSuperuser ? new Set(['code', 'lib']) : new Set(['code']);
}
getDefaultEditableRoot() {
@@ -300,12 +337,14 @@ class TextEditor {
async loadInitialDirectoryState() {
await this.loadDirectory('');
await this.ensureFolderExists('code');
await this.ensureFolderExists('lib');
this.selectedPath = 'code';
this.selectedIsDirectory = true;
this.expandedDirs.add('code');
await this.loadDirectory('code', { suppressError: true });
await this.loadDirectory('lib', { suppressError: true });
if (this.isSuperuser) {
await this.ensureFolderExists('lib');
await this.loadDirectory('lib', { suppressError: true });
}
this.renderFileTree();
}
@@ -448,7 +487,7 @@ class TextEditor {
this.setLspStatus('LSP: checking...', 'Running Jedi syntax diagnostics');
try {
await this.ensurePyodideReady();
const extraFiles = {};
const extraFiles = await this.getWorkspacePythonSources();
for (const tab of this.openTabs) {
if (tab.path && tab.path.toLowerCase().endsWith('.py')) {
extraFiles[tab.path] = tab.content;
@@ -530,7 +569,7 @@ class TextEditor {
try {
await this.ensurePyodideReady();
const extraFiles = {};
const extraFiles = await this.getWorkspacePythonSources();
for (const tab of this.openTabs) {
if (tab.path && tab.path.toLowerCase().endsWith('.py')) {
extraFiles[tab.path] = tab.content;
@@ -667,9 +706,18 @@ class TextEditor {
window.addEventListener('beforeunload', persistSession);
window.addEventListener('pagehide', persistSession);
window.addEventListener('keydown', (event) => {
if (!this.completionOpen) return;
if (event.key === 'Enter' || event.key === 'Tab') {
event.preventDefault();
event.stopPropagation();
this.applySelectedCompletion();
}
}, true);
document.addEventListener('keydown', (event) => {
if (this.completionOpen) {
if (event.key === 'Enter') {
if (event.key === 'Enter' || event.key === 'Tab') {
event.preventDefault();
this.applySelectedCompletion();
return;