aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 17:56:17 +0100
committerHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 17:56:17 +0100
commit924779e5fd49daabc2056f17df91fcafbe6e956d (patch)
tree24ca047fef962a17ce976d7efc9636e13d7e0bb3
parent8013a6146533e2f043d386ad774b8c12f57add53 (diff)
fix(newos4): a mode the user picks now survives a reload (v4.6.164)
Found by the operator actually running /ui-selftest.html on his phone. The report said 6/6 PASS, and the interesting part was what happened next: pick "easy" from the picker, reload, and you are back in classic. Two defects stacked on top of each other, neither of which reported anything: 1. `apply()` on an unregistered mode starts a lazy load and returns false. When the mode registers, `registerMode()` re-applied it with a hardcoded `persist:false` — so the picker's choice was never written to `webx11_ui_mode` at all. The intent ("the user asked for this") was dropped the moment the mode file had to be fetched, which is the normal case for all six modes. 2. Even when the mode did persist, `webx11_ui_experimental` was never set — and that key is what the in-bundle gate reads before UIShellModule is registered. A stored mode behind a closed gate is a preference the product silently ignores. Fix: * `_pendingPersist` carries the caller's intent through the lazy path; `registerMode()` honours it instead of hardcoding false, and the load-completion apply already forwarded `o.persist`; * `_persistMode(want)` writes the mode AND, for a real (non-classic) mode, sets `webx11_ui_experimental='1'`, so the gate opens on the next boot. Both directions verified live on v4.6.164: * user pick (lazy path) -> webx11_ui_mode=easy + webx11_ui_experimental=1, then main.html with NO query boots easy (#easy-actions present, body wx11-ui-easy, picker suppressed); * shared link ?ui=chat -> mode applied for the session, _fromQuery=true, localStorage STILL null (a link must not become the recipient's preference — that is the shell's own rule and it survives). Also: /ui-selftest.html no longer prints a meaningless `heapMB: 9.5`. Chromium pins performance.memory.usedJSHeapSize at 10,000,000 without --enable-precise-memory-info, which a page cannot request, so the report now says the heap is not measurable in-page instead of inventing a number — the same fake-green that was fixed in probe-ui-crash.py. guard 7/7 rc=0, maintain OK, served core-bundle byte-identical to disk.
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js33
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-uishell.js33
4 files changed, 64 insertions, 10 deletions
diff --git a/main.html b/main.html
index fb7b3f3..2b6c267 100644
--- a/main.html
+++ b/main.html
@@ -1350,13 +1350,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.163"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.164"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.163"></script>
+<script src="modules/core/module-loader.js?v=v4.6.164"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.163"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.164"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 349c02b..a9a8de5 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -23468,6 +23468,7 @@ class UIShellModule {
this._pending = null; // mode requested before its ext module loaded
this._picker = null;
this._fromQuery = false;
+ this._pendingPersist = null; // was the pending apply a USER choice or a link?
this.key = 'webx11_ui_mode';
this.seenKey = 'webx11_ui_seen';
}
@@ -23527,6 +23528,23 @@ class UIShellModule {
localStorage.setItem(this.key, this.current);
}
+ /** Store a user's mode choice AND the opt-in it depends on.
+ *
+ * The gate lives in terminal.js and reads `webx11_ui_experimental` before UIShellModule is
+ * registered. Storing only `webx11_ui_mode` therefore saves a preference the gate cannot see:
+ * pick "easy" in the picker, reload, and you are back in classic with no error anywhere
+ * (measured on the operator's phone via /ui-selftest.html, 2026-11-12). Only a real user action
+ * reaches this method — links and pending restores pass persist:false.
+ */
+ _persistMode(want) {
+ try {
+ localStorage.setItem(this.key, want);
+ if (want && want !== 'classic' && this.lazyIds().indexOf(want) !== -1) {
+ localStorage.setItem('webx11_ui_experimental', '1');
+ }
+ } catch (e) { /* private mode / quota: a preference that cannot be stored is not fatal */ }
+ }
+
/* ---------- registry ---------- */
registerMode(spec) {
if (!spec || !spec.id || typeof spec.mount !== 'function') {
@@ -23536,7 +23554,13 @@ class UIShellModule {
this.modes[spec.id] = spec;
if (this._pending === spec.id) {
this._pending = null;
- this.apply(spec.id, { silent: true, persist: false });
+ // Honour the ORIGINAL intent: a picker click persists, a shared link does not. This path
+ // used to hardcode persist:false, so a mode chosen from the picker lived for the session
+ // and vanished on the next reload — the opt-in flag stayed off and nothing reported it
+ // (measured 2026-11-12 on the operator's phone).
+ const persist = this._pendingPersist === true;
+ this._pendingPersist = null;
+ this.apply(spec.id, { silent: true, persist: persist });
}
return true;
}
@@ -23568,6 +23592,9 @@ class UIShellModule {
return false;
}
this._pending = want;
+ // Carry the intent into registerMode(): without this, a picker click that has to wait for
+ // the mode file is applied with persist:false and the choice dies at the next reload.
+ this._pendingPersist = o.persist !== false;
if (this.terminal) this.toast('Loading ' + want + '…', 1500);
this._loadMode(want).then((ok) => {
// registerMode() applies the mode the moment it registers — which already bumped this
@@ -23586,7 +23613,7 @@ class UIShellModule {
// _syncChrome all key off data-ui-mode, so it must be set even when nothing changes.
document.body.dataset.uiMode = want;
document.body.classList.add('wx11-ui-' + want);
- if (o.persist !== false) localStorage.setItem(this.key, want);
+ if (o.persist !== false) this._persistMode(want);
return true;
}
@@ -23599,7 +23626,7 @@ class UIShellModule {
.filter((c) => c.indexOf('wx11-ui-') === 0 && c !== 'wx11-ui-' + want)
.forEach((c) => document.body.classList.remove(c));
document.body.classList.add('wx11-ui-' + want);
- if (o.persist !== false) localStorage.setItem(this.key, want);
+ if (o.persist !== false) this._persistMode(want);
// _syncChrome on EVERY switch: it hides #quick-actions, which otherwise stays in the tab order
// and the accessibility tree behind the opaque mode root (found by review).
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 754b532..d87b11f 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.163';
+ const LATEST_VER = 'v4.6.164';
const CACHE = '?v=' + LATEST_VER;
// Exposed so ext modules can register their own URL with the same cache-busting version
// instead of an unversioned path the browser may already have cached (shell.js did).
diff --git a/modules/core/module-uishell.js b/modules/core/module-uishell.js
index 94e9bd1..2817790 100644
--- a/modules/core/module-uishell.js
+++ b/modules/core/module-uishell.js
@@ -25,6 +25,7 @@ class UIShellModule {
this._pending = null; // mode requested before its ext module loaded
this._picker = null;
this._fromQuery = false;
+ this._pendingPersist = null; // was the pending apply a USER choice or a link?
this.key = 'webx11_ui_mode';
this.seenKey = 'webx11_ui_seen';
}
@@ -84,6 +85,23 @@ class UIShellModule {
localStorage.setItem(this.key, this.current);
}
+ /** Store a user's mode choice AND the opt-in it depends on.
+ *
+ * The gate lives in terminal.js and reads `webx11_ui_experimental` before UIShellModule is
+ * registered. Storing only `webx11_ui_mode` therefore saves a preference the gate cannot see:
+ * pick "easy" in the picker, reload, and you are back in classic with no error anywhere
+ * (measured on the operator's phone via /ui-selftest.html, 2026-11-12). Only a real user action
+ * reaches this method — links and pending restores pass persist:false.
+ */
+ _persistMode(want) {
+ try {
+ localStorage.setItem(this.key, want);
+ if (want && want !== 'classic' && this.lazyIds().indexOf(want) !== -1) {
+ localStorage.setItem('webx11_ui_experimental', '1');
+ }
+ } catch (e) { /* private mode / quota: a preference that cannot be stored is not fatal */ }
+ }
+
/* ---------- registry ---------- */
registerMode(spec) {
if (!spec || !spec.id || typeof spec.mount !== 'function') {
@@ -93,7 +111,13 @@ class UIShellModule {
this.modes[spec.id] = spec;
if (this._pending === spec.id) {
this._pending = null;
- this.apply(spec.id, { silent: true, persist: false });
+ // Honour the ORIGINAL intent: a picker click persists, a shared link does not. This path
+ // used to hardcode persist:false, so a mode chosen from the picker lived for the session
+ // and vanished on the next reload — the opt-in flag stayed off and nothing reported it
+ // (measured 2026-11-12 on the operator's phone).
+ const persist = this._pendingPersist === true;
+ this._pendingPersist = null;
+ this.apply(spec.id, { silent: true, persist: persist });
}
return true;
}
@@ -125,6 +149,9 @@ class UIShellModule {
return false;
}
this._pending = want;
+ // Carry the intent into registerMode(): without this, a picker click that has to wait for
+ // the mode file is applied with persist:false and the choice dies at the next reload.
+ this._pendingPersist = o.persist !== false;
if (this.terminal) this.toast('Loading ' + want + '…', 1500);
this._loadMode(want).then((ok) => {
// registerMode() applies the mode the moment it registers — which already bumped this
@@ -143,7 +170,7 @@ class UIShellModule {
// _syncChrome all key off data-ui-mode, so it must be set even when nothing changes.
document.body.dataset.uiMode = want;
document.body.classList.add('wx11-ui-' + want);
- if (o.persist !== false) localStorage.setItem(this.key, want);
+ if (o.persist !== false) this._persistMode(want);
return true;
}
@@ -156,7 +183,7 @@ class UIShellModule {
.filter((c) => c.indexOf('wx11-ui-') === 0 && c !== 'wx11-ui-' + want)
.forEach((c) => document.body.classList.remove(c));
document.body.classList.add('wx11-ui-' + want);
- if (o.persist !== false) localStorage.setItem(this.key, want);
+ if (o.persist !== false) this._persistMode(want);
// _syncChrome on EVERY switch: it hides #quick-actions, which otherwise stays in the tab order
// and the accessibility tree behind the opaque mode root (found by review).