Commit graph

572 commits

Author SHA1 Message Date
Local Dev
65b4ac5ad3 perf(aegis): 251 KB off every panel open — jsQR loads when it is wanted
panel.html loaded lib/jsqr.js synchronously: 257 KB and 10,105 lines of
vendored decoder parsed on every panel open, so on every hide/show, for a
feature most sessions never touch. It is now fetched the first time someone
imports a QR image, with concurrent callers sharing one load and a failed
load retryable rather than cached as a permanent rejection.

qr.js and panel.js get `defer`, so the browser can paint the (already dark)
document before executing 300 KB of panel.js. Order is preserved, so qr.js
is still defined before panel.js runs.

Parsed on open: 650 KB -> 399 KB.

This shortens the blank window but does not remove it. The white box itself
is Theseus-side: the sidebar panel's view is built as

  new WebContentsView({ webPreferences: { preload: "sidebar-preload.js" } })

with no backgroundColor, and Electron defaults a view's background to white —
so white shows from view-attach until the page paints. Every other view in
the app passes a colour, and registerSidebarPanel takes only
{id, title, icon, page}, so an add-on cannot declare one. Not fixed here:
main.js belongs to the Theseus work in flight.
2026-10-03 16:39:45 +02:00
Silent Mode
0ba0e735ed Theseus: translator talks to silentmode.st/libre + libre.x / lingua.x
The translator client now ships with the right defaults for the actual
deployment: silentmode.st/libre (ICANN, via the main cert and no new
subdomain) is the primary peer; libre.x and lingua.x are registered on
BNS with `p` records that reverse-proxy back to the same backend; the
public LibreTranslate.com key-gated tier stays as the last-resort entry.

Two wiring fixes make the BNS fallback actually usable from Theseus:

1. translatorPostOnce rewrites the request URL through targetUrlFor
   before fetching, so a peer whose host is a BNS name (libre.x) is
   dispatched via the in-process bns:// handler — Chromium's net stack
   has no way to resolve `.x` by itself.

2. serveBns's p-record branch now forwards the method, headers and body
   of the original request to the upstream, not just a GET. Without
   that, a POST /translate against libre.x arrived at the backend as
   a GET with no body and 400'd — now the proxy is actually a reverse-
   proxy, as the record type's name promises.

Verified end-to-end against the live silentmode.st/libre instance from a
fresh Theseus profile with a Spanish test page: both the direct
silentmode.st/libre peer and the libre.x -> bns:// -> serveP -> upstream
path translate the page and the revert path restores the originals.
2026-10-03 16:39:45 +02:00
Local Dev
c74d2183e3 Merge branch 'claude/agitated-bell-bd4ac2' 2026-10-03 16:37:03 +02:00
Local Dev
84d5411a53 BNS indexer design: what steps 1, 2 and 4 built, and the step-3 contract
Records where the Ariadne 0.2.0 implementation differs from the design
and why: the protected index\ subfolder, the helper-relayed pipe check,
ariadne-run.exe as the restarter because Task Scheduler does not restart
a program that exits with an error, the resolver exiting in Theseus-only
mode, no owners on the HTTP API, and setup's own scope page. It also
records what the Theseus client needs from the pipe. Economy produces no
"fresh" pushes, so a missing heartbeat while paused must not trigger a
takeover. Last, the plan for bundling Ariadne's setup into Theseus's
installer, not yet wired into the build.
2026-10-03 16:14:52 +02:00
Local Dev
7983e24f2e perf(aegis): history was 473 round trips in series, and persisted all of them
Second half of the Theseus-lag investigation. 0.27.1 removed the 7.4 MB
store; this removes the two things that produced it and the latency that
came with it.

Measured on this profile's own cache: 6,981 transactions, 7.38 MB, and one
consolidation with 400 inputs (median 2).

- loadHistory() awaited getTx() per displayed transaction and then again per
  INPUT, strictly one at a time. The 400-input row alone cost 400 serial
  round trips. Both waves are now prefetched with bounded parallelism
  (getTxMany, 12 at a time). Against a simulated 10 ms link the same 473
  fetches take 771 ms instead of ~4,730 ms; on a real 30-50 ms link the
  serial version was 15-25 seconds per refresh, per wallet.
- Parent transactions were persisted forever. They exist only to compute a
  delta for the 25 rows on screen, and keeping every one ever seen is what
  grew the file. Only the displayed window is written now — 25 entries,
  16.4 KB in the harness — while parents stay in a process-lifetime map
  bounded at 20,000, seeded from the window so a restart does not refetch
  what is already visible. A second refresh issues zero fetches.

TX_CACHE_VERSION 4 discards v3 caches. The v3 cap of 400 was also exactly
the wrong number for this data: a 400-input transaction needs 401 entries,
so it would have evicted and refetched on every single refresh.
2026-10-03 16:12:53 +02:00
Local Dev
4ab61b83db Theseus 0.3.71: no more launch freeze from large add-on stores
Ships 057629d — add-on stores kept in memory instead of re-read and re-parsed
on every get (a 7.5 MB Aegis store held the window in Not Responding for
16 s) — plus the in-page translator (4fbfc9e, f54ebd6, b43b180).
2026-10-03 16:12:33 +02:00
Local Dev
65ef59f5c8 Theseus: add-on stores live in memory — no more 16 s "Not Responding" at launch
An add-on's storage.get read and parsed its whole store file on every call,
and storage.set read, parsed and rewrote it — synchronously, on the main
thread. Traced on a real profile (installed 0.3.70): with a 7.5 MB Aegis
store, 30 of the first 35 s of main-thread time went to storage.get, the
window sat in "Not Responding" from 3 s to 19 s, and the first page showed at
19 s. One get cost ~73 ms; Aegis does dozens per state update.

lib/addon-store.cjs keeps one in-memory copy per store, shared by the
add-on's api.storage (addons-host.js) and its pages (addon-storage-* IPC in
main.js). After a one-time load a get costs microseconds; values are copied
in and out (structuredClone), so callers keep the old semantics. Writes are
coalesced (100 ms) and land as temp-file + rename, and are flushed on quit;
a store that doesn't parse is moved aside instead of being replaced by {}.

Measured on copies of the same profile, dev build:
  first page 16.9-17.6 s -> 1.5-1.7 s; main thread blocked 24.7-25.8 s of
  30 -> 1.0-1.1 s; longest freeze 13.7-15.0 s -> 0.6 s.

The Aegis side (capping its unbounded txCache) ships separately through
Aegis's own update channel. The boot tracer gains total/longest block columns.
2026-10-03 16:08:22 +02:00
Local Dev
434f6d35d0 Theseus: hand-off for moving Aegis to on-demand activation
Aegis is most of the remaining launch cost: ~165 ms of synchronous
activation, then ~610 ms of main-thread work loading its crypto and
WizardConnect deps. It was left on startup because another session
owns bundled-addons/aegis. NOTE-aegis-on-demand.md says exactly what it
needs, measured against 0.27.1:

- addon.json: "activation": "on-demand", plus panels [{id:"main",
  title:"Wallet", page:"panel.html"}].
- activate() must return a promise that resolves once deps are loaded
  and, only if the vault is already unlocked, wallets are mounted.
  vault.derive never rejects on a locked vault, so awaiting
  mountAllWallets there would stall every first call for the full 5 s
  host wait.
- Persisted WizardConnect pairings (wc/<walletId>/uris) need a relay
  listener: call api.startAtLaunch(true) while any exist, false when
  none are left.
- Bridge calls, panel open, wiz:// links and Settings calls already wake
  it. The dapp globals are injected from the manifest before it runs.

GOTCHAS: an enabled add-on may not be running, so code in main reaches
add-ons through dispatch() rather than checking isActive()/hasHandler().
2026-10-03 16:05:41 +02:00
Local Dev
4cc2d53582 Theseus: start extensions on first use, Startup switches in Settings
Every enabled add-on used to be activated synchronously in initAddons(),
during app.whenReady and before the window exists. That is the largest
launch cost left (boot tracer, 2026-10-03). An add-on can now say
"activation": "on-demand" in addon.json. It is then listed at launch but
not started. Its declared surfaces stay live: "panels" (new: sidebar
panels declared up front), toolbar-menu, context-menu-items and the
page-inject bridge, whose source is read on the first matching page.
activate() runs on first real use: a panel opened, a menu or context
item picked, a message from its panel, tab or page bridge, a Settings
addon-invoke, a wiz:// link (for Aegis). All of those go through
AddonHost.dispatch(), which starts the add-on and waits for it, so no
call is dropped. Concurrent callers share one activation, and
activations run one at a time.

Startup stays the default: the host cannot tell what an older add-on
does in activate(). request-filter add-ons and add-ons that declare no
surface are forced to startup. If activate() returns a promise, calls
wait for it (at most 5 s). api.startAtLaunch(bool) lets an on-demand
add-on ask to be started at launch again (for live relay sessions).

Converted: notepad, screenshot, translate, docx-editor, pdf-editor, vpn
(none has launch-time work: no file association, no auto-connect, and
add-on file tabs are not part of the saved session). Shield and Cookie
Pop-ups stay startup: Shield owns the request filter and must see the
first request; Cookie Pop-ups costs ~6 ms and acts unasked on every
page. Aegis stays startup and untouched: another session owns it. See
NOTE-aegis-on-demand.md (next commit).

Settings › Performance › Startup:
- "Start extensions when first used" (default on). Off = all at launch;
  switching it off starts the waiting add-ons immediately.
- "Start the wallet at launch". Shown disabled with a hint until the
  installed Aegis manifest allows on-demand. It applies with no Settings
  change once Aegis opts in.
- "Preload common menus" gates prewarmOverlays().
- "Use lightest" preset.
New keys are plain SETTINGS_DEFAULTS through the existing settings-set.
No new IPC channels.

Measured: boot-trace, fresh profile, --seconds 20 so the 30 s add-on OTA
poll can't swap Aegis mid-series; warm runs 2-3 of two paired series.
- Add-on activation at launch: 121-154 ms -> 104-174 ms. The six
  converted add-ons went from 16-20 ms to 0. The rest is Shield (83-148
  ms, noisy) and Aegis (15 ms in this tree's 0.9.0).
- Toolbar painted: 1278-1584 ms -> 1282-1481 ms (within noise).
- With "Preload common menus" off: 0 overlays prewarmed, 8 processes
  instead of 11, about 50-70 MB less at 15 s.

The bundled add-on versions are not bumped. Existing profiles keep their
old addon.json, and so stay on startup activation, until those add-ons
ship with a higher version (seedBundledAddons only reseeds a strictly
newer bundle).
2026-10-03 16:05:32 +02:00
Silent Mode
e8317fef16 Theseus: translator defaults — libre.silentmode.st and libre.x
translate.silentmode.st had "translate" in the subdomain and in the
LibreTranslate path, which read awkwardly on both the chip tooltip and
the Settings list. The shipped defaults rename the primary peers to
libre.silentmode.st and libre.x (plus lingua.x registered server-side
as an alias — same ip record, so it's a URL users can also remember
without being another independent peer in the client's fallback list).
2026-10-03 15:30:54 +02:00
Silent Mode
0b36532922 Theseus: translator peers list — fall back when a mirror is down
The chip ran against a single endpoint, which is the fastest way to go
dark: libretranslate.com's public tier moved behind an API key in late
2026, and most of the historical public mirrors (libretranslate.de,
argosopentech, lt.vern.cc, translate.terraprint.co) either 502 at any
given time, serve a parked page, or started requiring a key of their
own. One URL in settings meant one of those going down meant the chip
stopped working.

Settings.translateEndpoints is now an ordered list. The translator tries
each peer in order and returns the first non-error answer; a dead peer
is logged and skipped. Order is preserved — the first entry is the
primary. Shipped defaults put Silent Mode's own instances
(translate.silentmode.st, the BNS name translate.x) at the top and keep
libretranslate.com as the last-resort entry; neither Silent Mode URL
answers today, but a user's chip starts working as soon as either goes
live without a browser release.

Settings › General › Translate pages grew a list editor (same shape as
the quick-links one): PRIMARY tag on row 0, add a peer, remove any row;
bns:// URLs are accepted so a BNS translator doesn't have to be fronted
by an https host. The single-URL `translateEndpoint` setting carried
over from the earlier draft is migrated on load — a custom URL goes to
the front of the list, the historical default is dropped.
2026-10-03 15:19:09 +02:00
Silent Mode
f3c8998ecc Theseus: in-page translator (LibreTranslate client)
The Website-language setting only tells servers what the user prefers via
Accept-Language — many static sites (including names on BCDN) serve one
language and ignore it, so e.g. hello.bch loads in English for every user,
in every language. This adds a translator that converts the page's visible
text in place, so a Lithuanian user reads hello.bch in Lithuanian without
asking the server for anything.

The URL-bar grows a translate chip next to the website-language globe. The
chip lights up when the page's declared `<html lang>` differs from the
user's preferred language. Click it once to translate in place; click again
to revert — originals are kept in a renderer-local state slot and swapped
back without a reload. Right-click opens the chip menu (change target /
translator settings).

The engine lives behind a swappable adapter in main — this ships with the
LibreTranslate backend (POST /translate with {q, source, target, format}).
The endpoint defaults to the LibreTranslate public tier but is settable in
Settings › General › Translate pages, so a user with a self-hosted
LibreTranslate (or Silent Mode's own translate.silentmode.st once it is
up) swaps it there without a code change. On-device Bergamot (the WASM
engine Firefox Translations uses) will plug into the same adapter in a
later release — same contract (array of texts in, array of translations
out), the chip and revert path are already engine-agnostic.

The fetch goes through session.defaultSession.fetch so Tor and add-on
proxy rules apply uniformly, chunks the batch at ~3.8 KB per POST so a
large page spreads across several requests, times each one out at 45 s,
and reports a failure to the chip's tooltip so a dead endpoint reads as
such and not as a silent no-op. The injected walker skips SCRIPT / STYLE
/ CODE / PRE / NOSCRIPT / TEXTAREA and contentEditable subtrees, keeps a
reference to each text node and the original text, and reverts by
restoring from that pair.
2026-10-03 15:01:40 +02:00
Local Dev
8b0cc00290 BNS indexer design: Theseus's own index is a standby behind Ariadne's
Theseus keeps serving bns:// itself but does not run its own indexer
process while Ariadne's is healthy (pipe answers, passes the server check,
heartbeats arrive, snapshot fresh). On failure it takes over warm from
Ariadne's last snapshot, and hands back once Ariadne has been healthy for a
while. Until the Ariadne pipe exists, Theseus's indexer stays always on.
2026-10-03 14:40:30 +02:00
Local Dev
adcea19f8a Theseus: background tabs stop unless kept running; popups close on click-away
A tab you switch away from is frozen (page lifecycle "frozen": no JS,
timers, network callbacks or media) one second later and thawed the moment
it is shown again. Right-click a tab → "Keep running in background" exempts
it (music, calls, dashboards); the strip marks it ▶. Dormant restored tabs
and tabs waiting on a page dialog are never frozen. Settings › Performance ›
"Stop tabs in the background" (on by default) turns it off. Freezing uses
the per-tab debugger applyFingerprint already keeps attached; Chromium only
freezes hidden pages.

The downloads, site-info and engine-picker popups close when focus moves
elsewhere in the window or to another app; the toolbar click that caused
that does not reopen them. Focus only moves into a popup while the window
is active — focusing it from the background blurs the window and closed the
popup it had just opened.

Also fixes a bug in the lazy overlays: isLoading() is still true while
did-finish-load is delivered, so a first show waited out the 4 s timeout
before appearing. Finished loads are now recorded explicitly.
2026-10-03 14:14:47 +02:00
Local Dev
b531c89f82 Theseus boot tracer: measure a working launch, and say when it did not
With a script as the Electron entry, the app path is the script's folder;
main.js loads chrome.html, home.html and every overlay by relative name, so
all of them failed with ERR_FILE_NOT_FOUND and every traced run measured a
broken launch (it also left a window showing the error page). The tracer now
sets app.setAppPath to TheseusNavigator, records failed main-frame loads, and
run.mjs marks such a run INVALID.

Re-measured (warm runs, fresh profile): the per-overlay lazy loading in
e524c12 saves 5 processes and ~100 MB private memory at 15 s — not ~30 MB
with an unchanged process count as its message says; that figure came from
the broken runs. The BNS indexer utilityProcess costs ~60-75 MB.
2026-10-03 14:14:46 +02:00
Local Dev
469ff482ea feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets
Ported from Digibyte.X/dgb-wallet @ 989a2696.

BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's
official 2018-19 Android/iOS wallets were BreadWallet forks and used
"DigiByte seed" as the HMAC key instead, so the same twelve words produce a
completely different key tree — every address differs, and a standard scan
finds nothing at all. Anyone importing one of those seeds into Aegis got a
valid, empty address and a zero balance with no way to tell why. Same shape
as the Bitcoin.com coin-type-0 problem.

- rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC
  key, matching upstream, and exports HMAC_BITCOIN_SEED /
  HMAC_DIGIBYTE_SEED.
- lib/import-derive.js grows the same option, since that is what actually
  derives the address on an import, and importWallet records the variant on
  the spec so the entry says which tree its stored address came from.
- The path scanner added in 0.24.0 now covers DigiByte: all four purposes
  under the standard key, plus m/0' and BIP44 under the legacy one. So the
  answer to "which tree holds my coins" is a scan rather than a guess, and
  the Use button carries the key along with the path.

Verified against an independent HMAC-SHA512 computation, and end to end: the
same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under
the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn.

Also fixes a coupling this exposed: lib/import-derive.js derives taproot
addresses but never called initEccLib, relying on chain-btc.js (and formerly
lib/dgb/core/address.js, before it went lazy here) doing it at load time. It
now installs the schnorr backend itself, once, so its taproot output no
longer depends on an unrelated module's import order.

The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is
upstream-only for now — Aegis's imported DGB adapter watches a single stored
address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
Local Dev
c1988b9674 fix(aegis): DigiByte import failed on every OTA install
Reported as "the Aegis DigiByte import error". The vendored @dgb-wallet/core
modules under lib/dgb/ imported "bitcoinjs-lib", "bip32", "ecpair",
"bip39" and "@bitcoinerlab/secp256k1" as bare specifiers. Node's ESM resolver
walks up from the importing file, so that resolves in a dev checkout — where
Theseus's node_modules sits above the add-on — and resolves nowhere once
Aegis is running from <userData>/extensions/aegis/, which is every OTA
install. The import threw, index.js caught it and set dgbCore = null, and any
DigiByte import then died in digibyteNetwork() with "DGB adapter not
available".

So DigiByte worked on a fresh install and disappeared after the first update.
Confirmed on this machine: the only node_modules reachable from the installed
extension carries bip39 and none of the other four.

lib/dgb/deps.js now holds the dependencies, injected by index.js from
api.require (which resolves against the app tree) before anything under
lib/dgb/ is imported — the same pattern every other lib/*.js in Aegis already
uses, and the reason that pattern exists. Consumers read them through
accessors rather than capturing them at module scope, so import order is no
longer load-bearing: hd.js builds its bip32 on first use and the two
initEccLib callers go through a once-only ensureEcc(). Using a DGB module
without injection now throws a named error instead of a resolver failure
swallowed into a null.

Verified by loading the modules from a directory with no reachable
node_modules and deriving real addresses: BIP44 D…, BIP49 S… with its legacy
3… pair, BIP84 dgb1q…, BIP86 dgb1p… taproot, plus a WIF round-trip and the
non-DGB WIF rejection.
2026-10-03 13:59:01 +02:00
Local Dev
a57d20751b Theseus: boot tracer under scripts/boot-trace
Measures dev-tree launches against a throwaway profile, without changing
main.js: time to app ready, toolbar and first page; main-thread blocks; add-on
activation; overlay pages loaded; main-process fetches; process count and
private memory at 15 s. One row per run, so a startup change can be compared
with the numbers before it.

  node scripts/boot-trace/run.mjs [--runs 3] [--seconds 25] [--fresh] [--profile <dir>]
2026-10-03 13:26:44 +02:00
Local Dev
c5b2383df8 Theseus: load each overlay page on first use, prewarm only the common ones
All nine overlay pages (site info, engine picker, downloads, address
suggestions, password fill, link pill, approvals, page dialogs) loaded 250 ms
after the toolbar, whether or not the session would ever open them. Each now
loads on its first use; the three used in nearly every session (address
suggestions, link pill, site info) are prewarmed one at a time after the
first page. Measured: 8 -> 3 overlay pages loaded at startup, ~30 MB less
private memory at 15 s (they share one renderer, so the process count is
unchanged); launch timing unchanged within noise.

The show functions send their data right after showing, which a page still
loading drops, so a first show waits for its page and then runs; a hide in
the meantime cancels it.

Also: the indexer's restart notice is logged when the restart happens,
not when the child exits — on app.exit() the timer never fires, so a
forced exit no longer prints a restart that does not happen.
2026-10-03 13:26:43 +02:00
Local Dev
1f1bde6e60 Theseus: BNS index in its own process; a name never waits for the download
The snapshot parse, index builds, electrum sync and snapshot refresh ran on
the browser's main thread at launch. They now run in bns-indexer.js, a
utilityProcess, in two phases: the local snapshot first (no network), then
— once the first page has loaded — the published snapshot (one download)
and the electrum poll. Main keeps a mirror of the name map for its
synchronous lookups; tabs no longer wait for the index to restore.

With no local index yet, a name under a known BCNR TLD gets one lookup of
just that name on the gateway and opens; the full snapshot and the electrum
check follow in the background, and every quick answer is compared with the
verified index when it lands (a mismatch reloads the affected tabs). Plain
web hosts are never sent to the gateway, and the extension-publisher check
only accepts verified data.

DESIGN-bns-indexer-service.md: Ariadne's Thread as the owner of the one
shared indexer (scope x mode, launch at start, power, and a resolver that
never depends on the indexer).
2026-10-03 13:08:28 +02:00
Silent Mode
c2ec0f0d02 Theseus 0.3.70: fully-lazy session restore, quick-links strip reordered
Session restore no longer loads any page on launch. In 0.3.63 the strip was
built from saved titles + favicons and only the previously-active tab's URL
was navigated at startup, so a 20-tab session cost one renderer load instead
of twenty — but that one load is still a real page, often the heaviest one
in the whole session, and it fought every other startup task for the main
thread while the window sat not-responding. Now every restored tab, the
previously-active one included, comes up dormant: zero page renderers at
launch, no page starts loading until the user asks for a specific tab
(clicks the chip, hits reload, types in the URL bar). The previously-active
tab stays highlighted in the strip so one click brings it back; the content
area sits with the tab's own background colour until that click. RAM-at-
launch is now just the chrome, the overlays and the strip — a 500 MB saved
page never materialises as a renderer the user did not even ask to see.

A pending tab that is navigated explicitly (URL bar, link, search) drops
its saved URL at the top of navigateTab, so a later reload or chip click
can't snap it back.

Quick-links strip default set is now Telegram, WhatsApp, X, YouTube — in
that order. Messenger and Spotify are out of the default; users who want
them can still add them via Settings › General › Quick links. Existing
installs whose list still matches the previous untouched default (same six
ids in the same order) migrate on next launch; any customisation (reorder,
add, remove) is left alone.
2026-10-03 11:03:53 +02:00
Local Dev
f2ff48b977 Theseus: the startup update check no longer silently misses new releases
The startup check shared the main thread with snapshot parsing, tab restore
and add-on activation, under a 5 s abort timer started before the request.
Measured on an empty profile: 3.2 s for the manifest fetch, 1.6 s of it the
event loop being busy; a real profile went past 5 s, the abort won, the
failure was swallowed and nothing retried before the 6-hourly recheck. The
update only appeared after a manual "Check for updates".

- the startup check runs 8 s after the toolbar is ready and retries with
  backoff (30 s, 2 min, 10 min, 30 min) when it fails
- 20 s timeout via AbortSignal.timeout
- a failed installer download is retried on the next check instead of
  staying failed until the next release
- failures are logged
2026-10-03 10:57:30 +02:00
Local Dev
40683c84cf perf(aegis): stop freezing Theseus — a 7.4 MB store parsed on every read
Reported as "opening Aegis makes Theseus get stuck / not responding", and it
was Aegis's fault.

The host's add-on store is ONE JSON file per add-on, and storage.get() does a
readFileSync plus a JSON.parse of the whole thing on every call —
synchronously, on the Electron main thread, the thread that drives the entire
browser. storage.set() additionally stringifies and writes all of it.

That store had grown to 7.4 MB, 99.9% of it one wallet's txCache: getTx()
kept every transaction it ever fetched, with full vin/vout arrays, and a busy
chipnet test wallet had thousands. Measured on the real file: parse 59 ms,
stringify 39 ms. So one storage.get blocked the UI for ~60 ms, one set for
~99 ms, and fullState() — which reads the store seven times over
selectedWalletId, walletRoles, walletEntries, snapshotForSelected and the
server list — cost ~420 ms. emitState() runs on every adapter change, across
nine wallets, so the main thread was never given back.

Three changes:

- txCache is capped at 400 entries, pruned newest-first by block time
  (unconfirmed entries sort as newest — they are the current ones). Worst
  case ~0.3 MB per wallet instead of unbounded.
- TX_CACHE_VERSION 3, so existing oversized caches are discarded on first
  load rather than needing a manual clear.
- loadHistory() only persists when something actually changed; an idle wallet
  was rewriting the whole file on every poll for nothing.
- api.storage gains a write-through read cache, so repeated gets cost one
  parse per process instead of one per call. Writes still go to the host
  unchanged. Safe because this process is the only writer — Aegis's panel
  talks over addon messages and never touches addon storage; if that changes,
  the cache has to go.
2026-10-03 10:56:59 +02:00
Local Dev
9b2d6322be Theseus 0.3.69: security fixes from the 2026-10-03 review
Ships fc25e1c and 380ac57: a website loaded into the Settings tab could read
the password vault, add-on updates accepted any publisher's signature, and
the review's follow-ups (stale BNS records, POST replay, background dialogs
stealing focus, update helper on non-ASCII profiles, ...).
2026-10-03 10:18:47 +02:00
Local Dev
2496e54385 Theseus: fix the review follow-ups (stale BNS records, POST replay, dialog focus theft, ...)
- Resolved names are re-resolved when a newer index lands and evicted when
  they drop out of it; an edited ip/s3/tls record, a transfer or an expiry
  used to keep serving the old target until restart. The signed-DNS A
  fallback follows its 30 s TTL instead of the first answer it ever saw.
- A cross-host navigation to a host the warm index knows is unregistered is
  left to Chromium: replaying it via loadURL turned form POSTs (OAuth
  form_post, SAML, 3-D Secure) into bodyless GETs. The site badge follows
  navigations Chromium makes on its own.
- A background tab's alert/confirm no longer pulls its tab to the front; it
  waits, marked in the tab strip, until the user switches to it. Dialogs in
  other windows use the async box, so they no longer freeze every tab.
- Messages resolves sender keys from the browser's own index (one map per
  index generation) instead of a full chain walk per unknown sender; the
  dedupe set is bounded.
- Ariadne uninstall reads HKLM only and runs nothing but unins###.exe from
  Program Files, elevated directly rather than via cmd /c.
- Tor and an add-on proxy no longer wipe each other's settings: Tor wins
  while on, the add-on's rules come back when it goes off.
- Profile migration copies beside the target and renames it into place;
  a failed copy keeps the old, complete profile instead of a partial one.
- Reload/DevTools/zoom shortcuts in app and link windows act on that window;
  Ctrl+B stays with web pages (bold) and toggles the sidebar elsewhere.
- quickPanel comment corrected: it shares the default session on purpose.
2026-10-03 09:59:53 +02:00
Local Dev
08beadcf8f Theseus: close the Settings-tab vault leak and the add-on update signer bypass
A preload belongs to the WebContents, not the page: a website loaded into
the Settings tab kept window.cfg and could read every vault password, flip
settings and install extensions without consent. Settings and add-on tabs
now never load web content, and the channels behind settings-preload check
their sender. `navigate` no longer accepts calls from web pages.

Add-on updates trusted any publisherSig, whatever name it carried, even for
bundled add-ons. The trust root is now the installed addon.json (publisher,
or the operator key when there is none); versions must be plain dotted
numbers; a community install can't take over a bundled or foreign id.

Also:
- autofill matches and fills against the live URL, not a stale prov.host
- bns:// forwards the raw request path (..%2F escaped the name's bucket)
- clipboard-read denied, openExternal asks; forged collision choices ignored
- web pages can't window.open file:/chrome:/theseus:; data:/blob: no longer
  go to the search engine; the quick-links panel loses home-preload
- clear-history-on-quit is awaited and removes history.json too
- update helper takes its paths from the environment (non-ASCII profiles)
- electrum poll has a deadline; misses wait at most 2.5 s
- p records go through Tor; add-on proxy credentials are actually used
- whole-folder require-cache bust on add-on version change; failed
  activate() no longer leaks its request filter
- approvals released when the window closes; web-app ids stay on-origin
2026-10-03 09:50:10 +02:00
Local Dev
64b75c5bf4 feat(aegis): the BCH bridge works on any site, not just .x
window.bitcoincash was gated on a ".x" hostname in both the preload and the
host handlers — inherited from the pre-multi-wallet build, when every dapp
Aegis knew about was a Silent Mode one. It was never a security boundary: the
TRX / ETH / SOL bridges have always injected everywhere, and every BCH call
still crosses into main, raises an approval overlay and honours per-origin
permissions. The preload cannot read an address or sign anything by itself.

What the gate actually cost was Cauldron, bch.guru and our own dapps on other
TLDs (potidaea.asm) seeing no BCH provider at all, on a wallet whose entire
subject is BCH. WizardConnect was never gated this way, so the two halves of
the same bridge disagreed about who could talk to it.

isBchOrigin becomes isDappOrigin and now only keeps non-web schemes out —
file://, chrome://, data:, javascript: and anything unparseable.

Separately: page-inject declared https://*/* only, so a dapp served from
http://127.0.0.1 got no preload at all, which is a different cause from the
.x gate and would have survived removing it. localhost, 127.0.0.1 and [::1]
are now matched over http as well (the host's compiler makes the port
optional, so :3000 and :5173 are covered). Plain http on any other host stays
out, since an http page is tamperable in transit and only local development
needs the exception.
2026-10-03 09:45:01 +02:00
Local Dev
40e7fe2ae9 fix(aegis): PIN asked at the door, and the pad stops erasing itself
Two regressions in 0.26.0.

The PIN was still asked after the wallet had painted. pinGateOnLoad() ran
after render() and was not awaited, so the interface appeared — balances and
all — and the pad then opened on top of a panel the user could already read.
It is now awaited before the first render, and `pinGateBlocked` keeps the
chrome hidden for as long as the PIN is owed: render() returns early into a
closed door, state pushes cannot paint around it, and Settings is covered too
(unlike the vault lock, where Settings must stay reachable to create a PIN in
the first place, here it would just be a way around the gate). Cancelling
leaves the door shut with a retry rather than falling through to an open
wallet.

The pad threw digits away mid-entry. render() fires on every state push —
balance polls fire it every couple of seconds — and renderLockScreen()
rebuilt body.innerHTML unconditionally, replacing the pad and its buffer
under the user's fingers. The reset was timed to the poll, not to the
keypress count, which is why it looked like "after two digits". All three
branches are now keyed on body.dataset.mode and rebuild only when the mode
actually changes; the master-password and first-run forms had the same defect
and were erasing half-typed input the same way.

Also: every pad was wired through getElementById, so two pads alive at once
(a load gate plus a transaction gate) were duplicate ids and the second
setupPinPad bound the first pad's buttons. All four are now scoped to their
own container, only one PIN prompt can be open at a time, and setupPinPad
carries a note saying why it must never be handed a global lookup.
2026-10-03 00:13:41 +02:00
Silent Mode
166e220343 Theseus 0.3.68: quick-links strip opens services in a dedicated side panel
Clicking an icon on the left strip now opens the service inside a dedicated
380-px mini-view (quickPanel) next to the strip, Opera-style, instead of a
new full-sized tab. Click the active icon again to close the panel; click a
different one to switch. If the panel is already pointed at the same host,
we skip the loadURL so scroll position, open chat and login state survive
a close+reopen round-trip.

Icons now paint as real brand SVGs (Messenger, WhatsApp, Telegram, X,
YouTube, Spotify) with their official colours, bundled inside quicklinks.html
so no external favicon fetch leaks the fact that the strip is loaded.
Unknown ids fall back to a letter chip. The strip vertically centres the
icons between two flex spacers to match Opera's layout.

Defaults ship Messenger, WhatsApp, Telegram, X, YouTube and Spotify. The
Settings › General › Quick links section still lists / adds / removes
entries and toggles the strip.
2026-10-03 00:09:44 +02:00
Silent Mode
08ecd093d2 Theseus 0.3.67: Opera-style quick-links strip on the left edge
New thin vertical column (44 px) on the left side of every page, Opera-style.
Click an icon to open its web app in a new tab; if a tab is already open on
that host, we focus it instead of stacking another one. Hidden in HTML
fullscreen so a video still fills the window; toggle via Settings › General ›
Quick links › Show the strip.

Defaults ship X, Telegram and WhatsApp. The Settings › General › Quick links
section lists the current entries with a Remove button each and a Title + URL
+ Add row that auto-prefixes https:// and auto-fills the title from the
hostname when empty. Edits write the whole settings.quickLinks array; the
strip view and the window layout react through settings-set, so no restart is
needed.

Settings › General › Updates panel also now runs standalone (no longer gated
by anything in the shared cfg.get().then() init), so a thrown exception in an
unrelated feature can't leave it stuck on "Loading…" any more — the version
line reads immediately and Check for Updates stays functional.
2026-10-02 23:49:39 +02:00
Silent Mode
cb4d900c1a Theseus 0.3.66: Updates panel is now truly independent of the shared Settings init
0.3.65 wrapped the Updates panel code in try/catch but still left it inside the
big C.get().then((s)=>…) block. If anything earlier in that block throws on a
specific profile — a feature's addEventListener on a missing element, a settings
read that rejects, anything — the panel's code never runs and the user sees
"Loading…" forever regardless of the try. Users reported this still happening on
0.3.65.

Updates panel now runs standalone right after the shared constants, as its own
immediately-invoked function, with no dependency on cfg.get() or any other
Settings init. Everything it needs (appVersion/recheckUpdate IPCs and two DOM
elements) is already available at script time. It will show "You're on vX.Y.Z"
or a specific error, never a stuck placeholder.
2026-10-02 23:04:59 +02:00
Silent Mode
a02deffe61 Theseus 0.3.65: Updates panel can't silently fatal into "Loading…" any more
Settings › General › Updates used to stay on "Loading…" forever if anything
earlier in the shared C.get().then((s)=>…) init threw, or if the version IPC
rejected — the .catch(()=>{}) on the version fetch swallowed it. Users then
had no in-app way to run "Check for updates", because the button sits in the
same panel.

Panel init now wraps in its own try, surfaces the actual error on the status
line (missing IPC / fetch rejection / init failure), and keeps the Check for
Updates button functional even when the earlier version read fails. Toolbar
chip is unaffected either way — the auto-updater runs independently.
2026-10-02 22:45:36 +02:00
Silent Mode
565972a23b Theseus 0.3.64: cleaner window title — "Theseus Navigator — is not responding", not the tagline
Windows's "App is not responding" dialog and Task Manager read FileDescription
from the exe's VERSIONINFO resource, which electron-builder sets from
package.json's description. The marketing tagline sat there, so a frozen tab
produced "Theseus Navigator — a browser that follows the thread. By Silent
Mode, a Deviant project. is not responding". Override via build.extraMetadata
so only the built asar's description is clipped to "Theseus Navigator"; the
source description stays as-is for npm metadata.

(The "not responding" freeze itself is fixed in 0.3.63 by the lazy tab
restore — users still on 0.3.62 will see it until they take 0.3.63.)
2026-10-02 22:15:14 +02:00
Silent Mode
3c35b2605b Theseus 0.3.63: lazy tab restore, Privacy language simplified, chip reloads the page
Session restore now paints the full strip from the saved titles + favicons and
loads only the ACTIVE tab's page; every other restored tab lives as a dormant
WebContentsView and navigates for the first time when the user clicks it. For
a 20-tab user that drops cold start from 20 renderer loads racing chrome.html
to one, so launch is roughly flat whatever the tab count — fixes the "not
responding" freeze on a session with many restored tabs. session.json is now
v3 ({v:3, tabs:[{url,title,favicon}], active}); v1/v2 session files still
parse (their tabs restore lazy without a cached title, which arrives on first
activation). Reload on a dormant tab materialises it.

Privacy › Anti-fingerprinting › Language is now two modes — Automatic (system
language) and Manual — matching the General › Website language row and the
URL-bar globe chip. The old Spoof-choose top-10 and Hide-en-US modes are gone
from the UI; legacy saved values auto-migrate to Automatic on first open. The
Manual list is the same 24 languages the General row uses, kept in one place
(WEB_LANG_LIST), so all three surfaces stay in sync.

Changing the language via the globe chip or either settings row now reloads
the active tab — the server picked the response body from Accept-Language on
the original request, so an already-rendered page can't adopt the new language
on its own. A reload is what a user clicking a one-click language switch
expects.

The Location row's country dropdown now stacks under the mode dropdown on its
own line when Manual is picked, so an open menu above it can't visually cover
it (the row's flex-row max-60% layout could wrap it where another dropdown's
overlay sat).

Also: the settings-update broadcast now reaches every open settings tab, not
only the chrome — so changing the chip updates both the General Website-
language row and the Privacy Anti-fingerprinting Language row live without a
Settings refresh.
2026-10-02 21:51:28 +02:00
Local Dev
be05fe67d4 feat(aegis): choose when Aegis asks for the PIN
Reported: after a restart Aegis opens without asking for a PIN, then demands
one as soon as you click something. That came from the only control being
requirePinForSending — safeStorage remembers the master password, so the
wallet reopens unlocked, and the PIN prompt then ambushes the first action.
Asking at the door is coherent. Asking nothing is coherent. Asking once the
user is already inside is not.

"Ask for PIN" is now four independent triggers, because wanting one at
startup and again per transaction is a normal combination:

  - On each browser restart  (compares a per-process boot id; a timestamp
                              cannot tell a restart from a long idle)
  - On each wallet launch     (hide/show — one panel load is one launch)
  - Every 6 hours
  - Each transaction

With none ticked, an open wallet is never interrupted again. The decision is
made host-side: the panel reloads on every hide/show and must not be the
thing that remembers a gate was cleared. A clearance is only recorded after
the panel has actually decrypted the PIN blob, which is proof rather than a
claim, and ticking a trigger does not fire it retroactively.

restart defaults ON for a wallet that otherwise reopens fully unlocked, and
transaction inherits the old requirePinForSending so nobody loses a gate they
had chosen. Removing the PIN clears every trigger and the clearance record.
2026-10-02 20:46:36 +02:00
Local Dev
123b7ad8e9 fix(aegis): imported chipnet wallets linked to the dead web explorer
Every history row and Explorer button on an imported chipnet wallet opened
https://chipnet.imaginary.cash/tx/… , whose web interface has been returning
502 for weeks. chain-bch.js was moved to our own explorer and this adapter
was missed — which is most chipnet wallets in practice, since a keystore bulk
import produces imported ones.

Points at https://aegis.x/explorer/chipnet/ like the HD adapter. The host's
Electrum endpoint on :50004 is a separate thing and is still in use.
2026-10-02 20:45:53 +02:00
Local Dev
57177a3439 fix(aegis): refresh was a silent no-op, and reported success either way
Three separate reasons the Refresh button looked dead:

- wallet.js refresh() returned immediately when state.scanning was set, so a
  manual press during a background poll did nothing at all. A forced refresh
  now awaits the in-flight pass and then does real work; background polls
  still yield. scanning is only ever written inside doRefresh, which only
  refresh() calls, so scanning implies a pending inflight to wait on.
- The adapters catch their own fetch failures onto state.error instead of
  rejecting, so awaiting refresh() proved nothing and refreshChain reported
  ok:true for a wallet that had just failed against a dead server. It now
  reads the snapshot back.
- Success changed only a tooltip. Balances that were already current left the
  screen identical, which is indistinguishable from a broken button. It now
  flashes a result and says how many wallets were refreshed, or how many
  failed and why.

Version bumped once for this batch; not published yet.
2026-10-02 20:34:01 +02:00
Local Dev
fce2331701 fix(aegis): 0.25.2 — one way out of the address list, not two
The coin drilldown header carried "← Back" on its left and ✕ on its right.
They were bound to the same handler and carried the same tooltip, so the row
spent its left edge on a duplicate of the control at its right edge. ✕ stays;
the title now starts at the left edge, which .wctitle already handled via
flex: 1.
2026-10-02 20:17:06 +02:00
Local Dev
b56c63a365 fix(aegis): 0.25.1 — the wallet list follows the network switch
Switching network on the new header chips moved the selected wallet but left
the list below it alone. In the addresses drilldown stripView.groupKey pins
"<chain>:<network>", so the header read chipnet while the rows underneath
were still mainnet addresses.

The chip now retargets that groupKey when the drilldown belongs to the chain
being switched, and leaves a drilldown on any other chain alone. The coins
view already keyed off activeNetworkByChain and needed nothing.

Switching also lands on whichever wallet that network was last left on
instead of the first in list order, which means row clicks have to record
that — otherwise picking a wallet, leaving the network and coming back
returned somewhere else.
2026-10-02 20:05:27 +02:00
Local Dev
a04eb5406a feat(aegis): 0.25.0 — balance first, one network control, status last
The header read bottom-up. The balance sat under a row of connection detail,
and the network you were on was reported in that status row while the control
that could actually change it was a chip row buried in the picker's coin
drilldown — two places for one idea, with the useful half two clicks deep and
only reachable from inside a wallet list.

Reordered to match what people open the panel for:

1. wallet label
2. balance and portfolio
3. network selector
4. connection status and the per-wallet verbs (Explorer, Faucet)

The selector and the indicator are now the same control: a chip per network
this coin has wallets on, the active one marked, always present while a
wallet is selected. Switching picks a wallet on that network and keeps the
picker's per-chain network memory in step, so the two views cannot disagree.

A single-network chain still renders its one chip. It is the indicator the
status row used to carry, and hiding it would make the header height jump as
you move between chains. A testnet whose label already says so (Chipnet
testnet) no longer also gets a TEST tag; Nile and Sepolia still do.
2026-10-02 18:24:42 +02:00
Local Dev
0b4ddee8c5 feat(aegis): 0.24.0 — ask the chain which derivation path holds the funds
A seed import offered exactly one prefilled path and no alternatives, so a
seed from a wallet on a different path derived a valid but empty address and
reported a 0 balance. Nothing distinguished "wrong path" from "empty wallet",
which is why repeated imports of a funded wallet all looked identical.

Both real cases hit it. Bitcoin.com derives mainnet BCH from coin type 0' —
its Copay lineage predates the 145' split — while Aegis prefilled
m/44'/145'/0'/0/0. And chipnet tooling derives from 145', while Aegis
prefilled BIP44's testnet 1'.

- A preset dropdown per coin and network, each entry naming the wallets that
  path belongs to, with the free-text field kept for anything unlisted.
- "Check which path has my funds" derives each candidate and asks Electrum
  for balance and history, five addresses deep per candidate so a wallet
  whose first address is spent clean is still found. Results are listed with
  balances and a one-click Use; when nothing matches, it says so instead of
  implying the wallet is empty.
- The chipnet IMPORT prefill becomes 145'. The vault-derived chipnet account
  path stays m/44'/1'/0' — changing that would move the addresses of wallets
  that already exist.

The scan derives, queries and returns counts; it stores nothing, and the
mnemonic crosses the same boundary importWallet already uses.
2026-10-02 00:39:54 +02:00
Local Dev
c82aad16f3 fix(aegis): 0.23.0 — chipnet wallets were in the pairing list, just buried
Reported as "WizardConnect pairing is done only via Mainnet, chipnet is not
available on UI". Chipnet pairing works and is not filtered anywhere: the
Connect pane reads state.wallets unfiltered, wcPairableWallets() keys only on
chain/phase/eligibility, and startForWallet registers every BCH wallet
whatever its network. The profile even holds a live wc:// URI against
bch-chipnet-0, so a chipnet wallet has already paired.

What it actually was: the picker listed wallets in registry order, and a bulk
keystore import put fifteen unpairable chipnet WIF wallets ahead of the two
chipnet HD wallets that pair fine. Every visible row was a greyed-out
"can't pair" testnet entry, so the list read as "chipnet is unsupported"
while the working options sat at positions 17 and 19 of 19.

Group the select instead: "Can pair" first, "Cannot pair" after. The reasons
stay visible — they were asked for, and a wallet greyed out with no
explanation reads as broken — but they no longer hide the wallets that work.

Only a WIF import is ineligible, because hdwalletv1 hands the dapp an xpub
and one key is not a key tree. Promote to HD (0.11.0) remains the fix for
those.
2026-10-02 00:15:06 +02:00
Local Dev
6c5fa9cea8 feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN
Getting a key back out of Aegis only worked for wallets it derived itself.
Every imported adapter's recovery() returned xprv:null with "Recovery lives
in the source of the import", so the wallets most likely to need exporting
were the ones that refused, and the vault-derived ones handed over an xprv
behind nothing but an approval click.

There is one gate now, and it is enforced in the host. revealSecret takes the
master password and verifies it with vault.lifecycle.unlock before it reads
anything; the panel obtains that password either by decrypting the PIN blob,
which wraps exactly it, or by asking. Both routes end at the same proof, so
the host never takes the panel's word for authorisation. The old
recovery({reveal:true}) path is gone and all six Settings buttons route here.

Three wrong PINs switch to the master password rather than dead-ending, and
those attempts still count toward the existing 15-minute lockout, so a
fumbled PIN costs nothing and a guessed one gains nothing. Being locked out
of the PIN also falls through to the password: the lockout exists to stop PIN
guessing, not to lock an owner out of their own key.

"Use PIN to show secret keys" defaults ON, unlike the send flag — a send is
already fronted by an approval overlay, whereas a revealed key is
irreversible the moment it is on screen. Turning it off moves the prompt to
the master password. There is deliberately no setting that reveals a key
without asking for anything.

It is NOT called a recovery phrase, because Aegis has none to show. An import
stores mnemonicToSeedHex(words) and discards the words, vault wallets are
HKDF(vault root, purpose) and never had words, and password-vault.js is
explicit that the seed is never persisted. So each form names itself — WIF,
private key (hex), wallet seed (hex), wallet key (hex) — and says where it
can actually be restored. Someone who writes down what this shows believing
it is twelve words has backed up nothing, which is the one outcome this
screen has to prevent.
2026-10-02 00:04:06 +02:00
Local Dev
838d686598 whitepaper: corrections from a review against the code; trademark note covers every brand kit 2026-10-01 22:28:11 +02:00
Silent Mode
f4514946bd Theseus 0.3.62: picker says just "English", not "American English"
Intl.DisplayNames.of("en-US") returns "American English", which spells out a
distinction the picker doesn't make — one row per language, with English the
UK original. Pass the base code to Intl so the chip tooltip, the "Automatic
(…)" label and the Settings hint all read as the plain language name
(English, Russian, Portuguese, Chinese) regardless of which regional variant
the OS or the saved setting happens to be.
2026-10-01 22:14:38 +02:00
Local Dev
93828a0172 fix(theseus): Presearch is frozen in the engine catalog
presearch.com has redirected every request, searches included, into a
dead host since 2026-09-28, so a user who picked it gets Cloudflare's
origin error instead of results. Rather than deleting the entry, a catalog
engine can now carry a frozen reason: it stays listed in Settings, greyed,
with the reason as its tooltip and an Unavailable badge where the switch
was; it is never enabled, never in the picker, never accepted as the
default from any path, and a profile that had it as default falls back at
startup. Turning it off still works, and deleting the field brings the
engine back exactly as the user had it.
2026-10-01 01:24:10 +02:00
Silent Mode
deae55647c Theseus 0.3.61: language picker — merge regional variants, sort by speakers
Sits on top of the earlier 0.3.61 commit (bundled into the same shipped build):

Spanish and Portuguese collapse to their originals — es-ES and pt-PT — and the
Mexican / Brazilian variants come off the picker (same 2-letter chip, roughly
the same text). Ordering is now global-speakers ranking with European
languages first, so the languages a European desktop is most likely to want
sit at the top: English, Español, Français, Português, Русский, Deutsch,
Italiano, Türkçe, Polski, Nederlands, Ελληνικά, Čeština, Svenska, Suomi —
then the non-European tier led by 中文, हिन्दी, العربية and so on.
2026-10-01 00:59:23 +02:00
Local Dev
6bb3c50d0d feat(aegis): 0.21.0 — you pick which wallet pays and which one dapps get
A WizardConnect pairing could land on an address the user had never seen.
Pairing took the selected wallet when it qualified and otherwise the first
pairable one in list order, so with the selected wallet ineligible (a WIF
import has no xpub) it silently fell through to whichever wallet happened to
be first. The approval named that wallet by label only, which does not help
when the label is one Aegis generated.

Three changes, one idea: the wallet a purpose uses should be something you
said, not something that fell out of list order.

- Roles. A wallet can be nominated for payments and/or for WizardConnect,
  set from its manage modal and badged on its row. A role that points at a
  removed wallet reads back as null instead of being trusted, so a stale
  pointer can never quietly redirect a payment.
- The pairing approval asks. With more than one candidate it offers a
  dropdown of them, each labelled with its own short address; with only one
  it shows that wallet's full address. The rows are static, so naming a
  wallet above a select the user can change would contradict itself — hence
  one or the other, never both. The panel's own Connect pane now follows the
  same precedence, because two rules for "which wallet" is how a pairing
  surprises someone.
- Send gets a From row listing the wallets on this coin and network, with
  balances. It switches the panel selection rather than carrying a separate
  source: planSend and send resolve the wallet host-side from that, and a
  second notion of "current" would let the form and the approval disagree.

Payments also becomes the opening selection when nothing has been picked yet,
which is what nominating it is for.

No Theseus release needed — approvalModal has supported a select row all
along, and the pick comes back as "allow+wallet=<id>", validated against the
options offered.
2026-10-01 00:59:17 +02:00
Local Dev
596ea09fc7 feat(theseus/ariadne): settings panel — policy + per-source toggles + status report
Ariadne 0.1.13 exposed /api/status and per-source enable flags in
policy.json. Theseus's Plug-ins > Ariadne's Thread sub-page now wires those
into a full UI, no daemon restart, no UAC.

Added to the plugins-ariadne sub-page (after Status, before Remove):

  Collision policy   -- radio group (BCNR-first / ICANN-first) writes
                        C:\ProgramData\Ariadne\policy.json.policy; hot-reloaded
                        by the daemon within 5 s.
  Sources            -- 3-column grid, one row per source (snapshotHttps,
                        electrumWss, perQueryLookup, diskCache, localApi):
                        enable checkbox + last-state summary
                        (last success / last error / hit-miss counters /
                        disk-cache size+mtime). Toggle writes
                        policy.json.sources.<name>.enabled and re-polls after
                        the 5-s hot-reload tick so the state text catches up.
  Status report      -- <pre> JSON dump of GET http://127.0.0.1/api/status
                        with Copy report + Refresh report buttons. This is
                        the paste-me-into-support artefact for any diagnosis.

IPC wiring:
  main.js
    ariadne-get-status  -> GET http://127.0.0.1/api/status  ({ok, status|error})
    ariadne-get-policy  -> read C:\ProgramData\Ariadne\policy.json (or {})
    ariadne-set-policy  -> merge {policy}, write back (validates enum)
    ariadne-set-source  -> merge {sources.<name>.enabled}, write back
                          (validates against the known 5 names)
  settings-preload.js
    ariadneGetStatus, ariadneGetPolicy, ariadneSetPolicy, ariadneSetSource

All four handlers write policy.json as the local user; no UAC. Works because
install.ps1 grants BUILTIN\Users Modify on the file (0.1.7+).

Sub-page auto-refreshes state every time it opens (listens on the existing
'section' custom event dispatched by showSection).

Not building/shipping Theseus here -- this rides the next Theseus release.
Panel gracefully handles: daemon down (shows 'Daemon unreachable' with a
pointer to the Status toggle), localApi disabled (daemon returns 503, panel
shows the error), missing policy.json (all sources default to true).
2026-10-01 00:51:50 +02:00
Silent Mode
a3fb8917c3 Theseus 0.3.61: Privacy tidied — country dropdown, VPN row honest, language names in the picker
Privacy › Location is three modes now: Show real, Hide, Manual. Manual reveals a
50-country dropdown whose pick becomes the coordinates navigator.geolocation
returns to pages — country-capital granularity, no regions or free-form cities.
Old profiles on the retired "Spoof (region)" auto-migrate to Manual + the
region's representative country on first open, so nothing breaks.

VPN row in Privacy stops opening the wrong add-on: the sidebar now no-ops on a
specific panelId that isn't registered (used to silently substitute panels[0],
which surfaced Aegis whenever the VPN add-on was disabled), and the row hides
itself when vpn:main isn't in the sidebar panel list.

Language picker (globe chip menu + Settings › General › Website language) drops
the BCP-47 tag from every visible label — the tag surfaces only as the 2-letter
chip in the URL bar once picked. "English" is the UK original; the US variant
row is retired (same 2-letter chip, ~same text). Ukrainian dropped from the
quick list too. "Automatic" reads as the OS language name (Intl.DisplayNames)
instead of a raw en-US style tag.
2026-10-01 00:48:19 +02:00