aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/core-bundle.js35
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-system.js14
-rw-r--r--modules/core/virtual-fs.js21
-rw-r--r--modules/ext/course.js5
-rw-r--r--modules/ext/critical-ext-bundle.js13
-rw-r--r--modules/ext/filemgr.js13
7 files changed, 61 insertions, 42 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 025572c..f275a1f 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -615,20 +615,13 @@ class VirtualFileSystem {
this.root.content['.trash'] = { type: 'dir', content: {} };
}
const trash = this.root.content['.trash'];
- const flatten = (n, prefix) => {
- if (n.type === 'file') {
- const flatName = prefix + '-' + name;
- let ctr = 0;
- let unique = flatName;
- while (trash.content[unique]) { unique = flatName + '-' + (++ctr); }
- trash.content[unique] = JSON.parse(JSON.stringify(n));
- } else if (n.type === 'dir') {
- for (const [childName, child] of Object.entries(n.content || {})) {
- flatten(child, prefix + '-' + childName);
- }
- }
- };
- flatten(node, ts + '-dir-' + name);
+ // Keep the subtree intact so it can be restored later; the old flatten loop
+ // turned every descendant into a sibling file and lost empty dirs entirely.
+ const baseName = ts + '-dir-' + name;
+ let unique = baseName;
+ let ctr = 0;
+ while (trash.content[unique]) { unique = baseName + '-' + (++ctr); }
+ trash.content[unique] = JSON.parse(JSON.stringify(node));
delete resolved.dir.content[resolved.name];
this.saveToStorage();
this._emitChange(path);
@@ -5758,8 +5751,18 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
errors: [],
};
try {
- const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
- const list = Array.isArray(raw) ? raw : (raw.errors || []);
+ // Prefer the LIVE error DB: localStorage writes are debounced, so a report
+ // filed right after an error would otherwise carry an empty list.
+ let list = [];
+ try {
+ const ds = Object.values(this.terminal.moduleManager.modules)
+ .find(m => m && m._errorDB && m._errorDB.errors);
+ if (ds) list = Object.values(ds._errorDB.errors);
+ } catch (_e) {}
+ if (!list.length) {
+ const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
+ list = Array.isArray(raw) ? raw : Object.values((raw && raw.errors) || {});
+ }
diag.errors = list.slice(-20).map(e => ({
message: String(e.message || e.msg || '').slice(0, 300),
source: String(e.source || e.url || '').slice(0, 200),
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index f9b4c9b..0913d6a 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.38';
+ const LATEST_VER = 'v4.6.40';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index 1a9ad05..bedceef 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -318,8 +318,18 @@
errors: [],
};
try {
- const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
- const list = Array.isArray(raw) ? raw : (raw.errors || []);
+ // Prefer the LIVE error DB: localStorage writes are debounced, so a report
+ // filed right after an error would otherwise carry an empty list.
+ let list = [];
+ try {
+ const ds = Object.values(this.terminal.moduleManager.modules)
+ .find(m => m && m._errorDB && m._errorDB.errors);
+ if (ds) list = Object.values(ds._errorDB.errors);
+ } catch (_e) {}
+ if (!list.length) {
+ const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
+ list = Array.isArray(raw) ? raw : Object.values((raw && raw.errors) || {});
+ }
diag.errors = list.slice(-20).map(e => ({
message: String(e.message || e.msg || '').slice(0, 300),
source: String(e.source || e.url || '').slice(0, 200),
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index d8b5098..8f11d25 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -613,20 +613,13 @@ class VirtualFileSystem {
this.root.content['.trash'] = { type: 'dir', content: {} };
}
const trash = this.root.content['.trash'];
- const flatten = (n, prefix) => {
- if (n.type === 'file') {
- const flatName = prefix + '-' + name;
- let ctr = 0;
- let unique = flatName;
- while (trash.content[unique]) { unique = flatName + '-' + (++ctr); }
- trash.content[unique] = JSON.parse(JSON.stringify(n));
- } else if (n.type === 'dir') {
- for (const [childName, child] of Object.entries(n.content || {})) {
- flatten(child, prefix + '-' + childName);
- }
- }
- };
- flatten(node, ts + '-dir-' + name);
+ // Keep the subtree intact so it can be restored later; the old flatten loop
+ // turned every descendant into a sibling file and lost empty dirs entirely.
+ const baseName = ts + '-dir-' + name;
+ let unique = baseName;
+ let ctr = 0;
+ while (trash.content[unique]) { unique = baseName + '-' + (++ctr); }
+ trash.content[unique] = JSON.parse(JSON.stringify(node));
delete resolved.dir.content[resolved.name];
this.saveToStorage();
this._emitChange(path);
diff --git a/modules/ext/course.js b/modules/ext/course.js
index d3b96de..8c615ec 100644
--- a/modules/ext/course.js
+++ b/modules/ext/course.js
@@ -177,7 +177,10 @@ class CourseModule {
if (cmd === 'course') return;
// Only capture while an exercise is actually running. Capturing every command
// swallowed all terminal output (pwd/help/version printed nothing).
- if (!this._active || !this._active.lesson || !this._active.lesson.exercise) return;
+ if (!this._active || !this._active.lesson || !this._active.lesson.exercise) {
+ this._capOut = ''; // never let a previous command's output satisfy an output-check exercise
+ return;
+ }
if (this._capActive) {
try { this.terminal._popPrintCapture(); } catch (e) {}
if (this._capOut) { try { this.terminal.print(this._capOut.replace(/\n+$/, '')); } catch (e) {} }
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
index 504dfa8..8f63081 100644
--- a/modules/ext/critical-ext-bundle.js
+++ b/modules/ext/critical-ext-bundle.js
@@ -1737,6 +1737,10 @@ class FileManagerModule {
const name = src.split('/').pop();
const destPath = dest === '/' ? '/' + name : dest + '/' + name;
if (destPath === src || destPath.startsWith(src + '/')) { skipped++; return; }
+ // Directory → existing directory would REPLACE the whole subtree silently; skip it.
+ let srcIsDir = false;
+ try { srcIsDir = typeof fs.readDir === 'function' && fs.readDir(src) !== null; } catch (_e) {}
+ if (this._exists(destPath) && srcIsDir) { skipped++; return; }
if (this._exists(destPath) && !confirm('Overwrite ' + destPath + '?')) { skipped++; return; }
try {
const ok = this._clipboard.mode === 'copy' ? fs.copy(src, destPath) : fs.move(src, destPath);
@@ -1754,8 +1758,8 @@ class FileManagerModule {
_batchDelete() {
const paths = this._getSelectedPaths();
- if (!paths.length) { this._updateStatus('No files selected'); return; }
- if (!confirm('Delete ' + paths.length + ' item(s)?')) return;
+ if (!paths.length) { this._updateStatus('No files selected'); return false; }
+ if (!confirm('Delete ' + paths.length + ' item(s)?')) return false;
const fs = this.terminal.fs;
let count = 0, failed = 0;
paths.forEach(p => {
@@ -1768,6 +1772,7 @@ class FileManagerModule {
this._selectedFile = null;
this._refreshFileList();
this._updateStatus('🗑️ Deleted ' + count + ' item(s)' + (failed ? ' — ' + failed + ' failed' : ''));
+ return count > 0;
}
_batchRename() {
@@ -1880,8 +1885,8 @@ class FileManagerModule {
if (!this._getSelectedPaths().length && this._selectedFile) {
this._multiSelect.add(this._selectedFile);
}
- this._batchDelete();
- this._clearPreview();
+ // Only clear the preview when something was actually deleted (not on cancel).
+ if (this._batchDelete()) this._clearPreview();
}
_actionDownload() {
diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js
index 28c62ce..e099a34 100644
--- a/modules/ext/filemgr.js
+++ b/modules/ext/filemgr.js
@@ -804,6 +804,10 @@ class FileManagerModule {
const name = src.split('/').pop();
const destPath = dest === '/' ? '/' + name : dest + '/' + name;
if (destPath === src || destPath.startsWith(src + '/')) { skipped++; return; }
+ // Directory → existing directory would REPLACE the whole subtree silently; skip it.
+ let srcIsDir = false;
+ try { srcIsDir = typeof fs.readDir === 'function' && fs.readDir(src) !== null; } catch (_e) {}
+ if (this._exists(destPath) && srcIsDir) { skipped++; return; }
if (this._exists(destPath) && !confirm('Overwrite ' + destPath + '?')) { skipped++; return; }
try {
const ok = this._clipboard.mode === 'copy' ? fs.copy(src, destPath) : fs.move(src, destPath);
@@ -821,8 +825,8 @@ class FileManagerModule {
_batchDelete() {
const paths = this._getSelectedPaths();
- if (!paths.length) { this._updateStatus('No files selected'); return; }
- if (!confirm('Delete ' + paths.length + ' item(s)?')) return;
+ if (!paths.length) { this._updateStatus('No files selected'); return false; }
+ if (!confirm('Delete ' + paths.length + ' item(s)?')) return false;
const fs = this.terminal.fs;
let count = 0, failed = 0;
paths.forEach(p => {
@@ -835,6 +839,7 @@ class FileManagerModule {
this._selectedFile = null;
this._refreshFileList();
this._updateStatus('🗑️ Deleted ' + count + ' item(s)' + (failed ? ' — ' + failed + ' failed' : ''));
+ return count > 0;
}
_batchRename() {
@@ -947,8 +952,8 @@ class FileManagerModule {
if (!this._getSelectedPaths().length && this._selectedFile) {
this._multiSelect.add(this._selectedFile);
}
- this._batchDelete();
- this._clearPreview();
+ // Only clear the preview when something was actually deleted (not on cancel).
+ if (this._batchDelete()) this._clearPreview();
}
_actionDownload() {