Merge extensions-on-first-use: add-ons start when first used, not at launch
The left-edge panels landed meanwhile, so a panel record now carries both its side and replace-by-id; a manifest-declared panel may say side:left too, or a dormant add-on would show its panel on the wrong edge until it starts.
This commit is contained in:
commit
278da658ce
11 changed files with 511 additions and 35 deletions
18
GOTCHAS.md
18
GOTCHAS.md
|
|
@ -81,6 +81,24 @@ The same applies to `home-preload.js`, which is in *every* tab. Handlers
|
|||
behind it check `isHomePageSender` / `isErrorPageSender`, which compare
|
||||
the sender against the exact shipped file:// URL.
|
||||
|
||||
## An enabled add-on is not necessarily running
|
||||
|
||||
An add-on whose `addon.json` says `"activation": "on-demand"` is listed at
|
||||
launch but not started. Its declared `panels`, `toolbar-menu`,
|
||||
`context-menu-items` and `page-inject` bridge are live anyway, and
|
||||
`AddonHost.dispatch()` starts it on the first call (see `ensureActive` in
|
||||
[addons-host.js](addons-host.js)). So in main:
|
||||
|
||||
- Reach an add-on through `addonHost.dispatch(...)`, never by checking
|
||||
`isActive()` / `hasHandler()` first. Both are false for a waiting
|
||||
add-on, and the call would never wake it (`routeWizUri` shows the
|
||||
pattern for when you really need to check).
|
||||
- "Enabled" in the snapshot means running or waiting; `running` tells
|
||||
them apart.
|
||||
- Settings › Performance › "Start extensions when first used" off makes
|
||||
every add-on start at launch again, which is the quickest way to rule
|
||||
this out when an add-on "does nothing".
|
||||
|
||||
## The resolver in Theseus is `resolver-web.mjs`, not `.js`
|
||||
|
||||
Packaged builds ship `Argus/src/lib/resolver-web.js` as `resolver-web.mjs`
|
||||
|
|
|
|||
147
NOTE-aegis-on-demand.md
Normal file
147
NOTE-aegis-on-demand.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Hand-off: switching Aegis to on-demand activation
|
||||
|
||||
Theseus can now start an add-on on first use instead of at every launch
|
||||
(`"activation": "on-demand"` in `addon.json`, see [addons-host.js](addons-host.js)).
|
||||
The small bundled add-ons use it. **Aegis was deliberately left on startup**
|
||||
because another session owns `bundled-addons/aegis/*`. It is where most of
|
||||
the launch win is: measured 2026-10-03, Aegis costs ~165 ms of synchronous
|
||||
activation plus ~610 ms of main-thread work right after (`loadDeps`: noble
|
||||
curves, bitcoinjs, libauth, WizardConnect). This note lists what Aegis has to
|
||||
declare and handle to switch. It was written against Aegis 0.27.1
|
||||
(branch `claude/sleepy-maxwell-251ee6-b`). Nothing in the host needs to change.
|
||||
|
||||
## What the host already does for an on-demand add-on
|
||||
|
||||
- While the add-on waits, the host still serves everything it declares:
|
||||
`panels` (dock button + sidebar), `toolbar-menu`, `context-menu-items` and
|
||||
the `page-inject` bridge. The bridge source is read from disk the first
|
||||
time a matching page loads, so `window.bitcoincash`, `window.ethereum`,
|
||||
`window.solana`, `window.tronWeb`/`tronLink`, `window.wizardconnect` and
|
||||
the EIP-6963 announcements are all on the page **before** Aegis runs.
|
||||
- These start it, then deliver the call. A call never gets dropped: it waits
|
||||
for the activation, and parallel calls share one activation.
|
||||
- any `addon-page-msg` from the bridge (`getAddress`, `eth.*`, `sol.*`,
|
||||
`trx.*`, `wcPageReady`, `wcConnectFromPage`, …)
|
||||
- any `addon-msg` from `panel.html` or an Aegis full-tab page
|
||||
- opening the Aegis panel (`setSidebar` starts it alongside the page load)
|
||||
- a `wiz://` link click (`routeWizUri` in main.js wakes Aegis, then
|
||||
dispatches `wcConnectFromPage`)
|
||||
- Settings `addon-invoke` calls
|
||||
- **If `activate()` returns a promise, every call waits for it, up to
|
||||
5 s** (`READY_WAIT_MS`). After 5 s the call goes through anyway.
|
||||
- `api.startAtLaunch(true|false)` lets an add-on ask to be started at the next
|
||||
launch even though its manifest says on-demand. The host saves the choice
|
||||
in `settings.addonsStartAtLaunch`. Settings › Performance › "Start the
|
||||
wallet at launch" does the same thing from the user's side, for `aegis`
|
||||
only.
|
||||
- Settings › Performance › Startup already has the "Start the wallet at
|
||||
launch" switch. It is disabled, with a hint, until the installed Aegis
|
||||
manifest says `"activation": "on-demand"`. After that it works with no
|
||||
Settings change (default: off).
|
||||
|
||||
## What Aegis has to change
|
||||
|
||||
### 1. `addon.json`
|
||||
|
||||
```json
|
||||
"activation": "on-demand",
|
||||
"panels": [{ "id": "main", "title": "Wallet", "page": "panel.html" }],
|
||||
```
|
||||
|
||||
Leave `icon` out of the panel entry so it inherits the manifest icon, as
|
||||
today. The `api.registerSidebarPanel({ id: "main", … })` call in `activate()`
|
||||
can stay: the same id replaces the declared entry. Bump the version, or
|
||||
existing profiles keep the old manifest (`seedBundledAddons` only reseeds a
|
||||
strictly newer bundle).
|
||||
|
||||
### 2. `activate()` must return a "started" promise, and must not wait for the vault
|
||||
|
||||
Today `activate()` returns `undefined`. It registers every handler
|
||||
synchronously, then loads deps after `whenUiReady()`, then `tryAutoUnlock`,
|
||||
then `mountAllWallets()`. That was fine at launch, because dapps arrived
|
||||
seconds later. On demand, **the call that woke Aegis is the first thing it
|
||||
sees**. Right now that call would fail:
|
||||
|
||||
- `getAddress` / `signAndSend` / `signMessage` → `legacyBchRuntime()` throws
|
||||
"wallet is not ready (vault locked?)" because no runtime is mounted yet
|
||||
- `wcConnectFromPage` throws "WizardConnect is still starting up"
|
||||
- `wcPageReady` answers `{available:false}`, and the dapp falls back to a QR
|
||||
- `eth.*` / `sol.*` / `trx.*` see no runtimes
|
||||
|
||||
Fix: return a promise that resolves once Aegis can actually serve calls:
|
||||
|
||||
```js
|
||||
activate(api) {
|
||||
…register handlers exactly as today…
|
||||
const started = uiReady.then(() => loadDeps(api)).then(async (d) => {
|
||||
…c.d = d; c.wc = …; // as today
|
||||
await tryAutoUnlock(api);
|
||||
const st = await api.vault.lifecycle.status().catch(() => null);
|
||||
const mounting = mountAllWallets();
|
||||
if (st && st.unlocked) await mounting; // see below
|
||||
});
|
||||
started.catch(…); // as today
|
||||
return started;
|
||||
}
|
||||
```
|
||||
|
||||
**Vault-derive waits:** `api.vault.derive()` does not reject while the vault
|
||||
is locked. It polls until the user unlocks (main.js `vaultDerive`:
|
||||
`while (!vaultState) await …`). So with a locked vault,
|
||||
`mountAllWallets()` never resolves. **Never await it in the returned promise
|
||||
unless the vault is already unlocked.** The host would wait out the full
|
||||
5 s on every first call, and then deliver the call to a wallet that is
|
||||
still not ready. With a locked vault, resolve right after deps load. The
|
||||
existing locked-vault behaviour then applies unchanged: the panel shows its
|
||||
unlock gate, and page calls get "wallet is not ready (vault locked?)", same
|
||||
as a locked vault at launch today.
|
||||
|
||||
The ~610 ms `loadDeps` cost now lands on the first dapp call or panel open,
|
||||
not on launch. `whenUiReady()` has long resolved by then, so it runs at once.
|
||||
That is the intended trade, but it is visible: the first
|
||||
`eth_requestAccounts` approval appears about 0.6–1 s later than it does
|
||||
today.
|
||||
|
||||
### 3. Live WizardConnect pairings must keep Aegis at launch
|
||||
|
||||
`lib/wc.js` saves pairings under `wc/<walletId>/uris` and reconnects them to
|
||||
the Nostr relays in `mountWallet()`. While Aegis is waiting for first use,
|
||||
**nobody listens on those relays**. A paired dapp that sends a sign request
|
||||
gets no answer until something else wakes Aegis. Aegis must tell the host:
|
||||
|
||||
```js
|
||||
// after any change to the persisted pairing set (persist(), disconnect, removeWallet)
|
||||
const live = [...uris.values()].some((s) => s.size > 0);
|
||||
api.startAtLaunch(live);
|
||||
```
|
||||
|
||||
Call it once at startup as well, after the pairings are restored, so a
|
||||
profile that already has pairings flips to startup on the next launch. Guard
|
||||
it with `typeof api.startAtLaunch === "function"` for older hosts.
|
||||
|
||||
### 4. Other work that only runs while Aegis runs (decide, no fix required)
|
||||
|
||||
- Price feed polling (`prices.js`, opt-in) and per-chain adapter refresh /
|
||||
electrum subscriptions do not run until first use. Balances are fetched
|
||||
when the panel opens, which already happens today for a locked vault.
|
||||
- `tryAutoUnlock` (the safeStorage "stay unlocked" session) now runs at
|
||||
first use, not at launch. It unlocks the shared password vault, so
|
||||
Settings › Passwords also stays locked until then. If that matters, the
|
||||
"stay unlocked" option should also call `api.startAtLaunch(true)`.
|
||||
- `installStorageCache(api)` and `migrateLegacyStorage(api)` are cheap and
|
||||
just move to first use.
|
||||
|
||||
### 5. Check after switching
|
||||
|
||||
- Fresh profile, launch: the log shows `aegis v… starts on first use`, the
|
||||
dock shows the Wallet button, and `scripts/boot-trace/run.mjs` shows the
|
||||
`addons` column without Aegis.
|
||||
- On a dapp: `window.ethereum` exists at page load. The first
|
||||
`eth_requestAccounts` starts Aegis (`started aegis on first use (page
|
||||
eth.requestAccounts)`) and shows the approval.
|
||||
- With a locked vault: the first page call fails with the usual locked
|
||||
message right away, not after 5 s.
|
||||
- A `wiz://` link click on a cold Aegis opens the pairing approval.
|
||||
- After pairing a WC dapp, `settings.json` → `addonsStartAtLaunch` contains
|
||||
`aegis`. The next launch logs `activated aegis` and the paired dapp's
|
||||
requests arrive.
|
||||
244
addons-host.js
244
addons-host.js
|
|
@ -8,7 +8,11 @@
|
|||
// dev-mode browser extensions: the user is choosing to run local code with
|
||||
// the app's full privileges.
|
||||
//
|
||||
// Loading is synchronous at app-ready time; there is no hot-reload. Failed
|
||||
// Discovery is synchronous at app-ready time. An add-on whose manifest says
|
||||
// "activation": "startup" (the default) is activated right then; one that
|
||||
// says "on-demand" is only listed — its declared panels, toolbar menu,
|
||||
// context-menu items and page-inject bridge are live, and activate() runs
|
||||
// the first time one of them is actually used (see ensureActive). Failed
|
||||
// activations are logged and skipped without breaking the app.
|
||||
//
|
||||
// Persistence:
|
||||
|
|
@ -19,6 +23,10 @@ const fs = require("node:fs");
|
|||
const { storeFor } = require("./lib/addon-store.cjs");
|
||||
const path = require("node:path");
|
||||
|
||||
// How long a call waits for an async activate() to settle before it is
|
||||
// delivered anyway.
|
||||
const READY_WAIT_MS = 5000;
|
||||
|
||||
// Extension points the framework understands. Extending this list means also
|
||||
// teaching main.js and (typically) the chrome renderer about the new point.
|
||||
// Right now only sidebar panels are wired — future rev adds toolbar-chip,
|
||||
|
|
@ -212,19 +220,64 @@ function validateManifest(raw, folderName) {
|
|||
// dock: "hidden" — the add-on starts without a toolbar button (its settings
|
||||
// live in Settings); main honours it once, the user can show it later.
|
||||
const dock = m.dock === "hidden" ? "hidden" : undefined;
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, contextMenuItems, absorbs, category, dock };
|
||||
// panels: sidebar panels declared up front — [{id, title, icon?, page}].
|
||||
// The dock shows them before the add-on runs, and they are registered
|
||||
// for it at activation, so activate() does not have to call
|
||||
// registerSidebarPanel for them (calling it with the same id replaces
|
||||
// the declared entry).
|
||||
const panels = [];
|
||||
if (m.panels != null) {
|
||||
if (!Array.isArray(m.panels)) throw new Error(`addon "${id}": "panels" must be an array`);
|
||||
const seen = new Set();
|
||||
m.panels.forEach((p, idx) => {
|
||||
const pid = String(p && p.id || "").trim();
|
||||
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(pid)) throw new Error(`addon "${id}": panels[${idx}].id is required and must match [a-z0-9._-]`);
|
||||
if (seen.has(pid)) throw new Error(`addon "${id}": panels[${idx}].id "${pid}" duplicates an earlier entry`);
|
||||
seen.add(pid);
|
||||
const page = String(p.page || "").replace(/^[\\/]+/, "");
|
||||
if (!page || page.includes("..") || path.isAbsolute(page)) throw new Error(`addon "${id}": panels[${idx}].page must be a relative path inside the addon folder`);
|
||||
panels.push({ id: pid, title: String(p.title || name), icon: p.icon == null ? icon : String(p.icon), page, side: p.side === "left" ? "left" : "right" });
|
||||
});
|
||||
}
|
||||
// activation: "startup" runs activate() at launch; "on-demand" waits for
|
||||
// the first use of something the manifest declares. Startup is the
|
||||
// default because the host cannot know what an add-on that predates this
|
||||
// field does in activate() — most register their panels there, and some
|
||||
// start work that must not wait (timers, a proxy, a filter). Two cases
|
||||
// are forced back to startup: a request filter has to see the first
|
||||
// request, and an add-on that declares nothing could never be woken.
|
||||
let activation = m.activation === "on-demand" ? "on-demand" : "startup";
|
||||
let activationNote = "";
|
||||
if (activation === "on-demand") {
|
||||
if (capabilities.includes("request-filter")) { activation = "startup"; activationNote = "request-filter add-ons start at launch"; }
|
||||
else if (!panels.length && !toolbarMenu && !contextMenuItems.length && !pageInject) { activation = "startup"; activationNote = "declares nothing that could start it"; }
|
||||
}
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, contextMenuItems, absorbs, category, dock, panels, activation, activationNote };
|
||||
}
|
||||
|
||||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||
// of the app queries via `getActive()` / `getInstalled()`.
|
||||
class AddonHost {
|
||||
constructor({ addonsDir, dataDir, isDisabled, 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, revealSidebar, requestFilter, tabs }) {
|
||||
this.addonsDir = addonsDir;
|
||||
this.dataDir = dataDir;
|
||||
this.isDisabled = isDisabled || (() => false);
|
||||
this.log = logger || ((...a) => console.log("[addons]", ...a));
|
||||
this._installed = []; // [{ manifest, folder, error? }]
|
||||
this._active = new Map(); // id -> { manifest, folder, exports, sidebarPanels: [...], handlers: Map, inject }
|
||||
this._active = new Map(); // id -> { manifest, folder, exports, sidebarPanels: [...], handlers: Map, inject, ready }
|
||||
// On-demand add-ons that are enabled but not started yet:
|
||||
// id -> { manifest, folder, panels, injectSource }.
|
||||
this._dormant = new Map();
|
||||
this._activating = new Map(); // id -> Promise of a first-use activation
|
||||
// First-use activations run one at a time, in the order they were asked for.
|
||||
this._queue = Promise.resolve();
|
||||
// activationFor(manifest) -> "startup" | "on-demand": main's settings may
|
||||
// override the manifest (Settings › Performance › Startup).
|
||||
this._activationFor = typeof activationFor === "function" ? activationFor : (m) => m.activation;
|
||||
this._onActivated = typeof onActivated === "function" ? onActivated : null;
|
||||
// setStartAtLaunch(id, on): backs api.startAtLaunch; main persists it and
|
||||
// folds it into activationFor.
|
||||
this._setStartAtLaunch = typeof arguments[0].setStartAtLaunch === "function" ? arguments[0].setStartAtLaunch : null;
|
||||
// Capability hooks injected by main. Each is (args..., addonId) so main
|
||||
// can log/gate per add-on. Missing hook = capability unavailable.
|
||||
this._vaultDerive = typeof vaultDerive === "function" ? vaultDerive : null;
|
||||
|
|
@ -340,8 +393,14 @@ class AddonHost {
|
|||
|
||||
discoverAndActivate() {
|
||||
this.ensureDirs();
|
||||
// A re-discover (toggle, reload, hot-applied update) restarts whatever
|
||||
// was running, on-demand or not: its panel or tab may be open and
|
||||
// listening for api.emit, and nothing would wake it again until that
|
||||
// page next called in.
|
||||
const wasRunning = new Set(this._active.keys());
|
||||
this._deactivateAll();
|
||||
this._installed = [];
|
||||
this._dormant.clear();
|
||||
let entries = [];
|
||||
try { entries = fs.readdirSync(this.addonsDir, { withFileTypes: true }); } catch { entries = []; }
|
||||
for (const dirent of entries) {
|
||||
|
|
@ -354,6 +413,14 @@ class AddonHost {
|
|||
this.log(`skipping disabled add-on ${manifest.id}`);
|
||||
continue;
|
||||
}
|
||||
if (manifest.activationNote) this.log(`${manifest.id}: starts at launch (${manifest.activationNote})`);
|
||||
if (!wasRunning.has(manifest.id) && this._activationFor(manifest) === "on-demand") {
|
||||
if (this._active.has(manifest.id) || this._dormant.has(manifest.id)) throw new Error(`duplicate add-on id "${manifest.id}"`);
|
||||
this._dormant.set(manifest.id, { manifest, folder, panels: this._resolvePanels(manifest, folder), injectSource: null });
|
||||
this.log(`${manifest.id} v${manifest.version} starts on first use`);
|
||||
continue;
|
||||
}
|
||||
if (this._dormant.has(manifest.id)) throw new Error(`duplicate add-on id "${manifest.id}"`);
|
||||
this._activateOne(manifest, folder);
|
||||
} catch (e) {
|
||||
this.log(`failed to load ${dirent.name}: ${e?.message || e}`);
|
||||
|
|
@ -373,6 +440,61 @@ class AddonHost {
|
|||
return validateManifest(raw, folderName);
|
||||
}
|
||||
|
||||
// Declared panels → the records getSidebarPanels hands out. A declared
|
||||
// page that is missing fails the add-on's load, same as a bad manifest.
|
||||
_resolvePanels(manifest, folder) {
|
||||
return (manifest.panels || []).map((p) => {
|
||||
const abs = path.join(folder, p.page);
|
||||
if (!fs.existsSync(abs)) throw new Error(`sidebar panel page not found: ${abs}`);
|
||||
return { panelId: `${manifest.id}:${p.id}`, title: p.title, icon: p.icon, pageFile: abs, addonId: manifest.id, side: p.side || "right" };
|
||||
});
|
||||
}
|
||||
|
||||
// Start an on-demand add-on now, if it isn't running yet. Resolves once
|
||||
// activate() has run and, when it returned a promise, that promise has
|
||||
// settled (bounded by READY_WAIT_MS) — so a call that woke the add-on
|
||||
// finds the handlers it registers. Callers arriving while an activation
|
||||
// is under way share its promise. Activations run one at a time.
|
||||
ensureActive(id, reason = "") {
|
||||
const running = this._active.get(id);
|
||||
if (running) return running.ready || Promise.resolve();
|
||||
const pending = this._activating.get(id);
|
||||
if (pending) return pending;
|
||||
const d = this._dormant.get(id);
|
||||
if (!d) return Promise.reject(new Error(`add-on "${id}" is not active`));
|
||||
const p = this._queue.then(() => {
|
||||
const now = this._active.get(id);
|
||||
if (now) return now.ready;
|
||||
// A re-discover while this waited in the queue replaced the entry.
|
||||
if (this._dormant.get(id) !== d) throw new Error(`add-on "${id}" is no longer installed`);
|
||||
this._dormant.delete(id);
|
||||
const t0 = Date.now();
|
||||
try { this._activateOne(d.manifest, d.folder); }
|
||||
catch (e) {
|
||||
const msg = String(e?.message || e);
|
||||
this.log(`failed to start ${id}: ${msg}`);
|
||||
const i = this._installed.findIndex((x) => x.folder === d.folder);
|
||||
if (i >= 0) this._installed[i] = { manifest: null, folder: d.folder, error: msg };
|
||||
try { if (this._onActivated) this._onActivated(id, false); } catch {}
|
||||
throw e;
|
||||
}
|
||||
this.log(`started ${id} on first use${reason ? ` (${reason})` : ""} in ${Date.now() - t0} ms`);
|
||||
try { if (this._onActivated) this._onActivated(id, true); } catch {}
|
||||
return this._active.get(id).ready;
|
||||
}).finally(() => { if (this._activating.get(id) === p) this._activating.delete(id); });
|
||||
this._activating.set(id, p);
|
||||
this._queue = p.catch(() => {});
|
||||
return p;
|
||||
}
|
||||
// Start every add-on still waiting for first use (the setting that defers
|
||||
// them was switched off).
|
||||
activateAllDormant(reason = "") {
|
||||
return Promise.all([...this._dormant.keys()].map((id) => this.ensureActive(id, reason).catch(() => {})));
|
||||
}
|
||||
isDormant(id) { return this._dormant.has(id); }
|
||||
// Enabled = running or waiting for first use.
|
||||
isEnabled(id) { return this._active.has(id) || this._dormant.has(id); }
|
||||
|
||||
_activateOne(manifest, folder) {
|
||||
const mainPath = path.join(folder, manifest.main);
|
||||
if (this._active.has(manifest.id)) {
|
||||
|
|
@ -400,7 +522,8 @@ class AddonHost {
|
|||
if (!mod || typeof mod.activate !== "function") {
|
||||
throw new Error(`main file must export an activate(api) function`);
|
||||
}
|
||||
const active = { manifest, folder, exports: mod, sidebarPanels: [], handlers: new Map(), inject: null, tabListeners: [] };
|
||||
// Declared panels are registered up front; activate() may still add more.
|
||||
const active = { manifest, folder, exports: mod, sidebarPanels: this._resolvePanels(manifest, folder), handlers: new Map(), inject: null, tabListeners: [], ready: Promise.resolve() };
|
||||
if (manifest.pageInject) {
|
||||
// Read the inject source once at activation. It's shipped to every
|
||||
// matching tab's preload verbatim, so a syntax error surfaces in the
|
||||
|
|
@ -420,7 +543,17 @@ class AddonHost {
|
|||
};
|
||||
try {
|
||||
const r = mod.activate(api);
|
||||
if (r && typeof r.then === "function") r.catch((e) => this.log(`[${manifest.id}] activate() rejected: ${e?.message || e}`));
|
||||
if (r && typeof r.then === "function") {
|
||||
r.catch((e) => this.log(`[${manifest.id}] activate() rejected: ${e?.message || e}`));
|
||||
// Calls wait for this (see dispatch) — bounded, because some
|
||||
// activations await things that can take forever (a vault the user
|
||||
// never unlocks).
|
||||
active.ready = new Promise((resolve) => {
|
||||
const t = setTimeout(resolve, READY_WAIT_MS);
|
||||
if (t.unref) t.unref();
|
||||
Promise.resolve(r).catch(() => {}).then(() => { clearTimeout(t); resolve(); });
|
||||
});
|
||||
}
|
||||
} catch (e) { cleanup(); throw new Error(`activate() threw: ${e?.message || e}`); }
|
||||
this._active.set(manifest.id, active);
|
||||
this.log(`activated ${manifest.id} v${manifest.version}`);
|
||||
|
|
@ -474,7 +607,9 @@ class AddonHost {
|
|||
if (!fs.existsSync(abs)) throw new Error(`sidebar panel page not found: ${abs}`);
|
||||
// Namespaced id so two add-ons can't collide.
|
||||
const panelId = `${manifest.id}:${id}`;
|
||||
active.sidebarPanels.push({ panelId, title, icon, pageFile: abs, addonId: manifest.id, side: side === "left" ? "left" : "right" });
|
||||
const rec = { panelId, title, icon, pageFile: abs, addonId: manifest.id, side: side === "left" ? "left" : "right" };
|
||||
const at = active.sidebarPanels.findIndex((p) => p.panelId === panelId);
|
||||
if (at >= 0) active.sidebarPanels[at] = rec; else active.sidebarPanels.push(rec);
|
||||
this.log(`[${manifest.id}] registered sidebar panel: ${panelId}`);
|
||||
},
|
||||
// Programmatically open the sidebar and switch to one of THIS add-on's
|
||||
|
|
@ -612,6 +747,13 @@ class AddonHost {
|
|||
if (!this._restartApp) throw new Error("restartApp unavailable (host not wired)");
|
||||
return Promise.resolve(this._restartApp(manifest.name || manifest.id));
|
||||
},
|
||||
// An on-demand add-on that has started something the user expects to
|
||||
// keep working with Theseus closed to it — a live relay session, say —
|
||||
// asks to be started at launch from the next start on (false undoes
|
||||
// it). Saved by the host; no effect on a startup add-on.
|
||||
startAtLaunch: (on) => {
|
||||
if (this._setStartAtLaunch) this._setStartAtLaunch(manifest.id, !!on);
|
||||
},
|
||||
// Resolves once the browser chrome has painted (immediately if it
|
||||
// already has). Put expensive dependency loading behind this so it
|
||||
// never competes with the first frame at launch.
|
||||
|
|
@ -736,10 +878,26 @@ class AddonHost {
|
|||
}
|
||||
|
||||
// Route a message to an add-on's registered handler. Callers (main) have
|
||||
// already established WHO is asking; `ctx` carries that provenance.
|
||||
// already established WHO is asking; `ctx` carries that provenance. An
|
||||
// on-demand add-on that hasn't started yet is started first — this is the
|
||||
// path every first use goes through (panel and add-on-tab pages, page
|
||||
// bridges, toolbar and context menus, Settings), and the call waits for
|
||||
// the activation rather than being dropped.
|
||||
async dispatch(id, msg, payload, ctx) {
|
||||
const active = this._active.get(id);
|
||||
if (!this._active.has(id)) {
|
||||
if (!this._dormant.has(id) && !this._activating.has(id)) throw new Error(`add-on "${id}" is not active`);
|
||||
await this.ensureActive(id, `${(ctx && ctx.from) || "call"} ${msg}`);
|
||||
}
|
||||
// An add-on whose activate() returned a promise gets calls only once it
|
||||
// has settled (bounded): handlers may be registered after an await, or
|
||||
// need state the activation is still loading.
|
||||
let active = this._active.get(id);
|
||||
if (!active) throw new Error(`add-on "${id}" is not active`);
|
||||
if (active.ready) {
|
||||
await active.ready;
|
||||
active = this._active.get(id);
|
||||
if (!active) throw new Error(`add-on "${id}" is not active`);
|
||||
}
|
||||
const handler = active.handlers.get(String(msg));
|
||||
if (!handler) throw new Error(`add-on "${id}" has no handler for "${msg}"`);
|
||||
return handler(payload, ctx || {});
|
||||
|
|
@ -748,13 +906,36 @@ class AddonHost {
|
|||
const active = this._active.get(id);
|
||||
return !!(active && active.handlers.has(String(msg)));
|
||||
}
|
||||
// Inject scripts that apply to a tab URL — [{ id, source }].
|
||||
// Running and waiting add-ons in discovery order — the order the dock,
|
||||
// menus and Settings list them in, which must not change when an add-on
|
||||
// starts.
|
||||
_enabledEntries() {
|
||||
const out = [];
|
||||
for (const { manifest } of this._installed) {
|
||||
if (!manifest) continue;
|
||||
const active = this._active.get(manifest.id);
|
||||
const dormant = active ? null : this._dormant.get(manifest.id);
|
||||
if (active || dormant) out.push({ manifest: (active || dormant).manifest, active, dormant });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Inject scripts that apply to a tab URL — [{ id, source }]. A waiting
|
||||
// add-on's bridge is injected too (its source is read on first match); the
|
||||
// bridge's first message is what starts the add-on.
|
||||
injectionsFor(url) {
|
||||
const out = [];
|
||||
for (const active of this._active.values()) {
|
||||
if (active.inject && urlMatchesAny(url, active.inject.matchers)) {
|
||||
out.push({ id: active.manifest.id, source: active.inject.source });
|
||||
for (const { manifest, active, dormant } of this._enabledEntries()) {
|
||||
if (active) {
|
||||
if (active.inject && urlMatchesAny(url, active.inject.matchers)) out.push({ id: manifest.id, source: active.inject.source });
|
||||
continue;
|
||||
}
|
||||
const pi = manifest.pageInject;
|
||||
if (!pi || !urlMatchesAny(url, pi.matchers)) continue;
|
||||
if (dormant.injectSource == null) {
|
||||
try { dormant.injectSource = fs.readFileSync(path.join(dormant.folder, pi.preload), "utf8"); }
|
||||
catch (e) { this.log(`[${manifest.id}] page-inject preload not readable: ${e?.message || e}`); dormant.injectSource = ""; }
|
||||
}
|
||||
if (dormant.injectSource) out.push({ id: manifest.id, source: dormant.injectSource });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -762,7 +943,9 @@ class AddonHost {
|
|||
// page → add-on IPC so a non-matching page can't spoof a matching one.
|
||||
pageAllowed(id, url) {
|
||||
const active = this._active.get(id);
|
||||
return !!(active && active.inject && urlMatchesAny(url, active.inject.matchers));
|
||||
if (active) return !!(active.inject && urlMatchesAny(url, active.inject.matchers));
|
||||
const d = this._dormant.get(id);
|
||||
return !!(d && d.manifest.pageInject && urlMatchesAny(url, d.manifest.pageInject.matchers));
|
||||
}
|
||||
|
||||
// Read-only views for the rest of the app.
|
||||
|
|
@ -781,7 +964,11 @@ class AddonHost {
|
|||
// the raw Extensions list. Absent → plain extension.
|
||||
category: manifest?.category || null,
|
||||
folder,
|
||||
enabled: manifest?.id ? this._active.has(manifest.id) : false,
|
||||
enabled: manifest?.id ? this.isEnabled(manifest.id) : false,
|
||||
// What the manifest asks for, and whether it is running right now
|
||||
// (false while an enabled on-demand add-on waits for first use).
|
||||
activation: manifest?.activation || null,
|
||||
running: manifest?.id ? this._active.has(manifest.id) : false,
|
||||
error: error || null,
|
||||
})),
|
||||
sidebarPanels: this.getSidebarPanels(),
|
||||
|
|
@ -793,25 +980,25 @@ class AddonHost {
|
|||
// instead of the extensions row.
|
||||
getSidebarPanels() {
|
||||
const out = [];
|
||||
for (const active of this._active.values()) {
|
||||
const plugin = active.manifest.category === "plugin";
|
||||
for (const p of active.sidebarPanels) out.push({ ...p, plugin, addonName: active.manifest.name });
|
||||
for (const { manifest, active, dormant } of this._enabledEntries()) {
|
||||
const plugin = manifest.category === "plugin";
|
||||
for (const p of (active ? active.sidebarPanels : dormant.panels)) out.push({ ...p, plugin, addonName: manifest.name });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Menu declarations from every active add-on that carries a toolbar-menu
|
||||
// Menu declarations from every enabled add-on that carries a toolbar-menu
|
||||
// manifest block. Chrome renders one dock button per entry, opens the
|
||||
// dropdown, then dispatches "menu-select" with the picked item id.
|
||||
getToolbarMenus() {
|
||||
const out = [];
|
||||
for (const active of this._active.values()) {
|
||||
const tm = active.manifest.toolbarMenu;
|
||||
for (const { manifest } of this._enabledEntries()) {
|
||||
const tm = manifest.toolbarMenu;
|
||||
if (!tm) continue;
|
||||
out.push({
|
||||
addonId: active.manifest.id,
|
||||
addonId: manifest.id,
|
||||
title: tm.title,
|
||||
icon: tm.icon,
|
||||
plugin: active.manifest.category === "plugin",
|
||||
plugin: manifest.category === "plugin",
|
||||
items: tm.items.map((it) => ({ id: it.id, label: it.label, icon: it.icon })),
|
||||
});
|
||||
}
|
||||
|
|
@ -834,20 +1021,19 @@ class AddonHost {
|
|||
image: ctx.mediaType === "image" && !!ctx.srcURL,
|
||||
};
|
||||
const out = [];
|
||||
for (const active of this._active.values()) {
|
||||
const items = active.manifest.contextMenuItems || [];
|
||||
for (const it of items) {
|
||||
for (const { manifest } of this._enabledEntries()) {
|
||||
for (const it of manifest.contextMenuItems || []) {
|
||||
if (it.when !== "always" && !has[it.when]) continue;
|
||||
out.push({ addonId: active.manifest.id, id: it.id, label: it.label, icon: it.icon, when: it.when });
|
||||
out.push({ addonId: manifest.id, id: it.id, label: it.label, icon: it.icon, when: it.when });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
getInstalled() { return this._installed.slice(); }
|
||||
isActive(id) { return this._active.has(id); }
|
||||
// Absolute folder of an active add-on, or null. Public so main can resolve
|
||||
// Absolute folder of an enabled add-on, or null. Public so main can resolve
|
||||
// add-on-relative paths (openAddonTab) without reaching into internals.
|
||||
folderOf(id) { const a = this._active.get(id); return a ? a.folder : null; }
|
||||
folderOf(id) { const a = this._active.get(id) || this._dormant.get(id); return a ? a.folder : null; }
|
||||
}
|
||||
|
||||
module.exports = { AddonHost, KNOWN_CAPABILITIES, validateManifest, compileOriginPattern };
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M6%202h8l6%206v12a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V4a2%202%200%200%201%202-2z%22%20fill%3D%22%232563eb%22%2F%3E%3Cpath%20d%3D%22M14%202v6h6z%22%20fill%3D%22%231e40af%22%2F%3E%3Ctext%20x%3D%2212%22%20y%3D%2217%22%20text-anchor%3D%22middle%22%20font-family%3D%22system-ui%2C-apple-system%2Csans-serif%22%20font-size%3D%225.5%22%20font-weight%3D%22800%22%20fill%3D%22%23fff%22%3EDOC%3C%2Ftext%3E%3C%2Fsvg%3E",
|
||||
"main": "index.js",
|
||||
"activation": "on-demand",
|
||||
"panels": [
|
||||
{ "id": "main", "title": "Word editor", "page": "panel.html" }
|
||||
],
|
||||
"capabilities": [
|
||||
"sidebar-panel",
|
||||
"open-tab"
|
||||
|
|
|
|||
|
|
@ -6,5 +6,9 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "📝",
|
||||
"main": "index.js",
|
||||
"activation": "on-demand",
|
||||
"panels": [
|
||||
{ "id": "main", "title": "Notepad", "icon": "📝", "page": "note.html" }
|
||||
],
|
||||
"capabilities": ["sidebar-panel"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYgMmg4bDYgNnYxMmEyIDIgMCAwIDEtMiAySDZhMiAyIDAgMCAxLTItMlY0YTIgMiAwIDAgMSAyLTJ6IiBmaWxsPSIjZGMyNjI2Ii8+PHBhdGggZD0iTTE0IDJ2Nmg2eiIgZmlsbD0iIzk5MWIxYiIvPjx0ZXh0IHg9IjEyIiB5PSIxNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9InN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNS41IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmIj5QREY8L3RleHQ+PC9zdmc+",
|
||||
"main": "index.js",
|
||||
"activation": "on-demand",
|
||||
"capabilities": ["toolbar-menu", "open-tab", "context-menu-item"],
|
||||
"toolbar-menu": {
|
||||
"title": "PDF Editor",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "📸",
|
||||
"main": "index.js",
|
||||
"activation": "on-demand",
|
||||
"panels": [
|
||||
{ "id": "main", "title": "Screenshot", "icon": "📸", "page": "panel.html" }
|
||||
],
|
||||
"capabilities": ["sidebar-panel", "capture-tab"],
|
||||
"updateURL": "https://navigate.st/bns/theseus.x/extensions/screenshot/updates.json"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4gPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTEuNSIgZmlsbD0iIzFkNGVkOCIvPiA8cmVjdCB4PSIwIiB5PSIwLjYiIHdpZHRoPSIxMy40IiBoZWlnaHQ9IjEzLjQiIHJ4PSIyLjgiIGZpbGw9IiNmZmYiLz4gPHRleHQgeD0iNi43IiB5PSIxMi4xIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0ic3lzdGVtLXVpLFNlZ29lIFVJLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIuNCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iIzFkNGVkOCI+QTwvdGV4dD4gPHJlY3QgeD0iMTAuNiIgeT0iMTAiIHdpZHRoPSIxMy40IiBoZWlnaHQ9IjEzLjQiIHJ4PSIyLjgiIGZpbGw9IiMwYjEyMjAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjEiLz4gPGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIj4gPHBhdGggZD0iTTE3LjMgMTIuMnYxLjEiLz4gPHBhdGggZD0iTTEzLjUgMTQuNmg3LjYiLz4gPHBhdGggZD0iTTE4LjIgMTUuNyAxMy45IDIxIi8+IDxwYXRoIGQ9Ik0xNi41IDE1LjcgMjAuOCAyMSIvPiA8L2c+IDwvc3ZnPg==",
|
||||
"main": "index.js",
|
||||
"activation": "on-demand",
|
||||
"panels": [
|
||||
{ "id": "main", "title": "Translate", "page": "panel.html" }
|
||||
],
|
||||
"capabilities": [
|
||||
"sidebar-panel",
|
||||
"context-menu-item"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA1djdjMCA1IDMuNSA5LjIgOCAxMCA0LjUtLjggOC01IDgtMTBWNWwtOC0zeiIgZmlsbD0iIzBhYzE4ZSIvPjxwYXRoIGQ9Ik05IDEybDIgMiA0LTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=",
|
||||
"main": "index.js",
|
||||
"activation": "on-demand",
|
||||
"panels": [
|
||||
{ "id": "main", "title": "VPN", "page": "panel.html" }
|
||||
],
|
||||
"capabilities": ["sidebar-panel", "session-proxy"],
|
||||
"updateURL": "https://navigate.st/bns/theseus.x/extensions/vpn/updates.json"
|
||||
}
|
||||
|
|
|
|||
54
main.js
54
main.js
|
|
@ -479,6 +479,12 @@ const SETTINGS_DEFAULTS = {
|
|||
restoreSession: true, // reopen last session's tabs on launch
|
||||
backgroundThrottle: true, // throttle inactive tabs / the window when unfocused
|
||||
freezeBackgroundTabs: true, // a tab you switch away from stops (JS, timers, media) unless marked "Keep running"
|
||||
// Startup (Settings › Performance). Off = the old behaviour: every enabled
|
||||
// extension's activate() runs at launch, before the window exists.
|
||||
extensionsOnDemand: true, // extensions whose manifest allows it start on first use
|
||||
walletAtLaunch: false, // start Aegis at launch even once it allows on-demand (no effect while it is startup-only)
|
||||
preloadMenus: true, // load address suggestions / link pill / site info after the first page (prewarmOverlays)
|
||||
addonsStartAtLaunch: [], // on-demand add-ons that asked (api.startAtLaunch) to start at launch anyway
|
||||
// Storage retention — nothing persists by default. Auto-clear on quit
|
||||
// means a session leaves no trace on disk unless the user opts in per-type.
|
||||
clearCookiesOnQuit: true, // drop cookies + logins + saved-form data
|
||||
|
|
@ -1446,7 +1452,7 @@ const justClosedByClickAway = (view) => Date.now() - (clickAwayClosedAt.get(view
|
|||
// Idle prewarm, one at a time, of the overlays used in nearly every session.
|
||||
let overlaysPrewarmed = false;
|
||||
async function prewarmOverlays() {
|
||||
if (overlaysPrewarmed) return;
|
||||
if (overlaysPrewarmed || settings.preloadMenus === false) return;
|
||||
overlaysPrewarmed = true;
|
||||
for (const view of [addressPicker, linkStatus, popover]) {
|
||||
if (!view || !winAlive()) return;
|
||||
|
|
@ -2339,6 +2345,26 @@ function initAddons() {
|
|||
},
|
||||
dataDir: addonsDataDir(),
|
||||
isDisabled: (id) => Array.isArray(settings.disabledAddons) && settings.disabledAddons.includes(id),
|
||||
// Settings › Performance › Startup can only move an add-on towards
|
||||
// startup, never make a startup-only manifest on-demand.
|
||||
activationFor: (m) => {
|
||||
if (m.activation !== "on-demand") return "startup";
|
||||
if (settings.extensionsOnDemand === false) return "startup";
|
||||
if (m.id === "aegis" && settings.walletAtLaunch) return "startup";
|
||||
if (Array.isArray(settings.addonsStartAtLaunch) && settings.addonsStartAtLaunch.includes(m.id)) return "startup";
|
||||
return "on-demand";
|
||||
},
|
||||
setStartAtLaunch: (id, on) => {
|
||||
const ids = new Set(Array.isArray(settings.addonsStartAtLaunch) ? settings.addonsStartAtLaunch : []);
|
||||
if (ids.has(id) === on) return;
|
||||
if (on) ids.add(id); else ids.delete(id);
|
||||
settings.addonsStartAtLaunch = [...ids];
|
||||
saveSettings();
|
||||
console.log(`[addons] ${id} ${on ? "asked to start at launch" : "no longer needs to start at launch"}`);
|
||||
},
|
||||
// A started add-on may register panels beyond the ones it declared, and
|
||||
// one that failed to start drops out of the dock.
|
||||
onActivated: () => emitSidebarState(),
|
||||
logger: (...a) => console.log("[addons]", ...a),
|
||||
// Session-proxy capability. Add-ons that declare "session-proxy" in
|
||||
// their manifest can call api.setSessionProxy(rules) to swap
|
||||
|
|
@ -2841,7 +2867,7 @@ function initAddons() {
|
|||
});
|
||||
addonHost.discoverAndActivate();
|
||||
const snap = addonHost.snapshot();
|
||||
console.log(`[addons] ${snap.installed.length} installed, ${snap.installed.filter((x) => x.enabled).length} enabled, ${snap.sidebarPanels.length} sidebar panels`);
|
||||
console.log(`[addons] ${snap.installed.length} installed, ${snap.installed.filter((x) => x.enabled).length} enabled (${snap.installed.filter((x) => x.running).length} started at launch), ${snap.sidebarPanels.length} sidebar panels`);
|
||||
}
|
||||
// Given a webContents sender URL, work out which add-on folder it lives in.
|
||||
// Used to gate storage IPC — a page hosted inside addons/<id>/ can only touch
|
||||
|
|
@ -3155,6 +3181,12 @@ function setSidebar(show, panelId) {
|
|||
if (panelId) { console.warn(`setSidebar: no panel "${panelId}"; ignoring`); return; }
|
||||
panel = panels[0];
|
||||
}
|
||||
// Opening a panel is a first use: start its add-on alongside the page
|
||||
// load. The panel's own calls would start it anyway, but a panel that
|
||||
// only listens for api.emit never calls.
|
||||
if (addonHost.isDormant(panel.addonId)) {
|
||||
addonHost.ensureActive(panel.addonId, "panel opened").catch((e) => console.warn(`[addons] ${panel.addonId} failed to start:`, e?.message));
|
||||
}
|
||||
if (sidebarActivePanelId !== panel.panelId) {
|
||||
sidebarActivePanelId = panel.panelId;
|
||||
try { sidebar.webContents.loadFile(panel.pageFile); } catch (e) { console.warn("sidebar loadFile failed:", e?.message); }
|
||||
|
|
@ -5537,10 +5569,18 @@ function routeWizUri(uri, origin, tabId) {
|
|||
if (!addonHost) return false;
|
||||
const u = String(uri || "");
|
||||
if (!/^wiz:/i.test(u) || u.length > 4096) return false;
|
||||
if (!addonHost.hasHandler("aegis", "wcConnectFromPage")) return false;
|
||||
addonHost
|
||||
const send = () => addonHost
|
||||
.dispatch("aegis", "wcConnectFromPage", { uri: u }, { from: "page", origin: origin || "unknown site", tabId })
|
||||
.catch((err) => console.log("[wiz] pairing failed:", err?.message || err));
|
||||
// A wallet that starts on first use is woken by the link itself.
|
||||
if (addonHost.isDormant("aegis")) {
|
||||
addonHost.ensureActive("aegis", "wiz link")
|
||||
.then(() => { if (addonHost.hasHandler("aegis", "wcConnectFromPage")) send(); })
|
||||
.catch((err) => console.log("[wiz] wallet failed to start:", err?.message || err));
|
||||
return true;
|
||||
}
|
||||
if (!addonHost.hasHandler("aegis", "wcConnectFromPage")) return false;
|
||||
send();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -7156,6 +7196,12 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
|||
if (key === "searchEngine") emitEngines(); // the toolbar button and the picker show the default
|
||||
if (key === "theme") applyTheme();
|
||||
if (key === "backgroundThrottle") applyThrottle();
|
||||
// Switching deferral off (or asking for the wallet at launch) starts what
|
||||
// is still waiting now rather than at the next launch. Switching it on
|
||||
// leaves running add-ons alone; it applies from the next launch.
|
||||
if (addonHost && key === "extensionsOnDemand" && val === false) addonHost.activateAllDormant("setting").catch(() => {});
|
||||
if (addonHost && key === "walletAtLaunch" && val && addonHost.isDormant("aegis")) addonHost.ensureActive("aegis", "setting").catch(() => {});
|
||||
if (key === "preloadMenus" && val && chromeReadyDone) prewarmOverlays();
|
||||
if (key === "freezeBackgroundTabs") applyFreezeSetting();
|
||||
if (["timezoneMode", "timezoneValue", "languageMode", "languageSpoof", "languageValue",
|
||||
"locationMode", "locationRegion", "locationCountry", "locationLat", "locationLon", "hideMediaDevices"].includes(key)) { applyFingerprintAll(); applyAcceptLanguage(); }
|
||||
|
|
|
|||
|
|
@ -639,6 +639,23 @@
|
|||
<div class="txt"><div class="t">Stop tabs in the background</div><div class="d">A tab you switch away from stops running — scripts, timers, audio and video — until you come back to it. Right-click a tab and choose “Keep running in background” for music, calls or anything that should keep going.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="freezeBackgroundTabs"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<h2 class="sub">Startup</h2>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Start extensions when first used</div><div class="d">Extensions such as Notepad, Screenshot, Translate and the document editors start the first time you open them, instead of every time Theseus starts. Their buttons and menu items are there either way. Shield and Cookie Pop-ups always start right away. <span id="startupNow"></span></div></div>
|
||||
<label class="sw"><input type="checkbox" id="extensionsOnDemand"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row" id="walletAtLaunchRow">
|
||||
<div class="txt"><div class="t">Start the wallet at launch</div><div class="d" id="walletAtLaunchHint">Aegis is ready the moment a site asks for it, at the cost of a slower start.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="walletAtLaunch"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Preload common menus</div><div class="d">After the first page loads, get address suggestions, the link preview and site info ready in the background so they open instantly. Off saves memory; each opens a moment slower the first time.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="preloadMenus"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Lightweight start</div><div class="d">Set all of the above to the lightest choice.</div></div>
|
||||
<button class="btn small" id="lightStart">Use lightest</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- PRIVACY -->
|
||||
<section id="privacy" hidden>
|
||||
|
|
@ -1055,7 +1072,48 @@
|
|||
el("dohCustom").addEventListener("change", () => C.set("dohCustom", el("dohCustom").value.trim()));
|
||||
});
|
||||
})();
|
||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "freezeBackgroundTabs", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc",
|
||||
// Performance › Startup. The three switches are plain TOGGLES; this adds
|
||||
// what they can't say on their own: how many extensions are running now,
|
||||
// whether the wallet switch can do anything yet (only once Aegis's
|
||||
// manifest allows on-demand starts — until then it always starts at
|
||||
// launch, so the switch is shown but off-limits), and the preset.
|
||||
(function () {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const onDemand = $("extensionsOnDemand"), wallet = $("walletAtLaunch"), menus = $("preloadMenus");
|
||||
if (!onDemand || !wallet || !menus) return;
|
||||
let aegisOnDemand = false;
|
||||
function syncWallet() {
|
||||
const usable = aegisOnDemand && onDemand.checked;
|
||||
wallet.disabled = !usable;
|
||||
$("walletAtLaunchRow").style.opacity = usable ? "" : ".55";
|
||||
$("walletAtLaunchHint").textContent = !aegisOnDemand
|
||||
? "Aegis still starts with Theseus every time, so there is nothing to choose yet. This switch takes over once Aegis can start on first use."
|
||||
: !onDemand.checked
|
||||
? "Every extension starts at launch while the switch above is off."
|
||||
: "Aegis is ready the moment a site asks for it, at the cost of a slower start. Off, it starts the first time a site, a link or its panel needs it.";
|
||||
}
|
||||
async function refresh() {
|
||||
let list = {};
|
||||
try { list = await (C.listAddons ? C.listAddons() : Promise.resolve({})); } catch {}
|
||||
const inst = (list.installed || []).filter((a) => a.id && !a.error);
|
||||
const aegis = inst.find((a) => a.id === "aegis");
|
||||
$("walletAtLaunchRow").hidden = !aegis;
|
||||
aegisOnDemand = !!(aegis && aegis.activation === "on-demand");
|
||||
const on = inst.filter((a) => a.enabled);
|
||||
$("startupNow").textContent = on.length ? `Running now: ${on.filter((a) => a.running).length} of ${on.length}.` : "";
|
||||
syncWallet();
|
||||
}
|
||||
onDemand.addEventListener("change", syncWallet);
|
||||
$("lightStart").addEventListener("click", () => {
|
||||
const want = { extensionsOnDemand: true, walletAtLaunch: false, preloadMenus: false };
|
||||
for (const [k, v] of Object.entries(want)) { $(k).checked = v; C.set(k, v); }
|
||||
syncWallet();
|
||||
});
|
||||
refresh();
|
||||
// Running counts change as extensions start; refresh when the page is shown.
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden) refresh(); });
|
||||
})();
|
||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "freezeBackgroundTabs", "extensionsOnDemand", "walletAtLaunch", "preloadMenus", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc",
|
||||
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit",
|
||||
"quickLinksShow", "translateAutoOffer"];
|
||||
// Text inputs that round-trip through settings-set on change. Trimmed; a
|
||||
|
|
@ -2322,7 +2380,7 @@
|
|||
box.innerHTML =
|
||||
'<div class="xhead"><button class="xback" id="xback" title="Back to the list" aria-label="Back">‹ Back</button>'
|
||||
+ '<span class="xi">' + addonIconHtml(a.icon) + '</span>'
|
||||
+ '<div class="xtitle"><div class="t">' + escapeHtml(a.name) + (a.bundled ? '<span class="xbadge">BUILT-IN</span>' : '') + (a.enabled ? '' : '<span class="xbadge off">OFF</span>') + '<span class="xv">' + escapeHtml(a.version) + '</span></div>'
|
||||
+ '<div class="xtitle"><div class="t">' + escapeHtml(a.name) + (a.bundled ? '<span class="xbadge">BUILT-IN</span>' : '') + (a.enabled ? (a.running ? '' : '<span class="xbadge off" title="Starts the first time you use it">NOT STARTED</span>') : '<span class="xbadge off">OFF</span>') + '<span class="xv">' + escapeHtml(a.version) + '</span></div>'
|
||||
+ '<div class="d">' + escapeHtml(a.description || "No description.") + '</div></div>'
|
||||
+ '<span class="xctl"><label class="sw sm" title="' + (a.enabled ? "On" : "Off") + '"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label>'
|
||||
+ '<button class="xmore" data-more="' + escapeAttr(a.id) + '" title="More options" aria-label="More options">⋯</button>'
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue