Commit graph

382 commits

Author SHA1 Message Date
Silent Mode
c61fef08dc Theseus: Updates description — honest about the startup retry 2026-10-03 19:09:47 +02:00
Local Dev
c02784a7d0 Theseus 0.3.72: Pithos built in, Language settings page
Ships 70b7325 (Pithos, the s3d control panel, as a bundled extension:
open it from the dock to run your own S3 gateway on Sia), dae6b9a
(Settings gets its own Language page), f3efae3 (translator served from
silentmode.st/libre with libre.x / lingua.x) and 604a990 (BNS names read
from Ariadne's indexer when it is installed, Theseus's own as standby).
2026-10-03 19:09:33 +02:00
Silent Mode
88044952e3 Theseus: Settings grows its own Language page
Website language, offer-to-translate and the translator peers list used
to sit as three sub-sections inside General, and Privacy › Anti-
fingerprinting duplicated the language picker on top of them — three
places to edit the one languageMode/languageValue setting. Settings
grows its own Language entry in the sidebar now; everything about
language — the preferred-language picker, the auto-offer toggle, the
peer list, the API key — lives there, and the Privacy duplicate is
gone. The chip in the URL bar still edits the same setting, so the
toolbar surface is unchanged.

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

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

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

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

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

main.js passes the shared module paths (packaged as .mjs, which is why the
shared modules no longer import each other) and keeps the host's status.
The Ariadne panel takes its state from the indexer task when one exists,
and the sub-page says where Theseus's names come from.
2026-10-03 17:06:24 +02:00
Silent Mode
0ba0e735ed Theseus: translator talks to silentmode.st/libre + libre.x / lingua.x
The translator client now ships with the right defaults for the actual
deployment: silentmode.st/libre (ICANN, via the main cert and no new
subdomain) is the primary peer; libre.x and lingua.x are registered on
BNS with `p` records that reverse-proxy back to the same backend; the
public LibreTranslate.com key-gated tier stays as the last-resort entry.

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

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

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

Verified end-to-end against the live silentmode.st/libre instance from a
fresh Theseus profile with a Spanish test page: both the direct
silentmode.st/libre peer and the libre.x -> bns:// -> serveP -> upstream
path translate the page and the revert path restores the originals.
2026-10-03 16:39:45 +02:00
Local Dev
c74d2183e3 Merge branch 'claude/agitated-bell-bd4ac2' 2026-10-03 16:37:03 +02:00
Local Dev
84d5411a53 BNS indexer design: what steps 1, 2 and 4 built, and the step-3 contract
Records where the Ariadne 0.2.0 implementation differs from the design
and why: the protected index\ subfolder, the helper-relayed pipe check,
ariadne-run.exe as the restarter because Task Scheduler does not restart
a program that exits with an error, the resolver exiting in Theseus-only
mode, no owners on the HTTP API, and setup's own scope page. It also
records what the Theseus client needs from the pipe. Economy produces no
"fresh" pushes, so a missing heartbeat while paused must not trigger a
takeover. Last, the plan for bundling Ariadne's setup into Theseus's
installer, not yet wired into the build.
2026-10-03 16:14:52 +02:00
Local Dev
4ab61b83db Theseus 0.3.71: no more launch freeze from large add-on stores
Ships 057629d — add-on stores kept in memory instead of re-read and re-parsed
on every get (a 7.5 MB Aegis store held the window in Not Responding for
16 s) — plus the in-page translator (4fbfc9e, f54ebd6, b43b180).
2026-10-03 16:12:33 +02:00
Local Dev
65ef59f5c8 Theseus: add-on stores live in memory — no more 16 s "Not Responding" at launch
An add-on's storage.get read and parsed its whole store file on every call,
and storage.set read, parsed and rewrote it — synchronously, on the main
thread. Traced on a real profile (installed 0.3.70): with a 7.5 MB Aegis
store, 30 of the first 35 s of main-thread time went to storage.get, the
window sat in "Not Responding" from 3 s to 19 s, and the first page showed at
19 s. One get cost ~73 ms; Aegis does dozens per state update.

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

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

The Aegis side (capping its unbounded txCache) ships separately through
Aegis's own update channel. The boot tracer gains total/longest block columns.
2026-10-03 16:08:22 +02:00
Silent Mode
e8317fef16 Theseus: translator defaults — libre.silentmode.st and libre.x
translate.silentmode.st had "translate" in the subdomain and in the
LibreTranslate path, which read awkwardly on both the chip tooltip and
the Settings list. The shipped defaults rename the primary peers to
libre.silentmode.st and libre.x (plus lingua.x registered server-side
as an alias — same ip record, so it's a URL users can also remember
without being another independent peer in the client's fallback list).
2026-10-03 15:30:54 +02:00
Silent Mode
0b36532922 Theseus: translator peers list — fall back when a mirror is down
The chip ran against a single endpoint, which is the fastest way to go
dark: libretranslate.com's public tier moved behind an API key in late
2026, and most of the historical public mirrors (libretranslate.de,
argosopentech, lt.vern.cc, translate.terraprint.co) either 502 at any
given time, serve a parked page, or started requiring a key of their
own. One URL in settings meant one of those going down meant the chip
stopped working.

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

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

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

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

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

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

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

Re-measured (warm runs, fresh profile): the per-overlay lazy loading in
e524c12 saves 5 processes and ~100 MB private memory at 15 s — not ~30 MB
with an unchanged process count as its message says; that figure came from
the broken runs. The BNS indexer utilityProcess costs ~60-75 MB.
2026-10-03 14:14:46 +02:00
Local Dev
a57d20751b Theseus: boot tracer under scripts/boot-trace
Measures dev-tree launches against a throwaway profile, without changing
main.js: time to app ready, toolbar and first page; main-thread blocks; add-on
activation; overlay pages loaded; main-process fetches; process count and
private memory at 15 s. One row per run, so a startup change can be compared
with the numbers before it.

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

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

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

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

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

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

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

- the startup check runs 8 s after the toolbar is ready and retries with
  backoff (30 s, 2 min, 10 min, 30 min) when it fails
- 20 s timeout via AbortSignal.timeout
- a failed installer download is retried on the next check instead of
  staying failed until the next release
- failures are logged
2026-10-03 10:57:30 +02:00
Local Dev
9b2d6322be Theseus 0.3.69: security fixes from the 2026-10-03 review
Ships fc25e1c and 380ac57: a website loaded into the Settings tab could read
the password vault, add-on updates accepted any publisher's signature, and
the review's follow-ups (stale BNS records, POST replay, background dialogs
stealing focus, update helper on non-ASCII profiles, ...).
2026-10-03 10:18:47 +02:00
Local Dev
2496e54385 Theseus: fix the review follow-ups (stale BNS records, POST replay, dialog focus theft, ...)
- Resolved names are re-resolved when a newer index lands and evicted when
  they drop out of it; an edited ip/s3/tls record, a transfer or an expiry
  used to keep serving the old target until restart. The signed-DNS A
  fallback follows its 30 s TTL instead of the first answer it ever saw.
- A cross-host navigation to a host the warm index knows is unregistered is
  left to Chromium: replaying it via loadURL turned form POSTs (OAuth
  form_post, SAML, 3-D Secure) into bodyless GETs. The site badge follows
  navigations Chromium makes on its own.
- A background tab's alert/confirm no longer pulls its tab to the front; it
  waits, marked in the tab strip, until the user switches to it. Dialogs in
  other windows use the async box, so they no longer freeze every tab.
- Messages resolves sender keys from the browser's own index (one map per
  index generation) instead of a full chain walk per unknown sender; the
  dedupe set is bounded.
- Ariadne uninstall reads HKLM only and runs nothing but unins###.exe from
  Program Files, elevated directly rather than via cmd /c.
- Tor and an add-on proxy no longer wipe each other's settings: Tor wins
  while on, the add-on's rules come back when it goes off.
- Profile migration copies beside the target and renames it into place;
  a failed copy keeps the old, complete profile instead of a partial one.
- Reload/DevTools/zoom shortcuts in app and link windows act on that window;
  Ctrl+B stays with web pages (bold) and toggles the sidebar elsewhere.
- quickPanel comment corrected: it shares the default session on purpose.
2026-10-03 09:59:53 +02:00
Local Dev
08beadcf8f Theseus: close the Settings-tab vault leak and the add-on update signer bypass
A preload belongs to the WebContents, not the page: a website loaded into
the Settings tab kept window.cfg and could read every vault password, flip
settings and install extensions without consent. Settings and add-on tabs
now never load web content, and the channels behind settings-preload check
their sender. `navigate` no longer accepts calls from web pages.

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

Also:
- autofill matches and fills against the live URL, not a stale prov.host
- bns:// forwards the raw request path (..%2F escaped the name's bucket)
- clipboard-read denied, openExternal asks; forged collision choices ignored
- web pages can't window.open file:/chrome:/theseus:; data:/blob: no longer
  go to the search engine; the quick-links panel loses home-preload
- clear-history-on-quit is awaited and removes history.json too
- update helper takes its paths from the environment (non-ASCII profiles)
- electrum poll has a deadline; misses wait at most 2.5 s
- p records go through Tor; add-on proxy credentials are actually used
- whole-folder require-cache bust on add-on version change; failed
  activate() no longer leaks its request filter
- approvals released when the window closes; web-app ids stay on-origin
2026-10-03 09:50:10 +02:00
Silent Mode
166e220343 Theseus 0.3.68: quick-links strip opens services in a dedicated side panel
Clicking an icon on the left strip now opens the service inside a dedicated
380-px mini-view (quickPanel) next to the strip, Opera-style, instead of a
new full-sized tab. Click the active icon again to close the panel; click a
different one to switch. If the panel is already pointed at the same host,
we skip the loadURL so scroll position, open chat and login state survive
a close+reopen round-trip.

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

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

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

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

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

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

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

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

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

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

Also: the settings-update broadcast now reaches every open settings tab, not
only the chrome — so changing the chip updates both the General Website-
language row and the Privacy Anti-fingerprinting Language row live without a
Settings refresh.
2026-10-02 21:51:28 +02:00
Local Dev
838d686598 whitepaper: corrections from a review against the code; trademark note covers every brand kit 2026-10-01 22:28:11 +02:00
Silent Mode
f4514946bd Theseus 0.3.62: picker says just "English", not "American English"
Intl.DisplayNames.of("en-US") returns "American English", which spells out a
distinction the picker doesn't make — one row per language, with English the
UK original. Pass the base code to Intl so the chip tooltip, the "Automatic
(…)" label and the Settings hint all read as the plain language name
(English, Russian, Portuguese, Chinese) regardless of which regional variant
the OS or the saved setting happens to be.
2026-10-01 22:14:38 +02:00
Local Dev
93828a0172 fix(theseus): Presearch is frozen in the engine catalog
presearch.com has redirected every request, searches included, into a
dead host since 2026-09-28, so a user who picked it gets Cloudflare's
origin error instead of results. Rather than deleting the entry, a catalog
engine can now carry a frozen reason: it stays listed in Settings, greyed,
with the reason as its tooltip and an Unavailable badge where the switch
was; it is never enabled, never in the picker, never accepted as the
default from any path, and a profile that had it as default falls back at
startup. Turning it off still works, and deleting the field brings the
engine back exactly as the user had it.
2026-10-01 01:24:10 +02:00
Silent Mode
deae55647c Theseus 0.3.61: language picker — merge regional variants, sort by speakers
Sits on top of the earlier 0.3.61 commit (bundled into the same shipped build):

Spanish and Portuguese collapse to their originals — es-ES and pt-PT — and the
Mexican / Brazilian variants come off the picker (same 2-letter chip, roughly
the same text). Ordering is now global-speakers ranking with European
languages first, so the languages a European desktop is most likely to want
sit at the top: English, Español, Français, Português, Русский, Deutsch,
Italiano, Türkçe, Polski, Nederlands, Ελληνικά, Čeština, Svenska, Suomi —
then the non-European tier led by 中文, हिन्दी, العربية and so on.
2026-10-01 00:59:23 +02:00
Local Dev
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
13e4c6997c Licenses: MPL-2.0 for Theseus, Ariadne and Hephaestus; Apache-2.0 for Argus; CC BY 4.0 for the documents
The public repos carried no license, so nobody could legally copy or build
on the code, and the whitepaper's "free software" had nothing behind it.
Theseus and its companions take the Mozilla Public License 2.0, the
file-level copyleft Firefox and Brave use, which is compatible with every
component they bundle. The resolver and gateway libraries take Apache-2.0
so that other implementations of the registry can reuse them without
copyleft in the way. The protocol documents and the whitepaper are CC BY 4.0.

A third-party notices file lists what the browser ships and fetches, with
the source offer the GPL sing-box binary the VPN add-on downloads requires;
the matching source archive is now published beside the binaries. The
names and marks are reserved in TRADEMARKS.md, separate from the code
license, so a fork must ship under its own name. Settings › General says
the license and links the three files; the whitepaper says the same.
2026-09-29 00:18:00 +02:00
Local Dev
c9c5d18ca2 translate 0.1.5 → 0.1.6: name the detected language in the source select
Auto-detect already worked — the backend returns
detectedLanguage:{language,confidence} and the panel put it in the
status line. But that only appeared after a translation had already
run, in small text, away from the control that raised the question. You
could not tell what "Auto-detect" had decided before committing to it.

The first row of the source select now says "Auto-detect · German", and
it says so while you are still typing. Detection runs on its own via
LibreTranslate's /detect, debounced 700 ms and gated at 12 characters,
because a detector given two words is guessing and firing per keystroke
would pound a public mirror for nothing. It chains the same mirror
fallback as translation, so a dead primary does not make detection look
broken while translating still works.

Confidence below 60 renders as "German?" rather than silently asserting
a coin-flip. The Google backend has no detect-only route, so there
doDetect returns null instead of burning a request, and the label is
filled from the translation response — which every backend returns
anyway, so a skipped or failed detect is never worse than before.

Detection retires when a source is named explicitly, comes back on
returning to Auto-detect, and is wiped by clear. A right-click
selection schedules one too, since that text arrives with no keystroke.

Verified against the real mirror (de/fr/ja at 100/100/90%) and in the
harness: short text fires nothing, long text fires once, four rapid
edits debounce to one call, and every transition above lands.

Also adds the xray removal script used to take the old engine off all
three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
Local Dev
18fce62a11 theseus 0.3.59 2026-09-28 20:49:31 +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
5946923547 translate 0.1.4 → 0.1.5: most-used languages pinned above the list
Both selects grow a "Frequently used" optgroup above "All languages",
holding up to ten entries ranked by how often each language has actually
been translated to or from.

Counted on a successful translation, not on a dropdown change: picking
your way down the list looking for something would otherwise rank every
language you skimmed past as highly as the ones you work in. A
detected source counts too — with Auto-detect on you never pick that
language explicitly, but it is one you read.

Ties break on the language's display name so the order is stable rather
than dependent on object key order. Counts live in the existing uiState,
so they persist through the saveUi/loadState path already there, and the
group only appears once there is something to put in it.

Verified in the harness: one translation records exactly one use for the
target and one for the detected source; the cap holds at ten with
fifteen tracked; and a pre-seeded profile comes back with its group,
target and zoom restored, Auto-detect still first in the source select.
2026-09-27 21:02:22 +02:00
Local Dev
3cb5081bdb vpn: sing-box on both ends, modern config schema, rebuild URL from catalogue
Three bugs, all found by driving the add-on in a real Theseus and
watching the egress IP rather than reasoning about it.

1. The generated config used pre-1.11 schema. `sniff` on an inbound and
   the `block` outbound type were deprecated in sing-box 1.11 and
   REMOVED in 1.13, so 1.14.1 refused the whole file and exited 1.
   Routing is now a bare `final`; rule `action` semantics changed in
   1.12 and the explicit inbound→outbound rule was never needed.

2. xray-core 26.3.27's REALITY would not complete a handshake with a
   sing-box client — and, after ruling out keys (three derivations, a
   fresh pair used verbatim), shortIds (explicit and empty), clock skew,
   dest reachability, TLS 1.3/X25519 on the dest, and xtls-rprx-vision,
   not with a correctly configured xray client either. sing-box against
   sing-box works first try. The exits now run sing-box, which is what
   the add-on already ships to every client, so there is no longer a
   cross-implementation surface at all. Migration script included; it
   keeps the port, the SNI and the existing uuid pool and only changes
   the Reality keypair.

   Worth recording separately: xray's REALITY inbound field is `dest`,
   not sing-box's `target`. That was wrong too, independently.

3. leaseEndpoint cached the full vless URL. The Reality key and short id
   live inside that URL, so re-keying an exit left every client failing
   against a stale copy for the whole 24h lease. It now caches only the
   uuid and rebuilds the URL from the current catalogue entry, so a
   re-key takes effect as soon as the catalogue refreshes.

Verified in Theseus over CDP: baseline 80.187.100.105, tunnel up
81.31.210.65 (the sm-1 exit), off restores the baseline, and sm-3 is
correctly refused to a free-tier caller.
2026-09-27 20:58:24 +02:00
Local Dev
5250672d64 Merge branch 'release/0.3.58' 2026-09-27 20:50:38 +02:00