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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user