diff --git a/addons-host.js b/addons-host.js index 6dec2b70..352d031e 100644 --- a/addons-host.js +++ b/addons-host.js @@ -282,7 +282,7 @@ function validateManifest(raw, folderName) { // Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest // of the app queries via `getActive()` / `getInstalled()`. class AddonHost { - constructor({ addonsDir, dataDir, isDisabled, activationFor, onActivated, logger, setSessionProxy, vaultDerive, vaultImports, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, openSettings, captureTab, saveCapture, scanTabForUris, checkAndStageUpdates, restartApp, revealSidebar, requestFilter, tabs }) { + constructor({ addonsDir, dataDir, isDisabled, activationFor, onActivated, logger, setSessionProxy, vaultDerive, vaultImports, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, openSettings, captureTab, saveCapture, scanTabForUris, checkAndStageUpdates, restartApp, applySelfUpdate, revealSidebar, requestFilter, tabs }) { this.addonsDir = addonsDir; this.dataDir = dataDir; this.isDisabled = isDisabled || (() => false); @@ -351,6 +351,7 @@ class AddonHost { // add-on ever gets to hand-write into its own installed folder. this._checkAndStageUpdates = typeof checkAndStageUpdates === "function" ? checkAndStageUpdates : null; this._restartApp = typeof restartApp === "function" ? restartApp : null; + this._applySelfUpdate = typeof applySelfUpdate === "function" ? applySelfUpdate : null; // revealSidebar: opens the sidebar and switches to the given panel id. // Signature: (addonId, panelId) => void. Add-ons use this from their // context-menu handler to surface a result in their sidebar UI. @@ -837,6 +838,13 @@ class AddonHost { staged: (full?.staged || []).find((s) => s.id === manifest.id) || null, }; }, + // Apply this add-on's staged update now, without a restart: Theseus + // swaps it in, restarts the add-on and reloads its open panels and + // tabs. Plug-ins (wallets) still update on the next launch. + applySelfUpdate: async () => { + if (!this._applySelfUpdate) throw new Error("applySelfUpdate unavailable (host not wired)"); + return this._applySelfUpdate(manifest.id); + }, // Ask Theseus to relaunch (the plug-in card's "apply update" chip). // The host asks the user first and resolves { restarted, deferred } // — an add-on cannot restart the browser on its own. diff --git a/main.js b/main.js index 33601ba6..f21c0b51 100644 --- a/main.js +++ b/main.js @@ -2746,6 +2746,15 @@ function initAddons() { // update) but never gets to perform one: the user is asked first, in a // native dialog the panel cannot draw over. Declining costs nothing — // a staged update applies on the next normal launch anyway. + // An add-on applying its own staged update in place (no restart). Runs a + // moment later so the add-on's own call can return before the host is + // rebuilt underneath it. + applySelfUpdate: (addonId) => { + if (isPluginAddon(addonId)) return { ok: false, reason: "plug-ins update on the next launch" }; + if (!listStagedAddons(addonsStagedDir()).some((s) => s.id === addonId)) return { ok: false, reason: "nothing staged" }; + setTimeout(() => applyStagedNow(addonId), 250); + return { ok: true, scheduled: true }; + }, restartApp: async (addonName) => { const who = String(addonName || "An add-on").slice(0, 60); console.log(`[restart] ${who} asked to relaunch Theseus`); @@ -5924,9 +5933,9 @@ ipcMain.handle("addons-check-updates", () => pollAddonUpdates("manual")); // launch — a wallet mid-session is not something to hot-swap. `id` limits // the apply to one extension (Settings row button); omitted = every // staged extension (toolbar chip). -ipcMain.handle("addons-apply-staged", (_e, id) => { +ipcMain.handle("addons-apply-staged", (_e, id) => applyStagedNow(typeof id === "string" && id ? id : null)); +function applyStagedNow(want) { if (!addonHost) return { ok: false, error: "add-ons not ready", applied: [] }; - const want = typeof id === "string" && id ? id : null; const applied = addonUpdater.promoteStagedUpdates({ addonsDir: addonsUserDir(), backupsDir: addonsBackupDir(), @@ -5934,10 +5943,36 @@ ipcMain.handle("addons-apply-staged", (_e, id) => { logger: (...a) => console.log("[addons]", ...a), only: (m) => (want ? m.id === want : true) && !isPluginAddon(m.id), }); - if (applied.length) { addonHost.discoverAndActivate(); emitSidebarState(); } + if (applied.length) { + addonHost.discoverAndActivate(); + emitSidebarState(); + reloadAddonViews(applied.map((a) => a.id || a)); + } notifyStagedAddons(); return { ok: true, applied, pending: listStagedAddons(addonsStagedDir()).map((s) => ({ id: s.id, version: s.version })) }; -}); +} +// After an update is applied in place, an open panel or tab of that add-on +// still runs the previous version's page (it was loaded before the swap), +// so it shows the old UI until Theseus restarts. Reload them from disk. +function reloadAddonViews(ids) { + const mine = (panelId) => panelId && ids.some((id) => String(panelId).startsWith(`${id}:`)); + try { + if (mine(leftPanelLoadedId)) { + const open = leftPanelId; + leftPanelLoadedId = null; + if (open && mine(open)) openLeftPanel(open); + } + } catch (e) { console.warn("[addons] left panel reload failed:", e?.message); } + try { + if (mine(sidebarActivePanelId) && sidebar) { + const p = addonHost.getSidebarPanels().find((x) => x.panelId === sidebarActivePanelId); + if (p) sidebar.webContents.loadFile(p.pageFile); + } + } catch (e) { console.warn("[addons] sidebar reload failed:", e?.message); } + for (const t of tabs) { + try { if (t.addonId && ids.includes(t.addonId)) t.view.webContents.reload(); } catch {} + } +} ipcMain.handle("addons-list-staged", () => listStagedAddons(addonsStagedDir())); function listStagedAddons(stagedDir) { const out = [];