From 3106abce964a67d895686ed563775c48b46a76a5 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:28:16 +0200 Subject: [PATCH] Preserve surface windows across refresh Restore restorable surface modals through the initFw_end extension hook and keep right-canvas state in session storage for normal reloads. Fix Desktop host visibility after modal cleanup so preserved iframes reattach to real modal or canvas hosts, and remove the duplicate file-browser path icon overlay. --- extensions/webui/initFw_end/AGENTS.md | 2 +- .../initFw_end/restoreRestorableModals.js | 5 + plugins/_desktop/AGENTS.md | 1 + plugins/_desktop/webui/desktop-store.js | 11 +- webui/components/canvas/AGENTS.md | 1 + webui/components/canvas/right-canvas-store.js | 105 ++++++++++++++++-- .../modals/file-browser/file-browser.html | 12 +- webui/js/AGENTS.md | 1 + webui/js/modals.js | 89 +++++++++++++++ webui/js/surfaces.js | 5 +- 10 files changed, 207 insertions(+), 25 deletions(-) create mode 100644 extensions/webui/initFw_end/restoreRestorableModals.js diff --git a/extensions/webui/initFw_end/AGENTS.md b/extensions/webui/initFw_end/AGENTS.md index 5f18507b4..7390427de 100644 --- a/extensions/webui/initFw_end/AGENTS.md +++ b/extensions/webui/initFw_end/AGENTS.md @@ -6,7 +6,7 @@ ## Ownership -- JavaScript files own post-bootstrap global setup such as self-update helpers. +- JavaScript files own post-bootstrap global setup such as self-update helpers and session-scoped UI restoration hooks. ## Local Contracts diff --git a/extensions/webui/initFw_end/restoreRestorableModals.js b/extensions/webui/initFw_end/restoreRestorableModals.js new file mode 100644 index 000000000..51462c2bf --- /dev/null +++ b/extensions/webui/initFw_end/restoreRestorableModals.js @@ -0,0 +1,5 @@ +import { restoreRestorableModalStack } from "/js/modals.js"; + +export default function restoreRestorableModals() { + restoreRestorableModalStack(); +} diff --git a/plugins/_desktop/AGENTS.md b/plugins/_desktop/AGENTS.md index e7bdef438..5fc6f449c 100644 --- a/plugins/_desktop/AGENTS.md +++ b/plugins/_desktop/AGENTS.md @@ -16,6 +16,7 @@ - Preserve session startup, cleanup, and route protection for desktop access. - Keep desktop state injected into prompts accurate and bounded. - Do not expose desktop routes without the expected auth protections. +- Keep Desktop host visibility tied to an attached modal or canvas host; modal cleanup may preserve the iframe in keepalive, but must not leave stale modal mode behind. - Keep Markdown and plain text file open-with handling routed to the Editor surface through the desktop intent bridge; Desktop owns the Xfce launcher/MIME setup, while Editor owns `.md` and `.txt` editing. ## Work Guidance diff --git a/plugins/_desktop/webui/desktop-store.js b/plugins/_desktop/webui/desktop-store.js index 9c793da90..83f4cc086 100644 --- a/plugins/_desktop/webui/desktop-store.js +++ b/plugins/_desktop/webui/desktop-store.js @@ -311,6 +311,7 @@ const model = { }, cleanup() { + const wasModal = this._mode === "modal"; this.flushInput(); this.stopDesktopMonitor(); this.stopDesktopResizeObserver(); @@ -321,7 +322,11 @@ const model = { if (!this._desktopIntentionalShutdown) this.moveDesktopFrameToKeepalive(); this._floatingCleanup?.(); this._floatingCleanup = null; - if (this._mode === "modal") this._root = null; + if (wasModal) { + this._root = null; + this._mode = "canvas"; + this._desktopHostVisible = false; + } }, async refresh() { @@ -1098,7 +1103,9 @@ const model = { }, isDesktopHostVisible() { - if (this._mode === "modal") return true; + if (this._mode === "modal") { + return Boolean(this._root?.isConnected && this._root.closest?.(".modal")); + } const surface = this._root?.closest?.('[data-surface-id="desktop"]'); return Boolean(surface?.classList?.contains("is-mounted") || surface?.classList?.contains("is-active")); }, diff --git a/webui/components/canvas/AGENTS.md b/webui/components/canvas/AGENTS.md index 77dec922a..1d5eca08e 100644 --- a/webui/components/canvas/AGENTS.md +++ b/webui/components/canvas/AGENTS.md @@ -16,6 +16,7 @@ - Preserve responsive layout and avoid overlapping the chat/sidebar shells. - Right-canvas rail and tab buttons are explicit canvas entry points above the mobile breakpoint; at mobile widths, keep the rail visible but route non-action surfaces into floating modals instead of the side canvas shell. - Keep the right-canvas rail and docked shell hidden while the welcome screen is active; non-action surface opens during welcome must route into floating/modal surfaces instead of docking beside the welcome screen. +- Preserve docked canvas open state across same-tab reloads with session-scoped state, but do not treat it as durable cross-session UI state. - In mobile mode, keep the rail below blocking modal layers and compact it on very narrow screens instead of letting it cover modal content. ## Work Guidance diff --git a/webui/components/canvas/right-canvas-store.js b/webui/components/canvas/right-canvas-store.js index 315eeabd1..e6b3b7989 100644 --- a/webui/components/canvas/right-canvas-store.js +++ b/webui/components/canvas/right-canvas-store.js @@ -13,6 +13,7 @@ import { } from "/js/surfaces.js"; const STORAGE_KEY = "a0.rightCanvas"; +const SESSION_STORAGE_KEY = "a0.rightCanvas.session"; const DEFAULT_WIDTH = 720; const MIN_WIDTH = 0; const DESKTOP_BREAKPOINT = 1200; @@ -32,6 +33,13 @@ function normalizeWidth(value, fallback = DEFAULT_WIDTH) { return Number.isFinite(width) ? Math.max(MIN_WIDTH, Math.round(width)) : fallback; } +function isReloadNavigation() { + const navigation = performance.getEntriesByType?.("navigation")?.[0]; + if (navigation?.type) return navigation.type === "reload"; + if (!performance.navigation) return false; + return performance.navigation?.type === performance.navigation?.TYPE_RELOAD; +} + const model = { surfaces: [], activeSurfaceId: "", @@ -46,11 +54,15 @@ const model = { _rootElement: null, _resizeCleanup: null, _lastPayloadBySurface: {}, + _restoreOpenRequested: false, + _restoreOpenTimer: null, + _restoringSurface: false, async init(element = null) { if (element) this._rootElement = element; if (this._initialized) { this.applyLayoutState(); + this.scheduleRestoreOpenSurface(); return; } @@ -70,6 +82,7 @@ const model = { await callJsExtensions("right_canvas_register_surfaces", this); this._registering = false; this.ensureActiveSurface(); + this.scheduleRestoreOpenSurface(); } }, @@ -127,6 +140,9 @@ const model = { return false; } + this._restoreOpenRequested = false; + this.cancelRestoreOpenSurface(); + if (surface.actionOnly) { try { await surface.open?.(payload || {}); @@ -226,6 +242,8 @@ const model = { }, async close() { + this._restoreOpenRequested = false; + this.cancelRestoreOpenSurface(); this.isOpen = false; this.persist(); this.applyLayoutState(); @@ -315,6 +333,9 @@ const model = { const openModal = globalThis.ensureModalOpen || globalThis.openModal; if (!openModal) return false; + this._restoreOpenRequested = false; + this.cancelRestoreOpenSurface(); + if (this.isOpen && this.activeSurfaceId === targetId) { this.isOpen = false; this.persist(); @@ -420,20 +441,58 @@ const model = { } }, - persist() { + cancelRestoreOpenSurface() { + if (!this._restoreOpenTimer) return; + globalThis.clearTimeout?.(this._restoreOpenTimer); + this._restoreOpenTimer = null; + }, + + scheduleRestoreOpenSurface() { + if (!this._restoreOpenRequested || this._restoreOpenTimer) return; + if (!this.shouldRender() || this.isMobileMode) return; + this._restoreOpenTimer = globalThis.setTimeout?.(() => { + this._restoreOpenTimer = null; + this.restoreOpenSurface(); + }, 0) || null; + }, + + async restoreOpenSurface() { + if (!this._restoreOpenRequested || this._restoringSurface) return false; + if (!this.shouldRender() || this.isMobileMode) { + return false; + } + + const targetId = normalizeSurfaceId(this.activeSurfaceId || this.panelSurfaces[0]?.id || ""); + if (!targetId || !this.getSurface(targetId)) { + return false; + } + + this._restoreOpenRequested = false; + this._restoringSurface = true; try { - localStorage.setItem( - STORAGE_KEY, - JSON.stringify({ - isOpen: this.isOpen, - activeSurfaceId: this.activeSurfaceId, - surfaceModes: this.surfaceModes, - width: this.width, - }), - ); + return await this.open(targetId, { source: "reload-restore" }); + } finally { + this._restoringSurface = false; + } + }, + + persist() { + const state = { + isOpen: this.isOpen, + activeSurfaceId: this.activeSurfaceId, + surfaceModes: this.surfaceModes, + width: this.width, + }; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (error) { console.warn("Could not persist right canvas state", error); } + try { + sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state)); + } catch (error) { + console.warn("Could not persist right canvas session state", error); + } }, restore() { @@ -452,6 +511,31 @@ const model = { } catch (error) { console.warn("Could not restore right canvas state", error); } + + if (isReloadNavigation()) { + try { + const savedSession = migratePersistedSurfaceState( + JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY) || "{}"), + ); + if (savedSession?.activeSurfaceId) this.activeSurfaceId = String(savedSession.activeSurfaceId); + if (savedSession?.surfaceModes) { + this.surfaceModes = Object.fromEntries( + Object.entries(savedSession.surfaceModes || {}).map(([surfaceId, mode]) => [ + surfaceId, + normalizeSurfaceMode(mode), + ]), + ); + } + if (Number.isFinite(Number(savedSession?.width))) this.width = Number(savedSession.width); + this._restoreOpenRequested = Boolean(savedSession?.isOpen && savedSession?.activeSurfaceId); + } catch (error) { + console.warn("Could not restore right canvas session state", error); + sessionStorage.removeItem(SESSION_STORAGE_KEY); + } + } else { + sessionStorage.removeItem(SESSION_STORAGE_KEY); + } + this.setWidth(this.width, { persist: false }); }, @@ -485,6 +569,7 @@ const model = { document.body.classList.toggle("right-canvas-open", this.isOpen && !this.isMobileMode && this.shouldRender()); document.body.classList.toggle("right-canvas-overlay-mode", this.isOverlayMode); document.body.classList.toggle("right-canvas-mobile-mode", this.isMobileMode); + this.scheduleRestoreOpenSurface(); }, widthStyle() { diff --git a/webui/components/modals/file-browser/file-browser.html b/webui/components/modals/file-browser/file-browser.html index c390caa30..25d4a11be 100644 --- a/webui/components/modals/file-browser/file-browser.html +++ b/webui/components/modals/file-browser/file-browser.html @@ -49,7 +49,6 @@