// ========================================== // Armature — Editor Package (v0.31.0) // ========================================== // Installable .pkg that provides the code editor surface. // Mounts into #extension-mount (surface-extension template). // // Uses Component.mount() for all sub-components — single source // of truth for DOM structure. No duplicated template partials. // // Dependencies (loaded by base.html): // FileTree, CodeEditor, ChatPane, NotePanel, note-graph, PaneContainer // API, UI, App, sw, esc (ui-primitives) // // Dynamically loaded: // codemirror.bundle.js // ========================================== (function () { 'use strict'; const SURFACE_ID = 'editor'; const PREFIX = 'ed'; const NOTES_PREFIX = 'edNotes'; const STATE_DEBOUNCE_MS = 2000; if (window.__SURFACE__ !== SURFACE_ID) return; const base = window.__BASE__ || ''; // ── Dynamic Script Loader ──────────────────── function _loadScript(src) { return new Promise((resolve, reject) => { if (document.querySelector('script[src*="' + src.split('?')[0] + '"]')) { resolve(); return; } const s = document.createElement('script'); s.src = base + src; s.type = 'module'; s.onload = resolve; s.onerror = () => { resolve(); }; // Non-fatal document.head.appendChild(s); }); } // ── Init ───────────────────────────────────── async function _init() { const mount = document.getElementById('extension-mount'); if (!mount) return; // Hide server-rendered user menu (we mount our own in the topbar) const serverMenu = document.getElementById('userMenuWrap'); if (serverMenu) serverMenu.style.display = 'none'; // Wrap in surface container for CSS scoping const surface = document.createElement('div'); surface.className = 'surface-editor'; surface.id = 'editorSurface'; mount.appendChild(surface); // Read workspace ID from query param const params = new URL(window.location.href).searchParams; const wsId = params.get('ws') || ''; // Load workspace name let wsName = 'Editor'; if (wsId && typeof API !== 'undefined') { try { const ws = await API._get('/api/v1/workspaces/' + wsId); wsName = ws?.name || ws?.data?.name || wsName; } catch (_) {} } // Build topbar const topbar = _buildTopbar(wsName); surface.appendChild(topbar); // Mount user menu via SDK (flyout drops down from topbar) if (typeof sw !== 'undefined' && sw.userMenu) { sw.userMenu(topbar, { flyout: 'down' }); } // Build body + bootstrap const body = document.createElement('div'); body.className = 'editor-body'; body.id = 'editorBody'; surface.appendChild(body); const bootstrap = _buildBootstrap(); surface.appendChild(bootstrap); // Load dynamic dependencies (codemirror only — ChatPane, NotePanel, note-graph are platform scripts in base.html) const ver = window.__VERSION__ || ''; const verQ = ver ? '?v=' + ver : ''; await _loadScript('/vendor/codemirror/codemirror.bundle.js' + verQ); _initWsSelector(wsId); if (!wsId) { body.style.display = 'none'; bootstrap.style.display = ''; _loadBootstrapList(); _initBootstrapCreate(); return; } _mountEditor(wsId, wsName); } // ── Topbar ────────────────────────────────── function _buildTopbar(wsName) { const el = document.createElement('div'); el.className = 'editor-topbar'; el.id = 'editorTopbar'; el.innerHTML = '' + '' + 'Back' + '' + '
' + '' + '
' + '' + '
' + '
' + '
' + '' + '
' + '
' + '' + '
' + ''; return el; } // ── Bootstrap (no workspace) ──────────────── function _buildBootstrap() { const el = document.createElement('div'); el.className = 'editor-bootstrap'; el.id = 'editorBootstrap'; el.style.display = 'none'; el.innerHTML = '
' + '' + '' + '' + '

Open a Workspace

' + '
' + '
Loading workspaces\u2026
' + '
' + '
' + '
' + 'or create new' + '
' + '
' + '' + '' + '
'; return el; } // ── Workspace Selector ────────────────────── function _initWsSelector(currentWsId) { const btn = document.getElementById('editorWsSelectorBtn'); const dropdown = document.getElementById('editorWsDropdown'); if (!btn || !dropdown) return; btn.addEventListener('click', (e) => { e.stopPropagation(); if (dropdown.classList.toggle('open')) _loadWsDropdown(currentWsId); }); document.addEventListener('click', (e) => { if (!e.target.closest('#editorWsSelector')) dropdown.classList.remove('open'); }); document.getElementById('editorWsNewBtn')?.addEventListener('click', async () => { dropdown.classList.remove('open'); const name = window.prompt('Workspace name:'); if (!name) return; try { const userId = sw.user?.id; if (!userId) throw new Error('Not authenticated'); const resp = await API.createWorkspace({ name: name.trim(), owner_type: 'user', owner_id: userId }); const newId = resp.id || resp.data?.id; if (newId) window.location.href = base + '/s/editor?ws=' + newId; } catch (e) { if (typeof UI !== 'undefined') UI.toast('Failed: ' + e.message, 'error'); } }); } async function _loadWsDropdown(currentWsId) { const listEl = document.getElementById('editorWsList'); if (!listEl) return; listEl.innerHTML = '
Loading\u2026
'; try { const resp = await API._get('/api/v1/workspaces'); const workspaces = resp.data || resp || []; listEl.innerHTML = ''; if (!workspaces.length) { listEl.innerHTML = '
No workspaces
'; return; } workspaces.forEach(ws => { const item = document.createElement('button'); item.className = 'editor-ws-dropdown-item' + (ws.id === currentWsId ? ' active' : ''); item.textContent = ws.name || ws.id?.slice(0, 8); item.addEventListener('click', () => { window.location.href = base + '/s/editor?ws=' + ws.id; }); listEl.appendChild(item); }); } catch (_) { listEl.innerHTML = '
Failed to load
'; } } // ── Bootstrap ──────────────────────────────── async function _loadBootstrapList() { const listEl = document.getElementById('editorBootstrapList'); if (!listEl) return; try { const resp = await API._get('/api/v1/workspaces'); const workspaces = resp.data || resp || []; if (!workspaces.length) { listEl.innerHTML = '
No workspaces yet
'; return; } listEl.innerHTML = ''; workspaces.forEach(ws => { const item = document.createElement('button'); item.className = 'editor-bootstrap-ws-item'; item.innerHTML = '' + esc(ws.name || ws.id?.slice(0, 8)) + '' + '' + esc(ws.created_at ? new Date(ws.created_at).toLocaleDateString() : '') + ''; item.addEventListener('click', () => { window.location.href = base + '/s/editor?ws=' + ws.id; }); listEl.appendChild(item); }); } catch (_) { listEl.innerHTML = '
Failed to load workspaces
'; } } function _initBootstrapCreate() { const btn = document.getElementById('editorBootstrapBtn'); const input = document.getElementById('editorBootstrapName'); if (!btn || !input) return; btn.addEventListener('click', async () => { const name = input.value.trim() || 'workspace'; btn.disabled = true; btn.textContent = 'Creating\u2026'; try { const userId = sw.user?.id; if (!userId) throw new Error('Not authenticated'); const resp = await API.createWorkspace({ name, owner_type: 'user', owner_id: userId }); const newId = resp.id || resp.data?.id; if (!newId) throw new Error('No workspace ID returned'); window.location.href = base + '/s/editor?ws=' + newId; } catch (e) { btn.disabled = false; btn.textContent = 'Create Workspace'; if (typeof UI !== 'undefined') UI.toast('Failed: ' + e.message, 'error'); } }); } // ── Mount Pane Layout ─────────────────────── function _mountEditor(wsId, wsName) { const body = document.getElementById('editorBody'); if (!body) return; // ── Layout via SDK ── const layout = sw.layout(body, 'editor', { workspaceId: wsId }); if (!layout) return; const filesPaneEl = layout._panes.get('files')?.el; const editorPaneEl = layout._panes.get('editor')?.el; const assistPaneInfo = layout._panes.get('assist'); // ── FileTree via SDK ── let fileTree; if (filesPaneEl) { fileTree = sw.fileTree(filesPaneEl, { id: PREFIX, workspaceId: wsId, onSelect: (path) => { codeEditor?.openFile(path); _saveState(wsId, codeEditor); }, onDelete: (path) => _deleteFile(wsId, path, fileTree, codeEditor), onNewFile: () => _createNewFile(wsId, fileTree), }); layout._panes.get('files').component = fileTree; } // ── CodeEditor via SDK ── let codeEditor; if (editorPaneEl) { codeEditor = sw.codeEditor(editorPaneEl, { id: PREFIX, workspaceId: wsId, onSave: () => fileTree?.refresh(), onActivate: (path) => { fileTree?.setActiveFile(path); _saveState(wsId, codeEditor); }, }); layout._panes.get('editor').component = codeEditor; } // ── Assist pane (tabbed: chat + notes) via SDK ── if (assistPaneInfo?.tabs) { // Chat tab — standalone mode handles everything (streaming, model selector, history) const chatPanel = assistPaneInfo.getTabPanel('chat'); if (chatPanel) { const chatPane = sw.chat(chatPanel, { id: PREFIX, standalone: true, getContext: () => _getFileContext(codeEditor), }); if (chatPane) { const chatTab = assistPaneInfo.tabs.find(t => t.id === 'chat'); if (chatTab) chatTab.instance = chatPane; } } // Notes tab const notesPanel = assistPaneInfo.getTabPanel('notes'); if (notesPanel) { const notePanel = sw.notes(notesPanel, { projectId: null }); if (notePanel) { const notesTab = assistPaneInfo.tabs.find(t => t.id === 'notes'); if (notesTab) { notesTab.instance = notePanel; const _loadNotes = () => { if (!notesTab._loaded) { notesTab._loaded = true; notePanel.loadNotesList(); notePanel.loadNoteFolders(); } }; notesTab.btn.addEventListener('click', _loadNotes); if (notesTab._activated) _loadNotes(); } } } } // Git branch _refreshGitBranch(wsId, codeEditor); // Toolbar document.getElementById('editorRefreshBtn')?.addEventListener('click', () => { fileTree?.refresh(); _refreshGitBranch(wsId, codeEditor); }); // Initial load fileTree?.refresh(); _restoreState(wsId, codeEditor); console.log('[EditorPkg] Mounted for workspace', wsId); } // ── File Operations ───────────────────────── async function _deleteFile(wsId, path, fileTree, codeEditor) { const ok = typeof sw !== 'undefined' && sw.confirm ? await sw.confirm('Delete ' + path + '?', { destructive: true }) : window.confirm('Delete ' + path + '?'); if (!ok) return; try { await API.deleteWorkspaceFile(wsId, path); if (codeEditor?.getOpenFiles().includes(path)) await codeEditor.closeFile(path); fileTree?.refresh(); } catch (e) { if (typeof UI !== 'undefined') UI.toast('Delete failed: ' + e.message, 'error'); } } async function _createNewFile(wsId, fileTree) { const name = window.prompt('File name (e.g. src/main.go):'); if (!name) return; try { await API.writeWorkspaceFile(wsId, name.trim(), ''); fileTree?.refresh(); if (typeof UI !== 'undefined') UI.toast('Created ' + name.trim(), 'success'); } catch (e) { if (typeof UI !== 'undefined') UI.toast('Failed: ' + e.message, 'error'); } } async function _refreshGitBranch(wsId, codeEditor) { try { const resp = await API.getWorkspaceGitBranches(wsId); const branch = resp.current || null; if (branch) { const badge = document.getElementById('editorBranchBadge'); const name = document.getElementById('editorBranchName'); if (badge) badge.style.display = ''; if (name) name.textContent = branch; } if (codeEditor) codeEditor.setBranch(branch); } catch (_) {} } // ── File Context (editor-specific, passed to ChatPane via getContext) ── function _getFileContext(editor) { if (!editor) return null; try { const openFiles = editor.getOpenFiles(); if (!openFiles.length) return null; const activeTab = document.querySelector('.code-editor-tab.active'); const path = activeTab?.dataset?.path || openFiles[0]; const inst = CodeEditor._instances?.get(PREFIX); if (inst?._files) { const file = inst._files.get(path); if (file?.view) return { path, content: file.view.state.doc.toString().slice(0, 4000) }; } } catch (_) {} return null; } // ── State Persistence (localStorage) ───────── const STATE_KEY_PREFIX = 'sb:editor:state:'; let _stateSaveTimer = null; function _stateKey(wsId) { return STATE_KEY_PREFIX + (sw?.user?.id || '') + ':' + wsId; } function _restoreState(wsId, codeEditor) { if (!wsId) return; try { const raw = localStorage.getItem(_stateKey(wsId)); if (!raw) return; const state = JSON.parse(raw); if (state.open_tabs && Array.isArray(state.open_tabs)) { for (const path of state.open_tabs) codeEditor.openFile(path); if (state.active_tab) codeEditor.activateFile(state.active_tab); } } catch (_) {} } function _saveState(wsId, codeEditor) { if (_stateSaveTimer) clearTimeout(_stateSaveTimer); _stateSaveTimer = setTimeout(() => { if (!wsId) return; try { const openFiles = codeEditor.getOpenFiles(); const activeTab = document.querySelector('.code-editor-tab.active'); localStorage.setItem(_stateKey(wsId), JSON.stringify({ open_tabs: openFiles, active_tab: activeTab?.dataset?.path || '', updated_at: new Date().toISOString(), })); } catch (_) {} }, STATE_DEBOUNCE_MS); } // ── Boot ───────────────────────────────────── document.addEventListener('DOMContentLoaded', _init); })();