Commit graph

174 commits

Author SHA1 Message Date
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
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
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
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
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
Silent Mode
df2b1f57ff Theseus 0.3.60: pick your browsing language from the URL bar; per plug-in settings
A globe chip next to the URL-bar star shows the language sites see you in
(Accept-Language + navigator.language) — "AUTO" while following the OS locale,
the two-letter code once you pin one. Click opens a 23-language menu; the same
setting has a friendly row at the top of Settings › General. Both write to the
existing languageMode/languageValue and stay in sync with the Anti-fingerprinting
Language row through a settings-update broadcast (settings.html and chrome.html
both react live).

Settings › Plug-ins is now two compact rows — one per plug-in — with the on/off
toggle on the right and the update controls beside it. Clicking a plug-in's title
opens its own sub-page (plugins/ariadne, plugins/aegis) with the full description
and the Uninstall button, so the main list stays scannable and dangerous actions
stop travelling with the everyday ones. The Ariadne toggle and its sub-page
mirror the same scheduled-task state.
2026-09-30 02:16:11 +02:00
Local Dev
cd7f8759ea fix(theseus): own fullscreen; a video's fullscreen no longer strands the window
Nothing handled HTML fullscreen. Electron put the window in fullscreen for a
page (a video player) with the toolbar still on top, and when the page left
fullscreen while its tab was hidden, or the tab was switched away from or
closed, the window stayed fullscreen: no title-bar buttons, the taskbar
covered, and no key to get out. Tabs now report entering and leaving
fullscreen; the toolbar and sidebar make way for the page; switching or
closing the tab ends it and tells the page; F11 toggles a fullscreen with
the toolbar kept and doubles as the way out. A page's own exit is left to
Electron, which has already taken the window out by the time it tells us;
exiting again during that transition brought the window back maximized.
2026-09-28 20:30:33 +02:00
Local Dev
668914354f fix(theseus): the default-engine control in Settings shows the engine icons
It was a native <select>, which can only show text, so each option carried
an emoji in front of the name; after the engine icons moved into the build
that was the one place still showing emojis. The control is now drawn by
Settings with the same icons as the rows below, grouped like the toolbar
picker, with arrow-key and Escape handling. Choosing a default there also
repaints the toolbar at once: the generic setting write never told it.
2026-09-28 20:07:51 +02:00
Local Dev
d016453f73 feat(theseus): search-engine icons ship in the build; custom engines cache theirs on disk
Every engine icon was an <img> pointing at Google's favicon service, fetched
again each time the picker, the toolbar or Settings rendered. Offline the
whole list collapsed to the emoji fallbacks, and each open told Google
which engines the user has configured. The catalog's icons now live in
engine-icons/<id>.png inside the app; a custom engine's icon is fetched
once (its own /favicon.ico first, the favicon service as fallback), cached
under the profile, and removed with the engine. Settings no longer falls
through to DuckDuckGo's icon service either. Phind ships no icon: its site
serves none through the bot wall.
2026-09-28 19:47:21 +02:00
Silent Mode
8d96e979fa feat(theseus): a subdomain rule applies wherever the name is served from
The gateway checks a name's host rules before it decides what to serve, so a
blocked or redirected subdomain behaves the same whatever record the name
carries. Theseus only inherited that for names it proxies through the
gateway's /bns/ mount. A name with both s3 and ip — the shape that caused
the 2026-08-13 subdomain bug — would have had its blocked subdomain answer
anyway, because Theseus talks straight to the IP.

It now asks the gateway for the host's verified rule before taking either of
the paths it serves itself, and only for those paths, so an ordinary
subdomain navigation gains no round trip. Verification stays in one place:
the client reads a decision, it does not re-derive one.

The spec catches up with what is implemented — it still described v1 and
called hosts a future idea.
2026-09-28 01:25:22 +02:00
Local Dev
27819684d5 feat(theseus): DNS over HTTPS and Global Privacy Control
DNS over HTTPS through Chromium's secure DNS (app.configureHostResolver),
under Privacy › Network: Default protection (encrypted via the chosen
provider, plain if that fails — the default), Increased protection
(always the provider, never plain) or Off, with Quad9, Cloudflare,
Mullvad, AdGuard or a custom resolver URL. Any DoH mode also turns on
Chromium's built-in resolver, as Chrome does. Silent Mode names never
touch DNS, and Tor resolves remotely through the SOCKS proxy, so
neither path goes around it.

Global Privacy Control, on by default, under Tracking protection: the
Sec-GPC header on every request (added in the one request-header hook
beside the client hints) and navigator.globalPrivacyControl in pages.
2026-09-27 22:10:06 +02:00
Local Dev
f1b1de5a9e feat(theseus): Settings pages have addresses and sub-pages; Privacy regrouped
Navigation base, the way Firefox does about:preferences#privacy: the
address bar follows the Settings page (theseus://settings/privacy) and
the hash mirrors it, so every page has a link; a page can have
sub-pages (theseus://settings/privacy/exceptions) with a breadcrumb and
a back arrow; open-settings and theseus:// links accept the two-level
slug.

Privacy now reads top-down: a "Theseus is on guard" card (Shield and
its running total, cookie pop-ups answered, Tor state, version), then
Tracking protection with the Shield and Cookie Pop-ups cards moved here
from Performance and a Manage exceptions sub-page listing the sites
each add-on was told to leave alone (remove to protect again), then
Device access, Anti-fingerprinting, Network (Tor switch and the VPN
panel) and Browsing data. Performance is about resources again.
2026-09-27 22:01:28 +02:00
Local Dev
305bda7c3e bns: one network table instead of chipnet constants copied into fifteen places
Every client hard-coded the chipnet beacons, address prefix and electrum
servers on its own: resolver, registrar config, wallets, gateway, indexer,
mirror scripts, the browser bundle and the mobile Java. A mainnet launch would
have meant finding all of them and hoping none was missed.

The table now lives in resolver-web.js, the one file every client already
shares, so it stays a single-file drop-in. BNS_NETWORK selects the record;
unset means chipnet, so nothing changes today: the live index resolves the
same 60 names and 20 TLDs, the 67 offline tests pass, and the dashboard,
market and studio load the same values through BNS.NETWORK.

The mainnet record carries the verified public servers, the prefix and its
own Sia bucket, but its beacons, start height and operator address are
deliberately null: requireBeacons() refuses to scan until they are pinned in
the order ROADMAP-MAINNET.md §6 requires. Bns.java reads a generated
BnsNetwork.java so the phone cannot drift from the desktop clients.
NETWORK-CONFIG.md records what reads the table and what a launch still pins.
2026-09-27 21:18:37 +02:00
Local Dev
890e6fae4b fix(theseus): quiet add-ons hide from the dock on a fresh profile
autoHideQuietDock ran only when the sidebar state was pushed; on a
fresh boot the chrome pulls it, so Shield and Cookie Pop-ups showed
in the dock until something re-emitted. Run it on the pull path too.
2026-09-27 20:45:06 +02:00
Local Dev
2230d4200c feat(theseus): protections live in Settings › Performance; quiet dock for Shield and Cookie Pop-ups
Shield and Cookie Pop-ups are settings more than tools, so their
switches, the cookie mode, the counters and "Update rules" now sit in
Settings › Performance under a Protections heading, driven through the
add-ons' own message handlers (Settings-only IPC). Each card opens the
add-on's panel for the per-site details, and each panel links back to
Settings. The two add-ons start hidden from the toolbar's extension
row (manifest dock:"hidden", honoured once so a user who shows them
keeps them); "Show hidden" on the row brings them back.

theseus://settings and theseus://settings/<section> are now addresses,
so any page or note can link to a Settings page.

Also: a Settings or add-on tab that the user navigates elsewhere stops
counting as that tab, otherwise "open Settings" kept focusing a tab
that no longer showed Settings.
2026-09-27 20:37:30 +02:00
Local Dev
70934a7553 feat(theseus): Theseus's own prompts use the dialog sheet too
Install-this-app, remove-app, the extension install flow (install,
already installed, installed, not found, failed) and the add-on restart
question were still bare OS message boxes titled "theseus-navigator"
after page dialogs moved to the sheet. askSheet() is a drop-in for
dialog.showMessageBox with the same options and result: title as the
headline, detail under it, the caller's buttons with the default first,
an optional checkbox, and the app's or extension's icon when there is
one. Tone follows the box type (error, warning) or the wording. The
native box stays as the fallback when the browser window is not there,
and for the two synchronous cases (beforeunload, app windows).
2026-09-27 20:13:06 +02:00
Local Dev
053d7bc127 feat(theseus): Shield — tracker and ad blocking as a bundled add-on
Theseus had no content blocking at all. Shield blocks requests to known
tracking and advertising hosts on every site, using EasyList and
EasyPrivacy through Ghostery's adblocker engine (the matcher those lists
are written for). The lists ship inside the add-on so blocking works
from the first launch, offline; the compiled engine is cached under the
add-on's data dir (a 22 ms load instead of a 500 ms parse), and the
lists refresh from their publishers about once a day.

The panel shows what was stopped on the current page, a one-click
allow for the site, the global switch, the running total and the rule
versions with an "Update now". Network filters only for now: a blocked
request never leaves the browser, but leftover empty ad boxes are not
hidden yet.

Host side: a "request-filter" capability. Chromium allows one
onBeforeRequest listener per session, so main owns it and consults the
add-ons' filters; a top-level navigation is never blocked, only http(s)
subresources are offered. api.tabs (active tab and a change event) lets
the panel show per-site numbers without seeing page content.
2026-09-27 19:43:14 +02:00
Local Dev
3c516b20ce style(theseus): page dialogs as notification cards, toned by what they say
The sheet now follows the notification-card pattern: a tone icon in a
tinted circle, the message in bold under a "who says" caption, a
tinted primary action and a quiet Cancel, close in the corner. The tone
is read from the message — delete/remove/error reads as destructive
(red, and the confirm button says Yes), unsaved/required/leave as a
warning, saved/completed as success, anything else as plain info —
since a page hands over only a sentence.

Also decide "who says" from the sender's current URL rather than the
tab's prov, which lags a navigation: a tab that had just left the home
page for a site was still labelled Theseus.
2026-09-27 18:02:09 +02:00
Local Dev
a75707d0ed feat(theseus): page dialogs drawn by Theseus instead of Chromium's stock boxes
alert / confirm / prompt from a page came up as bare OS message boxes
titled "theseus-navigator" (the package name), with no hint of who was
asking and nothing of the browser's styling — the PDF Editor's "Delete
signature?" was the reported case.

The session preload replaces the page's three functions with wrappers
that hand the call to the isolated world through a DOM event, which
asks main synchronously and writes the answer back; pages see Chrome's
return values (confirm → boolean, prompt → string or null) and no new
global. Main answers from a sheet hanging under the toolbar, in the
same surface as add-on approvals, that names who is asking: the site's
host, the add-on's name for an add-on page or panel (identified by its
path under the profile's extensions directory), or Theseus for its own
pages. The sheet belongs to the tab that asked — hidden while another
tab is in front, back when its tab returns — and a closing tab or
window answers "cancel" so no renderer stays blocked. Windows without
the chrome (installed apps, plain windows) get a native box with a
proper title, and app.name now reads "Theseus Navigator" for whatever
else still shows one.
2026-09-27 17:53:21 +02:00
Local Dev
9730b246a1 Merge release/0.3.56 into release/0.3.57
0.3.56 was cut from the Aegis line (WizardConnect auto-detection, Aegis
0.8.x, PDF Editor and VPN updates) on top of 0.3.55; 0.3.57 carries that
plus the install-as-app feature and the two main-process crash fixes.
2026-09-27 11:25:19 +02:00
Local Dev
0774235486 fix(theseus): closing the browser window must not leave tabs talking to a destroyed window
Tabs keep emitting events while the window is torn down: a hovered
link fires update-target-url, which positioned the link-status pill
against win.getContentBounds() on a destroyed window ("Object has been
destroyed", 2026-09-27). With installed web apps the browser window can
now close while their windows keep the process alive, so this stops
being a quit-time blip and becomes a normal state.

The overlay helpers and layout() now check the window is alive, the
session is captured on close (the quit-time save no longer overwrites it
with an empty list once the tabs are gone), and "closed" drops every
reference to the window's views. A later createWindow() starts from a
clean tab list, so a page opened from an app window after the browser
window was closed brings the window back with the restored session.
2026-09-27 11:22:07 +02:00
Local Dev
729930f0b5 fix(theseus): closing a still-connecting relay socket must not crash the main process
Node's ws aborts the handshake when close() is called on a CONNECTING
socket and emits an error on the next tick; with no listener that is an
uncaught exception, and Electron answers with the modal "A JavaScript
error occurred in the main process". nostr-tools drops its onerror
handler right before closing, which is what the WizardConnect relay
teardown in Aegis runs on every wallet disconnect while a relay is
still connecting. Browser WebSockets ignore the same sequence, which is
why the library gets away with it elsewhere.

Every consumer in the main process shares the one ws module, so
close() now adds a no-op error listener to a connecting socket before
aborting it. Anything else that still escapes to the top of the process
is logged to <userData>/main-errors.log instead of raising the modal;
Electron continued after that dialog anyway, so only the interruption
goes.
2026-09-27 10:15:22 +02:00
Local Dev
d3787f8a29 feat(theseus): install a site as an app, the way Chrome and Edge offer it
Electron ships Chromium's renderer without the browser-side web-app
install machinery, so beforeinstallprompt never fires and every site's
own "Install our app" chip (coin-spectrum.com's, for one) stays hidden
in Theseus. The browser side now exists:

- webapps.js reads a page's <link rel="manifest">, accepts it when it
  names an app with a standalone-style display mode and a start_url on
  the page's origin, and records the descriptor on the tab.
- The address bar shows an install chip for such pages (filled once the
  app is installed: click then opens or removes it); the page context
  menu carries the same entry.
- Pages get a synthetic beforeinstallprompt whose prompt() routes to the
  Theseus install dialog and resolves userChoice like Chrome, and an
  appinstalled event afterwards, so sites' own chips appear and work.
- Installing stores the app under <userData>/webapps/, wraps the
  manifest icon into an .ico, writes a Start Menu (optionally desktop)
  shortcut that launches Theseus with --app=<start_url>, and opens the
  app in a chromeless window with its own taskbar identity. The window
  shares the session, BCNR resolution, fingerprint and add-on bridges
  with tabs; popups and "open in Theseus" go to the browser window,
  Alt+arrows / F5 / Ctrl+R cover navigation without a toolbar.
- Theseus takes the single-instance lock so a shortcut launch lands in
  the running browser (second-instance) instead of a second profile
  owner; launched cold, --app= opens only the app window and a later
  plain launch brings the browser window back.
2026-09-27 01:23:36 +02:00
Local Dev
5b6b693694 fix(theseus): give pages a Chrome-shaped window.chrome
Electron hands every page an empty window.chrome. Real Chrome's carries
app, csi, loadTimes and runtime, and bot checks — Google's sign-in
botguard among them — look for exactly those to tell Chrome from an
embedded Chromium. The per-tab identity script now fills the object
with the same shapes and return types; a site learns nothing it would
not also learn from stock Chrome.
2026-09-24 22:51:49 +02:00