Theseus: an extension update applied in place reloads its open panels

Applying an extension update without a restart swapped the files and
restarted the add-on, but a panel or tab that was already open kept
running the previous version's page, so the extension looked unchanged
until Theseus restarted (Pithos showed its old layout this way). The
add-on's open left panel, sidebar panel and tabs now reload after the
swap. Extensions can also apply their own staged update with
api.applySelfUpdate(), so their own update button needs no restart;
plug-ins such as wallets still update on the next launch.
This commit is contained in:
Local Dev 2026-10-04 15:28:25 +02:00
parent f3cd63134a
commit 99d7684590
2 changed files with 48 additions and 5 deletions

View file

@ -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.

43
main.js
View file

@ -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 = [];