diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-26 06:00:26 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-26 06:00:26 +0200 |
| commit | f4d934d341056142da1212c845eeeb170fd452c2 (patch) | |
| tree | 55dd11c70d1342857537f4b0aeee6ea63d21545e | |
| parent | 4baa484b84c5184286cac2743575251454a5ee2b (diff) | |
auto: 2026-05-26_06:00 changes
| -rw-r--r-- | help.html | 2 | ||||
| -rw-r--r-- | main.html (renamed from index.html) | 0 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 23 | ||||
| -rw-r--r-- | modules/core/terminal.js | 25 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 21 | ||||
| -rw-r--r-- | modules/ext/webdav.js | 300 | ||||
| -rw-r--r-- | skills/test.md | 1 | ||||
| -rw-r--r-- | skills/webx11-dev-cycle.md | 12 |
8 files changed, 370 insertions, 14 deletions
@@ -668,6 +668,8 @@ ollamaconnect</pre> <tr><td><code>webdav-backup</code></td><td>Backup VFS</td></tr> <tr><td><code>webdav-restore</code></td><td>Restore VFS</td></tr> <tr><td><code>webdav-backup-list</code></td><td>List backups</td></tr> + <tr><td><code>webdav-browser</code></td><td>Visual file explorer modal</td></tr> + <tr><td><code>webdav-explorer</code></td><td>Interactive text browser</td></tr> </table></div> <h3>Calculator & Cron</h3> diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 0ef6a2f..f9e1e98 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -366,6 +366,13 @@ RULES: }, { type: 'function', function: { + name: 'session_terminal', + description: "Read the last ~50 lines of terminal output from the auto-saved session. Use this when the user references something they did in the terminal that you don't remember. Shows commands, outputs, and timestamps.", + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { name: 'session_stats', description: 'Aggregate statistics across all sessions: total tasks, most-used tools, common topics.', parameters: { type: 'object', properties: {}, required: [] } @@ -1015,6 +1022,22 @@ RULES: const sessions = dates.map(d => ({ date: d, ...rindex[d] })); return JSON.stringify({ sessions, count: sessions.length }); } + case 'session_terminal': { + const tfs = this.terminal.fs; + try { + const raw = tfs.readFile('_sessions/_auto.json'); + if (!raw) return JSON.stringify({ error: 'No auto-saved session found' }); + const data = JSON.parse(raw); + const lines = (data.terminalLines || []).slice(-50); + const text = lines.map(l => { + const ts = l.t ? '[' + l.t + '] ' : ''; + return ts + l.l; + }).join('\n'); + return JSON.stringify({ lines: lines.length, output: text, savedAt: data.savedAt || '' }); + } catch (e) { + return JSON.stringify({ error: 'Could not read terminal history: ' + e.message }); + } + } case 'session_stats': { const sfs = this.terminal.fs; const sindex = sfs.fsReadJSON('/memory/index.json') || {}; diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 0a9538f..ee1afa4 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -20,6 +20,7 @@ this._pipeBufferSize = 5000; this._historyBuffer = []; this._historyMaxLines = 50; + this._historySavePending = false; this.MAX_OUTPUT_LINES = 200; this._aiBusy = false; this._dirty = false; @@ -325,12 +326,17 @@ } } _saveHistory() { - try { - const slice = this._historyBuffer.slice(-30); - localStorage.setItem('webx11_terminal_output', JSON.stringify(slice)); - } catch (e) { - // Silently ignore storage errors (e.g. quota exceeded) - } + if (this._historySavePending) return; + this._historySavePending = true; + setTimeout(() => { + this._historySavePending = false; + try { + const slice = this._historyBuffer.slice(-30); + localStorage.setItem('webx11_terminal_output', JSON.stringify(slice)); + } catch (e) { + // Silently ignore storage errors (e.g. quota exceeded) + } + }, 3000); } _saveCommandHistory() { try { @@ -369,10 +375,15 @@ _autoSaveSession() { try { this.fs.mkdirRecursive('_sessions'); + // Clone fsRoot and strip _sessions to prevent recursive nesting + var rootClone = JSON.parse(JSON.stringify(this.fs.root)); + if (rootClone && rootClone.content && rootClone.content['_sessions']) { + delete rootClone.content['_sessions']; + } const snapshot = { type: 'session', savedAt: new Date().toISOString(), - fsRoot: this.fs.root, + fsRoot: rootClone, currentPath: this.fs.currentPath, version: 1, terminalLines: this._historyBuffer.slice(-50) diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index a505699..68d02bf 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -8,6 +8,12 @@ class VirtualFileSystem { try { const saved = localStorage.getItem('webx11_filesystem'); if (saved) { + // Safety: refuse to load if data is excessively large (>512KB raw) + if (saved.length > 524288) { + console.warn('FS data too large (' + (saved.length/1024).toFixed(0) + 'KB), skipping load'); + localStorage.removeItem('webx11_filesystem'); + return; + } const data = JSON.parse(saved); this.root = data.root || this.root; this.currentPath = data.currentPath || []; @@ -15,11 +21,16 @@ class VirtualFileSystem { } catch (e) { console.error('FS load error:', e); } } saveToStorage() { - try { - localStorage.setItem('webx11_filesystem', JSON.stringify({ - root: this.root, currentPath: this.currentPath - })); - } catch (e) { console.error('FS save error:', e); } + if (this._storageSavePending) return; + this._storageSavePending = true; + setTimeout(() => { + this._storageSavePending = false; + try { + localStorage.setItem('webx11_filesystem', JSON.stringify({ + root: this.root, currentPath: this.currentPath + })); + } catch (e) { console.error('FS save error:', e); } + }, 2000); } getCurrentDir() { let dir = this.root; diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js index 63dfb05..89e2115 100644 --- a/modules/ext/webdav.js +++ b/modules/ext/webdav.js @@ -47,6 +47,7 @@ class WebDAVModule { 'webdav-restore': 'Restore VFS from a WebDAV backup file', 'webdav-backup-list': 'List available backup files on WebDAV', 'webdav-explorer': 'webdav-explorer [path] — Interactive remote file browser', + 'webdav-browser': 'webdav-browser [path] — Visual WebDAV file explorer modal', }; } @@ -57,9 +58,15 @@ class WebDAVModule { this.config = JSON.parse(saved); // Try to get password from SecretVault if (window.webx11_secretVault && this.config && this.config.username) { - const encryptedPass = window.webx11_secretVault.decrypt('webdav_pass_' + this.config.username); + let encryptedPass = window.webx11_secretVault.decrypt('webdav_pass_' + this.config.username); if (encryptedPass !== null && encryptedPass !== undefined) { this.config.password = encryptedPass; + } else { + // Fallback to old token key + encryptedPass = window.webx11_secretVault.decrypt('webdav_token_' + this.config.username); + if (encryptedPass !== null && encryptedPass !== undefined) { + this.config.password = encryptedPass; + } } } } @@ -135,7 +142,7 @@ class WebDAVModule { } async _listDirViaAutoIndex(path) { - const baseUrl = this.config.url.replace(/\/+$/, ''); + const baseUrl = this.config.url.replace(/\/+$/, '') + '/'; const urlPath = path ? (path.startsWith('/') ? path : '/' + path) : '/'; // Use __list__ for directory listing const listUrl = baseUrl.replace(/\/spaces\/([^/]+)\//, '/spaces/$1/__list__/') + urlPath.replace(/^\//, ''); @@ -277,6 +284,20 @@ class WebDAVModule { token = localStorage.getItem('webx11_spaces_' + spaceName); } + // Fallback: fetch .token directly from server + if (!token) { + try { + const tokenResp = await fetch('/spaces/' + spaceName + '/.token'); + if (tokenResp.ok) { + token = (await tokenResp.text()).trim(); + // Cache in SecretVault for future use + if (window.webx11_secretVault) { + window.webx11_secretVault.encrypt('webdav_token_' + spaceName, token); + } + } + } catch (e) { /* .token not accessible */ } + } + if (token) { self.config = { url: autoUrl, @@ -500,6 +521,12 @@ class WebDAVModule { } }; + // --- webdav-browser: visual modal file browser --- + this.commands['webdav-browser'] = async (args) => { + const startPath = args[0] || self._browsingPath || '/'; + await self.openWebdavBrowser(startPath); + }; + // --- webdav-explorer: interactive file browser via WebDAV --- this.commands['webdav-explorer'] = async (args) => { const startPath = args[0] || '/'; @@ -524,6 +551,275 @@ class WebDAVModule { }; } + /* ── Visual WebDAV Browser Modal ── */ + + async openWebdavBrowser(path) { + if (!this.config || !this.config.url) { + if (this.terminal) this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning'); + return; + } + this._browsingPath = path; + const self = this; + + // Remove existing modal if any + const existing = document.getElementById('webdav-browser-modal'); + if (existing) existing.remove(); + + // Fetch listing + let entries = []; + let errorMsg = ''; + try { + entries = await this._listDir(path); + } catch (e) { + errorMsg = e.message; + } + + // Build modal + const overlay = document.createElement('div'); + overlay.id = 'webdav-browser-modal'; + overlay.className = 'modal-overlay'; + overlay.style.cssText = 'display:flex; justify-content:center; align-items:center; z-index:999;'; + overlay.onclick = function(e) { if (e.target === this) self.closeWebdavBrowser(); }; + + const content = document.createElement('div'); + content.className = 'modal-content'; + content.style.cssText = 'max-width:650px; max-height:80vh; width:92%;'; + + // Header + const header = document.createElement('div'); + header.className = 'modal-header'; + header.innerHTML = '<span>📁 WebDAV Browser</span><span style="cursor:pointer" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.remove();var t=window.webx11;if(t&&t.inputField)t.inputField.focus();">✕</span>'; + content.appendChild(header); + + // Body + const body = document.createElement('div'); + body.className = 'modal-body'; + body.style.cssText = 'padding:8px; overflow-y:auto; flex-grow:1;'; + + if (errorMsg) { + const errDiv = document.createElement('div'); + errDiv.style.cssText = 'color:var(--error-color); padding:16px; text-align:center;'; + errDiv.textContent = '❌ ' + errorMsg; + body.appendChild(errDiv); + } else if (entries.length === 0) { + const emptyDiv = document.createElement('div'); + emptyDiv.className = 'text-center'; + emptyDiv.style.cssText = 'padding:20px; color:var(--text-muted);'; + emptyDiv.textContent = '(empty) — upload files using the button below'; + body.appendChild(emptyDiv); + } else { + // Sort: dirs first, then files, alphabetically + entries.sort(function(a, b) { + if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; + return a.name.localeCompare(b.name); + }); + + var frag = document.createDocumentFragment(); + entries.forEach(function(entry) { + var item = document.createElement('div'); + item.className = entry.type === 'dir' ? 'file-item dir' : 'file-item'; + item.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:8px; border-bottom:1px solid #111; cursor:pointer; min-height:36px;'; + item.onmouseover = function() { this.style.background = 'var(--bg-tertiary)'; }; + item.onmouseout = function() { this.style.background = ''; }; + + var nameSpan = document.createElement('span'); + var icon = entry.type === 'dir' ? '📁' : '📄'; + nameSpan.textContent = icon + ' ' + entry.name + (entry.type === 'dir' ? '/' : ''); + + var sizeSpan = document.createElement('span'); + sizeSpan.style.cssText = 'font-size:0.8em; color:var(--text-muted);'; + if (entry.type === 'file') { + sizeSpan.textContent = self._formatSize(entry.size); + } + + item.appendChild(nameSpan); + item.appendChild(sizeSpan); + + if (entry.type === 'dir') { + item.onclick = function() { self.openWebdavBrowser(entry.href); }; + } else { + item.onclick = function() { self.previewWebdavFile(entry); }; + // Right-click or context menu could download — but for simplicity, long tap or regular click previews + // Double-click to download + item.ondblclick = function() { self.downloadFromWebdav(entry); }; + } + frag.appendChild(item); + }); + body.appendChild(frag); + } + + content.appendChild(body); + + // Footer with breadcrumb + buttons + const footer = document.createElement('div'); + footer.className = 'modal-footer'; + footer.style.cssText = 'display:flex; justify-content:space-between; align-items:center; gap:6px; flex-wrap:wrap; padding:8px 10px;'; + + // Breadcrumb + var breadDiv = document.createElement('div'); + breadDiv.style.cssText = 'font-size:0.8em; color:var(--info-color); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0;'; + var cleanPath = path.replace(/\/+$/, '') || '/'; + var parts = cleanPath.split('/').filter(Boolean); + var breadHtml = '📁 <span style="cursor:pointer;text-decoration:underline;" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.__nav(\'/\');return false;">/</span>'; + var cumulative = ''; + for (var i = 0; i < parts.length; i++) { + cumulative += '/' + parts[i]; + var seg = parts[i]; + breadHtml += '<span style="cursor:pointer;text-decoration:underline;" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.__nav(\'' + cumulative.replace(/'/g, "\\'") + '/\');return false;">' + seg + '</span>'; + if (i < parts.length - 1) breadHtml += ' ▸ '; + } + breadDiv.innerHTML = breadHtml; + // Attach nav function to modal element for breadcrumb clicks + overlay.__nav = function(p) { self.openWebdavBrowser(p); }; + + // Button group + var btnGroup = document.createElement('div'); + btnGroup.style.cssText = 'display:flex; gap:4px; flex-shrink:0;'; + + var uploadBtn = document.createElement('button'); + uploadBtn.className = 'btn'; + uploadBtn.textContent = '⬆ Upload'; + uploadBtn.onclick = function() { self.uploadToWebdav(path); }; + + var refreshBtn = document.createElement('button'); + refreshBtn.className = 'btn'; + refreshBtn.textContent = '🔄'; + refreshBtn.title = 'Refresh'; + refreshBtn.onclick = function() { self.openWebdavBrowser(path); }; + + var closeBtn = document.createElement('button'); + closeBtn.className = 'btn'; + closeBtn.textContent = 'Close'; + closeBtn.onclick = function() { self.closeWebdavBrowser(); }; + + btnGroup.appendChild(uploadBtn); + btnGroup.appendChild(refreshBtn); + btnGroup.appendChild(closeBtn); + + footer.appendChild(breadDiv); + footer.appendChild(btnGroup); + content.appendChild(footer); + + overlay.appendChild(content); + document.body.appendChild(overlay); + + this.terminal.print('📂 Opened WebDAV browser: ' + path, 'info'); + } + + closeWebdavBrowser() { + var modal = document.getElementById('webdav-browser-modal'); + if (modal) modal.remove(); + if (this.terminal && this.terminal.inputField) this.terminal.inputField.focus(); + this.terminal.print('WebDAV browser closed', 'text-muted'); + } + + async previewWebdavFile(entry) { + try { + var resp = await this._webdavRequest('GET', entry.href); + if (!resp.ok) { this.terminal.print('Failed to fetch: HTTP ' + resp.status, 'error'); return; } + var content = await resp.text(); + var isHtml = entry.name.match(/\.html?$/i); + var isImage = entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i); + var size = content.length; + + if (size > 512000 && !isHtml && !isImage) { + // Large non-html file — offer download instead + this.terminal.print(entry.name + ' is ' + this._formatSize(size) + ' — double-click to download instead of preview.', 'warning'); + this.terminal.print('--- ' + entry.name + ' (first 2000 chars) ---'); + this.terminal.print(content.substring(0, 2000)); + if (content.length > 2000) this.terminal.print('...(truncated ' + this._formatSize(size) + ' total)'); + return; + } + + if (isImage) { + var dataUrl = 'data:image/' + (entry.name.split('.').pop()) + ';base64,' + btoa(unescape(encodeURIComponent(content))); + // Use preview modal + var previewTitle = document.getElementById('preview-title'); + var previewIframe = document.getElementById('preview-iframe'); + if (previewTitle && previewIframe) { + previewTitle.textContent = '🖼 ' + entry.name; + previewIframe.srcdoc = '<img src="' + dataUrl + '" style="max-width:100%; max-height:100%; display:block; margin:auto; background:var(--bg-color);">'; + document.getElementById('preview-modal').style.display = 'flex'; + return; + } + } + + if (isHtml) { + var previewTitle = document.getElementById('preview-title'); + var previewIframe = document.getElementById('preview-iframe'); + if (previewTitle && previewIframe) { + previewTitle.textContent = '📄 ' + entry.name; + previewIframe.srcdoc = content; + document.getElementById('preview-modal').style.display = 'flex'; + return; + } + } + + // Text file — print to terminal + this.terminal.print('--- ' + entry.name + ' (' + this._formatSize(size) + ') ---', 'info'); + this.terminal.print(content); + this.terminal.print('--- End: ' + entry.name + ' ---', 'text-muted'); + } catch (e) { + this.terminal.print('Preview error: ' + e.message, 'error'); + } + } + + uploadToWebdav(currentPath) { + var self = this; + var input = document.createElement('input'); + input.type = 'file'; + input.onchange = async function(e) { + var file = e.target.files[0]; + if (!file) return; + var reader = new FileReader(); + reader.onload = async function(evt) { + var basePath = currentPath.replace(/\/+$/, ''); + var remotePath = basePath + '/' + file.name; + var parentDir = remotePath.split('/').slice(0, -1).join('/'); + try { + await self._ensureDir(parentDir); + var resp = await self._webdavRequest('PUT', remotePath, { + body: evt.target.result, + headers: { 'Content-Type': 'application/octet-stream' } + }); + if (resp.ok || resp.status === 201 || resp.status === 204) { + self.terminal.print('⬆ Uploaded: ' + file.name + ' (' + self._formatSize(file.size) + ')', 'success'); + self.openWebdavBrowser(currentPath); + } else { + self.terminal.print('Upload failed: HTTP ' + resp.status, 'error'); + } + } catch (e) { + self.terminal.print('Upload error: ' + e.message, 'error'); + } + }; + reader.readAsText(file); + }; + input.click(); + } + + async downloadFromWebdav(entry) { + try { + var resp = await this._webdavRequest('GET', entry.href); + if (!resp.ok) { this.terminal.print('Download failed: HTTP ' + resp.status, 'error'); return; } + var content = await resp.text(); + var blob = new Blob([content], { type: 'application/octet-stream' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = entry.name; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + setTimeout(function() { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 200); + this.terminal.print('⬇ Downloaded: ' + entry.name + ' (' + this._formatSize(content.length) + ')', 'success'); + } catch (e) { + this.terminal.print('Download error: ' + e.message, 'error'); + } + } + _formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; diff --git a/skills/test.md b/skills/test.md new file mode 100644 index 0000000..cefd788 --- /dev/null +++ b/skills/test.md @@ -0,0 +1 @@ +# Hello from WebDAV
\ No newline at end of file diff --git a/skills/webx11-dev-cycle.md b/skills/webx11-dev-cycle.md new file mode 100644 index 0000000..66fefde --- /dev/null +++ b/skills/webx11-dev-cycle.md @@ -0,0 +1,12 @@ +--- +name: webx11-dev-cycle +description: Professional workflow for analyzing, prototyping, and implementing new features in WebX11. +steps: + 1. Research: Use web_search and web_fetch to find similar implementations or documentation. + 2. Audit: Use view_module or run_command to examine current system state. + 3. Prototype: Use edit_module to implement a change in a sandbox environment. + 4. Verify: Use test_module and run_command to validate the change. + 5. Document: Save the final implementation and a a changelog in /webx11_dev/logs/. + 6. Persist: Save the session using session_save. +tags: development,workflow,webx11 +--- |