Commit graph

560 commits

Author SHA1 Message Date
Local Dev
8b74f5cbf9 Merge branch 'master' into claude/zen-archimedes-463527 2026-10-03 23:02:46 +02:00
Local Dev
7185509355 Theseus 0.3.75: previous tabs reopen, extensions start on first use
Ships 2437506 (the restore setting was a no-op with the default quit
clear), the first-use loader for extensions with Aegis 0.29.0 bundled
(~0.7 s less main-thread work at launch, ETH/SOL bridge fixed), the
language picker as an overlay, Pithos 0.3.4 and the Ariadne status row.
2026-10-03 23:02:25 +02:00
Local Dev
7bea16aa28 Merge restore-fix: previous tabs reopen again 2026-10-03 23:02:03 +02:00
Local Dev
71f0c96e93 Theseus: reopen the previous tabs even when history is cleared on quit
"Open previous windows and tabs" and "Clear history on quit" both default
to on, and the quit clear deleted session.json along with the history, so
every launch started from the start page and the restore setting did
nothing. The open tabs are what the user asked to reopen, not history:
while restore is on, the quit clear keeps them and still drops back/forward
and address-bar history. With restore off, the tab list is deleted as
before.
2026-10-03 23:01:56 +02:00
Local Dev
03b497dcae Aegis: DigiByte addresses follow the chosen address family
WalletKeys.entry() built a bech32 p2wpkh address whatever the account
path's purpose, so picking Legacy (D...), Wrapped SegWit (S...) or
Taproot (dgb1p...) in Settings showed a dgb1q address from the BIP44/
49/86 key tree, one no other wallet restoring that path would find.
Each family now derives its own address and script, and spending
supplies what its inputs need (previous tx for P2PKH, redeem script for
P2SH-P2WPKH, tap-tweaked key for Taproot, which is active on DigiByte),
with per-family fee sizes. Unknown purposes are refused instead of
falling back to BIP84.

Also: inputs were signed in selection order but the PSBT orders them by
BIP69, so a spend from two addresses tried to sign each input with the
other's key. They are now signed in the PSBT's order.
2026-10-03 23:01:15 +02:00
Local Dev
03140fae9d Aegis: WizardConnect signing requests can be approved
approvalRequest passed approve/reject keys the host overlay does not
know, so it showed a lone "OK" button whose id never equalled
"approve": every WizardConnect signing request was refused, and the
HTML body was shown as literal markup. It now passes a Sign action and
plain rows: wallet, input count, each output decoded to a cashaddr on
the wallet's network (or OP_RETURN / raw script), total, and who
broadcasts.
2026-10-03 23:01:15 +02:00
Local Dev
5769d837bd Aegis: load the DigiByte deps module as ESM in a dev checkout
lib/dgb/deps.js is ESM, but in a dev checkout the nearest package.json
is TheseusNavigator's, which says "type": "commonjs"; the import threw
and DigiByte was silently unavailable whenever Theseus ran from the
repo. Shipped installs have no package.json above the add-on, so they
were unaffected. lib/dgb/core and lib/dgb/psbt already carry the same
marker.
2026-10-03 23:00:47 +02:00
Local Dev
3d2e3c784b Theseus: bundle Pithos 0.3.4
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs.
2026-10-03 22:57:47 +02:00
Local Dev
f57cf4c894 Aegis: only the page's own scripts can reach the wallet relay
The isolated-world relay accepted any postMessage carrying the fixed
tag "aegis-aegis", including one from a cross-origin iframe (an ad,
an embed), and attributed it to the top-level origin and its grants.
The tag now carries a per-load random nonce that only the injected
main-world bridge knows, and both listeners drop events whose source
is not this window. The document_start install path is unchanged.
2026-10-03 22:54:59 +02:00
Local Dev
206f54edd2 Aegis: Tron signing overlay comes from the bytes being signed
The overlay for tronWeb-built transactions was built from the dapp's
raw_data JSON, which need not match raw_data_hex: a site could show
"1 TRX to X" and get a signature over anything. lib/tron-decode.js
decodes Transaction.raw from raw_data_hex (contract type, owner, to,
amount, TRC-20 transfer/approve calldata, fee limit, memo); the txID
must match the bytes, every contract's owner must be this wallet, and
unlimited approvals or permission/resource delegation get a danger
action.

sendRawTransaction relayed any signed transaction a site handed it;
it now broadcasts only txids Aegis itself signed.
2026-10-03 22:53:58 +02:00
Local Dev
0e7d6cc3c4 Aegis: Solana bridge signs the real bytes and shows what they do
- signAndSendTransaction signed String(Uint8Array) ("1,2,3,..."), so
  every dapp transaction got an invalid signature. Adapters gain
  signBytes(), which signs the exact message bytes.
- v0 (VersionedTransaction) messages were parsed with the version byte
  as the header; the shared parser handles legacy and v0.
- window.solana.signTransaction went through signMessage and its
  "moves no SOL" overlay. It now has its own handler and overlay, and
  signMessage refuses bytes that parse as a transaction (Phantom's rule),
  since such a signature is a valid transaction signature.
- Overlays decode System transfers and SPL transfer / approve /
  set-authority, and list everything else as not decoded.
2026-10-03 22:52:41 +02:00
Local Dev
314bce0905 Aegis: EVM bridge signs what the dapp asked for, chain per site
- eth_sendTransaction dropped the calldata, gas and fee fields, so an
  ERC-20 transfer went out as a 0-value send to the token contract and
  any contract call was broadcast as something else. plan() now carries
  data/gas/fee caps/nonce, estimates gas for calls, and the overlay
  decodes transfer/approve/permit/setApprovalForAll, flags unlimited
  approvals and calldata to a non-contract, and shows estimated vs max
  fee.
- personal_sign signed the hex string ethers/viem send as literal text;
  it now signs the decoded bytes.
- wallet_switchEthereumChain flipped the global selected wallet, so any
  site could move every connected dapp to another chain. Chain is now
  per origin; the sidebar selection no longer redirects a dapp.
- wallet_addEthereumChain silently persisted a connection for chains
  Aegis already had, handing the address to any site. Adding a chain
  no longer grants anything, and RPC URLs must be https.
- eth_sign (blind hash signing) is disabled, as in MetaMask.
2026-10-03 22:51:05 +02:00
Local Dev
4c3d27f1b3 Aegis: a plain Connect now lasts for the session
Connecting without ticking "Always allow" stored nothing, so the
site's very next call (personal_sign, signTransaction, ...) failed with
"not connected". Plain Connect now grants the origin in memory until
Theseus restarts; "Always allow" still persists. Every bridge (BCH,
Tron, EVM, Solana) checks grants the same way, revoke clears both, and
the connected-sites list shows EVM/Solana grants and which ones are
session-only.
2026-10-03 22:49:15 +02:00
Local Dev
da56187b39 Aegis 0.30.0: start the dapp-bridge audit batch
The 2026-09-13 audit of the dapp bridges found real signing bugs whose
fixes were never committed; 0.30.0 carries them, ported onto the current
add-on.
2026-10-03 22:47:48 +02:00
Local Dev
4f745406d8 Settings > Ariadne's Thread: Refresh/Install/Update live in the Status row
They had a row of their own with nothing else in it, which read as an
empty card with a stray button. Same layout as the Plug-ins list row now:
buttons beside the on/off switch.
2026-10-03 22:43:30 +02:00
Local Dev
282884092d Merge theseus-lazy-start: extensions and Aegis start on first use
Bundles Aegis 0.29.0, which starts on first use and fixes the ETH/SOL
bridge that went missing on most pages.
2026-10-03 22:40:13 +02:00
Local Dev
9710a22d7a Theseus: bundle Pithos 0.3.3
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs.
2026-10-03 22:12:45 +02:00
Local Dev
e150cfb442 Theseus: bundle Pithos 0.3.2
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs.
2026-10-03 22:10:01 +02:00
Silent Mode
fa50f97e6f Theseus: language picker is a floating overlay now, not a native menu
The globe-chip menu was a native Electron menu — square corners, system
font, no theming beyond the OS's own context-menu paint. Replaces it with
a floating overlay WebContentsView (lang-picker.html + preload),
following the same pattern the engine picker and the popover already
use: rounded 12px surface, acid-tint accents on the current pin, soft
shadow, dark + light scheme, flush under the chip's bottom-right.

The content is organised around the user's intent — translate first,
pick a language second. The "Translate this page" row sits at the top
when it is actionable (web tab + supported source + supported target),
with the detected source and the target under the label so the user can
tell what the backend will do before they click. Once a page is
translated, that row flips to "Show original (<source>)". Below the
action strip is Automatic + the six supported languages, each with a
two-letter code chip in the left slot and a ✓ on the current pin.
Unsupported languages are hidden by default inside a collapsible "More
languages (translator coming later)" group — click to expand, click to
collapse; a pinned-unsupported auto-expands so its ✓ stays visible.

Plumbing matches engine-picker: deferred-load WebContentsView,
closeOnClickAway, setBounds anchored under the chip, picker renderer
reports its own content height after each render so the overlay
contracts and expands with the "More languages" toggle. State pushes
come from emitTranslateState (so the Translate / Show-original row
updates when a page finishes auto-translating with the picker open)
and from broadcastSettings (so a Settings-side language change
re-paints the ✓).

Verified end-to-end: picker loads, shows Automatic + 6 supported in the
default list and 18 greyed in the "More" group, repaints to "⟲ Show
original (Spanish)" after an auto-translate completes.
2026-10-03 21:33:16 +02:00
Local Dev
987aca57a8 Theseus: bundle Pithos 0.3.1
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs.
2026-10-03 21:25:25 +02:00
Silent Mode
a2c43d6a22 Theseus: language menu stays short, language change always re-translates
The chip menu showed every entry in WEBSITE_LANGUAGE_QUICK (24 rows, most
greyed with "— translator coming later") and the picker read as a wall
of coming-soon noise. The top strip now carries only the languages the
translator actually handles — Automatic plus the six supported ones
(en, es, fr, de, el, ru) — and everything else moves into a "More
languages (translator coming later)" submenu where the greyed rows live
without crowding the main menu. If the user's current pin is one of the
unsupported ones, it stays visible at the top so the ✓ reads at a
glance, not two levels deep.

Translation didn't follow a language change reliably:
- A page with no `<html lang>` left pageLang empty, which the auto-
  translate hook took as "no translation needed" and skipped the entire
  page. Now an empty source is still translated (the backend auto-detects
  the real language), and the hook only skips when pageLang is known AND
  matches the user's target.
- Changing the pin via Settings or the chip reloaded the active tab but
  did not re-translate — the hook needs auto-offer on, and even then
  a `pending` latch set by setWebsiteLanguage was wiped by the reload's
  did-start-navigation reset. The `pending` bit now survives that reset,
  so the did-finish-load hook translates unconditionally for an explicit
  language switch (the user ASKING for a new language IS the request to
  translate the current page too). A translated page is reverted before
  the reload so the fresh HTML lands on original DOM, not a mix of old
  translated nodes and new content. Verified end-to-end: an es→en auto-
  translate followed by a pin to French takes the page to French in one
  shot without the chip being touched.
2026-10-03 21:24:43 +02:00
Local Dev
9f46d932b6 Aegis 0.29.0: start on first use, not at every Theseus launch
Aegis was most of what was left of launch cost: its crypto and
WizardConnect deps block the main thread for ~0.6 s right after the first
frame. It now declares its panel and "activation": "on-demand"; the
dapp bridges are still on every page from the start, and the first page
call, panel open or wiz:// link starts it.

activate() returns a promise the host waits on, so the call that woke
Aegis finds WizardConnect and the mounted wallets. With a locked vault it
does not wait for the mount (derive blocks until unlock), so the page gets
the usual locked answer in ~0.7 s instead of after the host's 5 s limit.

Two things only work while Aegis runs: a live WizardConnect pairing (no one
else listens on its relays) and "stay unlocked" (which also opens
Settings > Passwords). While either is on, Aegis asks the host to start it
at launch, and withdraws the request when both are off. Pairings left
behind by a removed wallet do not count.

Boot trace, same profile, 3 warm runs: main thread blocked in the first
6 s 970-1040 ms -> 300-320 ms, longest block 605-667 ms -> 227-244 ms,
toolbar 1.34-1.61 s -> 0.83-0.87 s.
2026-10-03 21:17:39 +02:00
Local Dev
129e4c6729 Aegis: the ETH and SOL bridge went missing on most pages, for good
The main-world bridge is appended at document_start, which often runs
before the page has an <html> element. The append threw on null, and the
catch marked the origin as Trusted-Types-blocked in localStorage, so every
later visit skipped window.ethereum, window.solana and the EIP-6963
announcement on that site. On example.com it failed on 6 of 6 loads.

Wait for <html> with a MutationObserver (it fires at the microtask
checkpoint before the first parser-inserted script, so the bridge is still
first), remember only real Trusted Types refusals, and use a new key so the
origins wrongly marked by the old one get the bridge back.
2026-10-03 21:17:38 +02:00
Local Dev
278da658ce 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.
2026-10-03 21:07:44 +02:00
Local Dev
e65c5bea52 Merge Aegis 0.28.1: bundle the wallet users already get over the air
Fresh installs started on Aegis 0.9.0 and froze on large stores until the
OTA caught up. Bundling 0.28.1 makes the first launch the fixed one.
2026-10-03 21:07:11 +02:00
Local Dev
61153481a6 Theseus 0.3.74: vault PIN, extension panels on the left, Pithos 0.3.0
Ships 08b101f (a quick-unlock PIN for the vault, shared with extensions
through api.vault.requestUnlock), 3fd0fe3 (extensions can open from the
left edge, beside the quick links), Pithos 0.3.0 bundled (c2df6e3: guided
setup, PIN gate, recovery phrase from the vault, on the left) and e1c5d91
(one language chip with "Translate this page" and automatic translation).
2026-10-03 20:49:51 +02:00
Local Dev
7087ab57ca Theseus: extension panels on the left edge
Extensions could only put panels in the right sidebar. An add-on can now
register a panel with side: "left": its icon joins the quick-links strip
(above the web-app links), and it opens in the strip's panel slot in its
own view with the sidebar preload, so the add-on bridge, events and the
widen/narrow controls work as on the right. A left panel and a quick-link
web app never share the slot; reopening keeps the panel's page and state.
Left panels drop out of the right sidebar and its dock. With the strip
turned off they fall back to the right sidebar so they stay reachable.

Pithos is the first: bundled copy rebuilt with side: "left".
2026-10-03 20:48:55 +02:00
Local Dev
a3d7c90b78 Theseus: bundle Pithos 0.3.0 (PIN gate, vault-derived phrase, guided setup)
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs.
2026-10-03 20:42:23 +02:00
Local Dev
de7735feb3 Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:

- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
  wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
  and the result is sealed with the OS keystore (safeStorage: DPAPI /
  Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
  elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
  lives in the same file, so a restart does not reset it; a successful
  master-password unlock does. A PIN whose password no longer opens the
  vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
  the master password. Extensions call api.vault.requestUnlock({ reason })
  (vault-derive capability) and get { ok } back; what the user typed never
  reaches them. Settings' locked screen offers "Unlock with PIN" through
  the same prompt.
2026-10-03 20:33:26 +02:00
Silent Mode
3536cd89bd Theseus: one language chip, auto-translate, picker owns up to what works
Two chips carried the same word in two shapes — a globe (Accept-Language)
and a translate chip (chip lights when page lang differs) — both labelled
"RU" at the same time for a Russian user. The chip for translation is
gone. The globe menu now covers both: a "Translate this page from X to Y"
item appears at the top when the loaded page is in another supported
language, flipping to "Show original" while a translation is on screen.
The chip's own code still shows the user's language (EN, RU, …); its
tooltip switches to "Translated to <X>. Menu: Show original." when a
translation is up, so the one chip reads the whole state.

With "Translate automatically" on, Theseus translates in place on
did-finish-load the first time it sees a supported source + target
mismatch for the active tab — no chip-click needed. A `_tr.autoTried`
latch keeps it to one attempt per document (a failing backend doesn't
retry on every reflow), and the latch resets on did-start-navigation so
the next page gets a fresh shot. The setting copy in Settings › Language
now says "Translate automatically" instead of "Offer to translate", so
the switch's label matches the behaviour.

The picker (both in Settings and in the globe menu) still lists every
language in WEBSITE_LANGUAGE_QUICK, but entries whose base code isn't
on the translator backend (en, es, fr, de, el, ru today) are shown
greyed out with "— translator coming later", and "Other… (Accept-Language
only, no translation)" is explicit about what free-form tags buy you.
The menu is a roadmap, not a lie: a user picking one of the greyed
entries sets Accept-Language and nothing else surprises them.
2026-10-03 19:37:04 +02:00
Local Dev
8186407b61 Theseus: bundle Pithos 0.2.0 (sidebar panel)
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs. Pithos moves from a toolbar menu that opened a loopback tab to a left-sidebar panel. Existing installs get the same version over the extension update channel.
2026-10-03 19:34:55 +02:00
Local Dev
1e461e9b83 Theseus docs: pithos is a live extension update channel 2026-10-03 19:20:41 +02:00
Silent Mode
b0206f9c1e Theseus 0.3.73: Updates description — honest about the startup retry
0.3.72 shipped without the Settings › General › Updates copy update:
"Theseus already checks the release manifest at boot and every 6h" is
accurate for a successful first check, but silent about the retry
backoff (8s, 30s, 2min, 10min, 30min) that fires when the startup
attempt is offline — on a slow or captive-portal connection the user
would see several checks in the first 43 minutes and the copy made
that look like a bug. The new wording owns the retry.
2026-10-03 19:20:32 +02:00
Silent Mode
c61fef08dc Theseus: Updates description — honest about the startup retry 2026-10-03 19:09:47 +02:00
Local Dev
c02784a7d0 Theseus 0.3.72: Pithos built in, Language settings page
Ships 70b7325 (Pithos, the s3d control panel, as a bundled extension:
open it from the dock to run your own S3 gateway on Sia), dae6b9a
(Settings gets its own Language page), f3efae3 (translator served from
silentmode.st/libre with libre.x / lingua.x) and 604a990 (BNS names read
from Ariadne's indexer when it is installed, Theseus's own as standby).
2026-10-03 19:09:33 +02:00
Silent Mode
88044952e3 Theseus: Settings grows its own Language page
Website language, offer-to-translate and the translator peers list used
to sit as three sub-sections inside General, and Privacy › Anti-
fingerprinting duplicated the language picker on top of them — three
places to edit the one languageMode/languageValue setting. Settings
grows its own Language entry in the sidebar now; everything about
language — the preferred-language picker, the auto-offer toggle, the
peer list, the API key — lives there, and the Privacy duplicate is
gone. The chip in the URL bar still edits the same setting, so the
toolbar surface is unchanged.

The copy for the picker ("Your language") now says what the setting
actually drives: it's sent as Accept-Language and it's the translator's
target. Legacy "hide"/"spoof" language modes migrate to Automatic on
first open of Settings (the picker only has Automatic / a tag / Other),
so a profile that still carries one of those from an older release
lands on a valid state the first time Settings opens.

Greek (el-GR) was already in the picker; this release's silentmode.st/
libre backend added `el` to --load-only so the translator now has real
en↔el support — the picker and the backend are in step.
2026-10-03 19:05:25 +02:00
Local Dev
4ad95b3c7a Theseus: bundle the Pithos add-on
Ships Pithos (the s3d control panel) as a built-in extension: the dock
menu opens it in a tab served from a loopback port, and "Stop s3d"
shuts the gateway down. Generated by Pithos/scripts/build-theseus-addon.mjs.

yaml is vendored under vendor/yaml/lib rather than node_modules/ or
dist/, because Theseus git-ignores both and a clean-worktree release
build would otherwise ship the add-on without its only dependency.
2026-10-03 19:03:05 +02:00
Local Dev
7254ffe6f4 Theseus: read BNS names from Ariadne's indexer; its own is the standby
Migration step 3 (DESIGN-bns-indexer-service.md). Ariadne's Thread now owns
BNS indexing on the machine, so Theseus no longer runs a second electrum
indexer beside it.

bns-indexer.js keeps its process and its messages to main.js, but inside
it is now an index host on the shared source chain:
- Ariadne's indexer over its pipe, trusted only after ariadne-helper.exe
  has checked the server process on that connection (found through
  Ariadne's uninstall key), then pushes;
- the local copies: Ariadne's files for both scopes, Theseus's own raw
  copy, the bundled one. The richest wins.
- Theseus's own index copy, written from the pipe data.

The shared core runs as Theseus's own indexer only while Ariadne is
unhealthy. That means: no pipe 4 s after launch, a pipe that fails the
check, a pipe that went silent, or an index not confirmed for 10 min while
Ariadne is not paused. The own indexer warm-starts from Ariadne's
snapshot, so there is no download and no cold sync. It hands back after
90 s of health, so a flapping service does not start and stop it. Economy
is not a failure and never triggers a takeover. With no checkable Ariadne
(portable, not installed, older than the pipe) the own indexer starts at
once, as in 0.3.70. The one thing Theseus does on Ariadne's side is run
the indexer's task at launch when "Launch at start" is off.

main.js passes the shared module paths (packaged as .mjs, which is why the
shared modules no longer import each other) and keeps the host's status.
The Ariadne panel takes its state from the indexer task when one exists,
and the sub-page says where Theseus's names come from.
2026-10-03 17:06:24 +02:00
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