Commit graph

200 commits

Author SHA1 Message Date
Local Dev
19dc0649dd Theseus Settings: a Sidebar section for the left strip
Quick links sat in a small block under General, and the panel behaviour
added this week (per-app widths, pins, the Opera-style auto-hide) had no
place in Settings at all. Settings › Sidebar now holds the strip switch,
the web apps (drag to reorder like Search engines, pin, reset a remembered
width, remove), the extension panels on the strip (show or hide each one,
pin, reset width) and three switches for when the open panel steps aside:
a new tab, the right side panel opening, a click in the page. Apps gains
"Add to sidebar", so an installed site can also open in the strip.
2026-10-04 21:53:39 +02:00
Local Dev
7907451b24 Theseus: a page click closes the left panel unless it is pinned
Opera's other sidebar rule. A click in the active tab's page now hides the
left panel (web app or add-on panel), so a chat opened for a quick look
gets out of the way on its own. A pin button in the strip, shown while a
panel is open, keeps that app open instead; pins are per app and survive
restarts (settings.leftPanelPinned). The click is reported by
home-preload, which every web tab already runs; main ignores it unless a
panel is open and the click came from the active tab.
2026-10-04 20:58:03 +02:00
Local Dev
29b346250f Theseus: the left panel resizes, and steps aside like Opera's
Web apps in the left panel were a fixed 380 px, too narrow for WhatsApp,
and add-on panels a fixed 400. A grip on the panel's right edge now
resizes it, each app keeps its own width (settings.leftPanelWidths), a
double-click resets it, and the page always keeps at least 320 px. The
grip is a small view of Theseus's own laid over the edge, since the panel
may be a third-party page; while dragging it covers the window so the drag
keeps its events.

Like Opera's sidebar, the left panel now hides when a new tab is opened or
the right sidebar opens. It is only hidden, so reopening lands where the
user left off.

Add-on panels on the left used to get the right sidebar's grip on their
left edge, where dragging resized the right sidebar; that grip is hidden
there and its drags are ignored.
2026-10-04 20:53:16 +02:00
Local Dev
3dbbc452cd fix(theseus): an installed app can be closed and removed; Settings lists apps
An app whose page has a beforeunload handler (CoinSpectrum) could not be
closed: tabs answer the page's "stay?" request, app windows had nothing
listening, and Electron reads silence as a veto. Closing the window is now
always the user's call; a reload or navigation inside the app asks, as a
tab does.

Removing it looked dead for a related reason. The confirmation was drawn
as Theseus's sheet in the main window, where the user was not looking (or
nowhere, with the main window closed), and the removal then closed the app
window with the same call the page vetoes. The question is now asked on
the app window that asked, and removal destroys the window.

Installed apps were only reachable from the address-bar chip while on the
site. Settings gets an Apps page, at theseus://settings/apps, that lists
them with Open and Remove and follows installs, removals and open windows.
The three list calls it uses are settings-only now.
2026-10-04 19:15:11 +02:00
Local Dev
dc7dc75bcc fix(theseus/ariadne): toggle every Ariadne task regardless of install scope
User report (2026-10-04): "Plug-Ins/Ariadne's Thread is not working
properly, it doesn't switch on / off the daemon. Other PC can't turn it
on, and other browsers on it don't recognize BNS names."

Root cause. ariadneSetState's elevated script hard-coded two names,
'BNS Resolver Daemon' and 'BNS Sia Bridge'. Those are the pre-0.2 names
and the All-users / All-browsers compat names. A machine that received a
fresh 0.2.0 install under Just-me scope has NO task with either of those
names -- the resolver there is 'Ariadne BNS Resolver (<user>)' and the
indexer is 'Ariadne BNS Indexer (<user>)'. The toggle silently did nothing
(exit 2 handling returned 'no BNS Resolver Daemon'), and the panel's
state query -- which already knows about 'Ariadne BNS Indexer' -- still
displayed the previous state because it was reading the correct task but
nothing had actually started it.

Fix. The inner script now enumerates scheduled tasks dynamically and
anchors a regex at:
  ^(Ariadne BNS Indexer
   |Ariadne BNS Indexer \(.+\)
   |Ariadne BNS Resolver \(.+\)
   |BNS Resolver Daemon
   |BNS Sia Bridge)$
That hits every Ariadne task across all three install schemes without
touching the user's own unrelated 'BNS Indexer' task (which has no
'Ariadne' prefix -- memory://ariadne-task-names). Enable/Start (or
Stop/Disable) runs against each match; exit code is:
  0 = at least one task accepted the verb
  2 = no Ariadne task found at all (needs reinstall)
  3 = tasks found but none accepted (surface in a clearer error)

Note. Full 0.2.0 integration (named-pipe IPC via Argus/src/lib/bns-pipe.js,
scope-aware policy.json path) ships via step 3 on branch
claude/agitated-bell-bd4ac2 (see TheseusNavigator/DESIGN-bns-indexer-service.md).
This commit fixes the specific toggle bug on master so the user's "can't
turn it on" symptom is resolved WITHOUT waiting for that larger merge.
The 0.1.13 settings-panel sections (collision policy / sources /
status-report via http://127.0.0.1/api/status) still display 'Daemon
unreachable' honestly on 0.2.0 installs; they'll be superseded by the
pipe-based status feed when step 3 lands.
2026-10-04 17:39:53 +02:00
Local Dev
828100e2ce Theseus: warn before opening a name a blocklist flags
The blocklist consumer existed in the resolver library and the gateway, but
the browser opened a flagged name without comment. Now the indexer process
reads the subscribed lists from the chain every ten minutes and hands the
flags to main. A flagged name loads a warning page naming the reason, the
list and the report; the user may continue, and that is remembered per name.

Two gates, because content is reached two ways. loadBns shows the real
interstitial. serveBns refuses with an inline page on every path that skips
it: reload, back and forward, bns:// links, web app windows. The inline page
has no button, since a page at the site's own origin must not be able to
approve itself; only blocked.html may ask to continue, checked by file URL.

Settings › Naming has the policy: warn (default), never open, or ignore the
lists. The csam reason is never offered a way through. A list that cannot be
read keeps the last known flags and never stops a name from resolving.

The gateway put its own warning in front of flagged sites, which this
browser could not get past: it fetches files itself, with no cookie jar. It
now sends x-bns-policy: client and the gateway stays out of the way for a
client that says it decides for itself.

dev/blocklist-selftest.js runs the real protocol handler and decision
functions against a scratch profile.
2026-10-04 15:50:35 +02:00
Local Dev
99d7684590 Theseus: an extension update applied in place reloads its open panels
Applying an extension update without a restart swapped the files and
restarted the add-on, but a panel or tab that was already open kept
running the previous version's page, so the extension looked unchanged
until Theseus restarted (Pithos showed its old layout this way). The
add-on's open left panel, sidebar panel and tabs now reload after the
swap. Extensions can also apply their own staged update with
api.applySelfUpdate(), so their own update button needs no restart;
plug-ins such as wallets still update on the next launch.
2026-10-04 15:28:25 +02:00
Local Dev
f3fbbc5805 Merge ship/mirrors-and-cf: branded error pages and the mirrors docs
These three commits were pushed to the forge's theseus and site repos
from their branch but never reached master, so pushing master would
have dropped them there. Merged so master carries them: the branded
error page on BCNR-to-clearnet fallback failures, the Cloudflare Bot
Fight 503 hint with a .bch mirror suggestion, and the site's docs page
for the p-record reverse-proxy workaround.
2026-10-04 15:14:59 +02:00
Silent Mode
0ce2ab95dd Theseus: translator provider setting — LibreTranslate or Bergamot
A new row in Settings › Language, Translator engine: a radio between the
hosted LibreTranslate backend (the current default) and the on-device
Bergamot engine. The chip and auto-translate route through a single
dispatcher (translatorCall) that picks the provider by setting; adding
or removing an engine only touches the dispatcher.

Bergamot's WASM runtime and model manager are not bundled yet, so the
Bergamot provider falls back to LibreTranslate for now and prints a
one-time console notice. The setting itself is real today — a user can
declare their preference and the real engine lands in a later release
without the user touching Settings again. See bergamot.x for status.
2026-10-04 14:20:18 +02:00
Local Dev
76d342797b Theseus: page scans run in an isolated world; install sheet names the risk
The wiz:// page scan ran in the page's own world, where the page can
replace RegExp, querySelectorAll or Set and shape what the wallet is
handed; it now runs in an isolated world of its own and the results
are checked again in main. The extension install sheet now says what a
community extension can reach while the vault is unlocked (passwords
and the wallet), since extensions still run in the main process.
2026-10-04 04:29:02 +02:00
Local Dev
f834fbf80a Theseus: a wallet approval belongs to the tab that asked for it
Approvals were one global queue drawn over whatever tab was in front,
so a background tab could time a request to pop up while the user was
in the middle of something on a trusted dapp, and a request outlived
the page that made it. The host now carries the tab of a page message
through the add-on's handler (AsyncLocalStorage across its awaits); an
approval from a page shows only while its tab is in front, hides and
comes back re-armed when the user switches tabs, and is cancelled when
the tab closes or navigates. Approvals from a panel or from
WizardConnect have no tab and behave as before.
2026-10-04 04:28:18 +02:00
Local Dev
455e468186 Theseus: scrub stale add-on store copies of the PIN and session secrets
Stores nothing reads any more kept whatever they held when they were
copied: the pre-rename addons-data/ folder, the bchwallet.json left by
the Aegis absorb, and parse-failure copies. For Aegis before 0.31 that
could include the master password behind only an unsealed 6-digit PIN
and the stay-unlocked blob. Those two keys are removed from such copies
at startup; nothing else in them is touched, and the live store is not
among them. extensions-backups/, which kept a copy of an add-on on
every reseed and update forever, is pruned to the newest three per
add-on.
2026-10-04 04:26:39 +02:00
Local Dev
2f7d42d077 Theseus: ids that ever shipped in the bundle stay reserved
Reserved ids came from the current bundle only, so an add-on dropped
from a later release became an id anyone could publish under, and the
newcomer inherited its extensions-data store and vault.derive
namespace. Every id that has shipped is now reserved permanently.
2026-10-04 04:25:23 +02:00
Local Dev
bc1b5fc0f3 Theseus: add-on panels never leave their own page
The right-hand panel had neither a will-navigate nor a window-open
handler, so a link in a panel navigated the privileged view itself to
a remote page that kept sidebar-preload, and window.open made a bare
window with it; panel events were routed by the selected panel id, so
Aegis's state (every address, balances, WizardConnect sessions) would
then have reached that page. Both panels now open web links as tabs and
refuse to navigate away from file://, and events go only to a view that
has the add-on's own page loaded.
2026-10-04 04:23:54 +02:00
Local Dev
f823c042e3 Theseus: wallet-imports and Hermes IPC only answer their own pages
wallet-imports-signer hands out raw seeds and WIFs while the vault is
open, and hermes-* sends and reads the user's Nostr messages; none of
these handlers checked who was asking. No preload exposes the
wallet-imports channels (add-ons use the vaultImports shim), so they
are now Settings-only like the password channels; the Hermes channels
answer only the Messages window.
2026-10-04 04:22:41 +02:00
Local Dev
f214abaf1b Theseus: page dialogs wait while a wallet approval or the unlock prompt is up
A dapp could request a signature and then call alert(): the page's
sheet was raised over the approval, and closing it focused the page
again while the approval's buttons were already armed, so a
double-click on the sheet's OK landed on Approve. Page dialogs and
Theseus's own sheets are now held until the approval or unlock prompt
is answered, and nothing hands focus to the page while one is open.
2026-10-04 04:21:58 +02:00
Local Dev
db1acc1417 Theseus: only theseus.x can ask to install an extension, and the sheet is armed
bcnr.installExtension is in every page's main world, and install links
were honoured from any page and any frame, with no user gesture. Any
site could raise the install sheet timed so that a double-click landed
on Install, whose two buttons are always in the same place; an
installed community extension runs in the main process at once. The
call and the links now work only from theseus.x's top frame, the call
needs a real click (checked in the isolated world), and Theseus's own
sheets ignore every choice except Cancel for 800 ms, like the approval
overlay.
2026-10-04 04:21:05 +02:00
Local Dev
9d6d8c3cc5 Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.

The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
Local Dev
b292408cea Theseus: credit a wiz:// pairing link only to the top document
will-navigate and the window-open handler routed every wiz:// link to
the wallet with the top-level URL's origin, whichever frame raised it.
An ad iframe on a trusted dapp could window.open() a pairing URI and
the pairing prompt would name the trusted site; one click on Pair gave
the attacker the wallet's xpubs and a standing signing channel. A
link must now come from the main frame (navigation initiator, or for
window.open the referrer's origin), and only on pages where the wallet
is allowed by the inject policy.
2026-10-04 04:11:42 +02:00
Local Dev
561cb122ea Vault PIN: tie it to the TPM and never store it unsealed
Theseus's own quick-unlock PIN had the same limit as Aegis's: once the
DPAPI seal is opened (as the user, or from a disk image plus the
Windows password) the 6-digit PIN falls to an offline search. The PIN
is now also the authorization value of a Platform Crypto Provider TPM
key whose secret is mixed into the wrapping key, so the chip's lockout
bounds guessing; lib/tpm-pin.cjs is the same module Aegis uses.

set() now refuses when there is no real OS keystore (Linux basic_text
included) instead of writing the blob in the clear, an unsealed record
from an older build is deleted, and Settings says what the PIN actually
protects against on this machine.
2026-10-04 03:42:02 +02:00
Local Dev
bdc8f951c6 Theseus: built-in extensions can answer paths under a name they ship with
Pithos needs its full-page app at pithos.sia/<user>/<drive>/<folder>
instead of a loopback address. A new site-route capability lets a
built-in add-on declare names in its manifest (siteRoutes) and register
a handler for them; the bns:// handler asks it first for every path but
the root, and a handler that returns nothing hands the request back to
the name's own site. Community add-ons cannot use it, since answering
for a name is impersonating it.
2026-10-04 03:29:30 +02:00
Local Dev
6fd44db1d6 Theseus: the tab that was open at close loads on launch
Restore left every tab dormant, the active one included, so a fresh launch
showed the right tab highlighted over an empty page until it was clicked,
which reads as a broken restore. The active tab now loads as soon as the
toolbar has painted; every other restored tab stays dormant until it is
activated, so launch still costs one page renderer.
2026-10-04 03:03:17 +02:00
Local Dev
a5ce638848 Theseus: a background tab that is playing sound is not frozen
Freezing background tabs stopped music and talks the moment you
switched to another tab, which no other browser does. A tab that is
audible when its freeze is due is checked again later and frozen only
once it falls quiet.
2026-10-04 02:23:08 +02:00
Local Dev
61144e821e Theseus: a widened left panel covers the page instead of squeezing it
Widening an add-on's left panel shrank the open tab to a 120 px strip
and, with the right sidebar open, pushed the sidebar narrower too. Now
the widened panel lies over the tab area like a page of its own, up to
the right sidebar, which keeps its width; the tab underneath keeps its
size. Choosing a tab, opening Settings or a new tab, or using the
address bar narrows the panel back to a bar beside the page, the way a
maximized right sidebar already steps back.
2026-10-04 02:16:23 +02:00
Local Dev
14d9df31cc fix(theseus/error): show branded error page on BCNR→clearnet fallback failures
flytastic.ru (and any other site where the BCNR lookup NXDOMAINs and
the clearnet fallback fails with a cert / network error) was leaving
the tab blank instead of showing the error page.

Cause — tab.internalNav was overloaded. loadBns's fallbackToWeb sets
internalNav=true before loadURL("https://host/") so will-navigate
doesn't re-route the programmatic nav back through navigateTab. The
did-fail-load handler was using the SAME flag to suppress recursion
on its own loadFile(error.html) call — so a cert error during the
clearnet fallback was silently swallowed.

Narrow the suppression to its real target: skip did-fail-load only
when the failing URL is file:// (our own error.html / home.html
loads). Clearnet HTTPS failures from a programmatic loadURL now
surface the branded error page like any other failed navigation. The
will-navigate guard (its original purpose) is untouched.
2026-10-04 01:50:31 +02:00
Local Dev
faea4147d2 feat(theseus/error): CF Bot Fight 503 hint with .bch mirror suggestion
When a top-level HTTPS load returns 503, sniff the body for CF Bot
Fight markers (cloudflare + blocked/challenge/attention-required, or
cf-chl). If it matches, replace the CF interstitial with a branded
error page carrying kind=cf-blocked. The page explains why Electron
browsers get 503 (TLS ClientHello fingerprint below HTTP, no user-
agent tweak fixes it) and offers next steps.

If a .bch name has a `p` record proxying the same origin, the page
surfaces it as a one-click retry — preserving the original path, so
/faq becomes /faq on the mirror rather than the mirror's root. Mirror
lookup walks the warm sharedIndex, so it's synchronous and works
offline. If no mirror exists, links to silentmode.st/mirrors where
users can mint one via Sirius.

Guarded against races — the 250 ms delay before body sniff re-checks
that the tab is still on the same URL and not destroyed, both before
and after the executeJavaScript resolves, so a JS-redirect after the
503 doesn't cause misclassification.
2026-10-04 01:50:09 +02:00
Local Dev
2dfa9e9c3e Theseus: an add-on's key namespace cannot be taken by another add-on
- `absorbs` lets one add-on derive under another's vault namespace. A
  community install has the field stripped, but an update of one is placed
  as shipped, so a second version could declare absorbs:["aegis"] and derive
  the wallet's keys. It is now honoured only for ids that ship inside
  Theseus, at the one place that matters: vault.derive.
- The legacy ids Aegis absorbs ("bchwallet", "siawallet") no longer have a
  bundled folder, so nothing stopped a catalog extension from installing
  under one and deriving `bchwallet/...`. They are reserved at install and
  refused at derive.
- A page-to-add-on message is accepted only from the tab's top-level frame.
  The origin shown to the user is the top-level URL, so a subframe that
  reached the channel would have been credited with its parent's origin.
- The approval overlay ignores everything but Cancel for the first 800 ms.
  A page can raise it without a gesture and knows where the primary button
  lands, which made "double-click here" a way to approve a spend.
2026-10-04 01:41:01 +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
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
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
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
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
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
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
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
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
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
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
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
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
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
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