aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--help.html2
-rw-r--r--main.html (renamed from index.html)0
-rw-r--r--modules/core/module-aiassist.js23
-rw-r--r--modules/core/terminal.js25
-rw-r--r--modules/core/virtual-fs.js21
-rw-r--r--modules/ext/webdav.js300
-rw-r--r--skills/test.md1
-rw-r--r--skills/webx11-dev-cycle.md12
8 files changed, 370 insertions, 14 deletions
diff --git a/help.html b/help.html
index 80c2e9c..8d30b4e 100644
--- a/help.html
+++ b/help.html
@@ -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/index.html b/main.html
index f1f315e..f1f315e 100644
--- a/index.html
+++ b/main.html
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
+---