New record kind alongside h / s3 / ip / u: `p` reverse-proxies the
request to a full upstream URL while keeping the BNS host in the
address bar. Unlike `ip` (which pins the upstream to a raw IP + on-chain
TLS fingerprint), `p` uses the upstream's own DNS + public CA cert and
sends `Host:` of the upstream so vhost-based origins answer correctly.
Argus (record-picker.js):
- Placed AFTER `ip` in the apex chain: a name carrying both keeps its
existing pinned-IP behavior; names with only `p` get honored instead
of falling through to `u` and 302-ing away.
- Not applied to subdomain inheritance — `p` is single-URL by intent,
and inheriting through a subdomain would silently mangle the target.
Theseus (main.js serveBns):
- Fetches the upstream (path prefix from the `p` URL is preserved) and
returns the response body/status/content-type verbatim. Provenance
label "mirror" appears in the source badge.
Tests: record-picker.test.mjs covers `p`-only, `p`+ip precedence, and
the subdomain-suppression rule.
Two new settings under Settings > Appearance > Toolbar:
urlBarSize: wide (default) | medium | compact
searchBoxSize: hidden | compact | normal (default) | wide
Applied as data-attrs on the .bar element in chrome.html; CSS switches
the flex-basis of .urlwrap and the width of .searchbox. min-width on
.urlwrap guards against squeezing the URL invisible.
Settings-set now broadcasts settings-update to the chrome renderer, so
resizing takes effect live without a relaunch. Same channel is exposed
for future chrome-side settings.
Motivation: the extension dock grew a per-addon button per install, and
the URL bar (flex:1) had been eating all the remaining space. Users who
want more room for extensions can now shrink or hide the search box and
cap the URL bar width.
The packaged build stripped the native app menu, which took Chromium's
default DevTools accelerators with it. Wire the two everyone expects —
F12 and Ctrl+Shift+I — in the same before-input-event handler that
already owns reload / sidebar shortcuts. Always target the active tab
regardless of which view received the keystroke (chrome, overlay, tab)
so debugging is consistent with every other browser. Detach mode keeps
the tools out of the tab strip.
The dapp send approval gains an "Afterwards" dropdown: ask every time, or
allow up to 0.001 / 0.01 / 0.1 BCH more without asking. The allowance is
stored as permissions[origin].sendTx {capSats, usedSats}; sends within the
remainder go through silently and draw it down, a larger request re-prompts
(showing what is left) and the choice made there replaces the allowance.
No unlimited option. Settings > Connected sites shows the remaining budget
and Revoke clears it. Message signing still asks every time.
Host: approvalModal accepts `select` {id, label, options}; a chosen value
comes back as "+<id>=<value>" and is validated against the offered options.
Adds a "Search for '<selection>'" item to the page context menu when
the user right-clicks with text selected. Uses SEARCH() so it honours
whichever engine the user has picked (Startpage default, or their own
via the picker), opens in a new foreground tab so the current page
isn't lost, and truncates the label at 40 chars so a long selection
doesn't stretch the menu. Ampersands in the label are doubled so
Chromium doesn't eat them as accelerator markers.
wallet-inject.js runs in the isolated world of https://*.x pages and exposes
window.bitcoincash { isTheseus, version, network, getAddress, signAndSend,
signMessage }. Every call is routed page -> addon-page-msg -> activate()
handler -> approval overlay showing the requesting origin:
- getAddress: approval with an "always allow" checkbox; grants persist in
api.storage.permissions and are listed/revocable under Settings.
- signAndSend / signMessage: approval on every call, never remembered.
signMessage returns a BIP-137 recoverable signature (verified offline).
- one pending approval per origin; page-facing errors never echo balance.
Host fix: the inject IPC assigned event.returnValue twice, so pages always
got an empty script list.
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
Two bugs the user hit:
1) Edit title silently did nothing. window.prompt is disabled in
Electron BrowserView contexts, so prompt() returned null and the
round-trip remove-then-re-add never fired. Built an in-chrome
modal (.promptmask/.promptbox) that resolves to the trimmed
string on OK/Enter or null on Cancel/Escape/mask-click, and
swapped the ctx-menu Edit action to use it. Added a real
bookmark-update IPC (title + optional favicon merge) so the edit
no longer round-trips through remove/add — same URL, just the
title changes.
2) Bookmarks had no favicon. The bookmark data model was {title,url}
only, and the row template had no icon slot. Extended:
- Renderer tracks current.favicon from onTabs's active tab.
- Star click and ctx "Add current page" pass favicon along.
- main persists a favicon field (capped 2KB) and emits it.
- Row template shows the favicon (14×14) with an onerror
fallback so a broken data:/URL doesn't leave a broken glyph.
- Backfill: when a currently-open URL matches a stale bookmark
without favicon, the render pass patches it once via
updateBookmark. Stops itself the next render.
Older bookmarks.json files stay valid — no schema migration needed;
missing favicon just renders the empty placeholder slot.
Two problems bundled into the branded error page:
1) "fetch failed" on no network — the tab kept falling through to
name-not-registered (and other "we asked and got nothing" verdicts)
because a totally offline browser can't reach the beacon to know.
Add a first-class `offline` kind that runs before every other
verdict: navigator.onLine === false wins; the page says "You appear
to be offline" and keeps the address so the user can Retry after
reconnecting.
2) Typos on BCNR names (games.x when they meant game.x) — the page
only offered Search / Register. Now the page asks main for the top
near-matches from the warm sharedIndex (Levenshtein ≤ 2, same TLD)
over an origin-gated `error-bns-similar` IPC. Up to three matches
render as chip-links; clicking one retries at that host, preserving
the original path. Works offline too — the index is local.
Origin-gate on the IPC uses the existing isErrorPageSender check, and
error-preload.js exposes only the invoke — no arbitrary index access.
Setup 3d6d3d6aeacf482284707f50d129315d07a593e09b5d68af852abe5aa0ed4c92
Portable 6c8c5ef212bfccb377e17404e83a18cf2f09771d0eb3e0ec86a85587efdcfc9e
Three address-bar suggestion fixes.
Per-row ✕ delete on hover. Clicks on the X call address-forget instead
of address-pick; the row disappears optimistically in the picker and
main drops the entry from history + persists. Sender-URL gated to the
picker's own file:// origin.
Enter with a highlighted suggestion now navigates to THAT url. The
URL input handler tracks a pickerHasCursor flag that flips true on
ArrowDown/ArrowUp and false on any input; Enter with cursor forwards
to the picker's own submit path via addressCursor("enter") — before
this fix, Enter always ran goURL against the typed letters, which
submitted them as a web search instead of opening the selected url.
Address bar reliably shows the picked URL. onAddressPicked now arms
an overrideUrlBarUntil = now+1500ms flag; the onTabs handler treats
that window as "force write the url", bypassing the focus-guard that
was leaving the bar blank when blur() hadn't landed yet.
Deployed: scp + sia-upload of both trees, verified LIVE 0.3.4.
Setup e016d66543976b7a4df5ac16033319d59cb0f418e54d7e838ef16de274525a83
Portable b9881a62c39ecfe22644611f5aa16a48f32f97f9ec5751b5888ccbd870338c3e
Three 0.3.2 misses fixed:
Tab context menu was clipped by the chrome view's fixed height — the
menu rendered with position:fixed inside a WebContentsView whose
bottom edge stops at CHROME_H, so items below the tab strip weren't
visible. New growChromeForMenu() measures every open menu's bounding
box and calls T.setChromeHeight(needed) so main.js grows the chrome
view to fit; closeAllMenus() calls syncHeight() to restore.
Bookmarks bar was hidden on every tab except Home, so Add-to-
Bookmarks had no on-screen feedback. Now always visible.
Tab groups now actually cluster. Assigning a color to a tab moves it
adjacent to its group-mates in the tabs array (main-side splice, no
render-only trick). The chrome renderer inserts a colored group chip
before the first tab of each group; clicking the chip toggles a
per-group collapsed state (in-memory, resets on relaunch). Collapsed
groups hide their member tabs and the chip shows the member count.
Expanded groups keep the per-tab top color stripe from 0.3.2 so the
cluster reads as one visual block.
Deployed: scp + sia-upload, verified LIVE 0.3.3 on dl.silentmode.st.
Setup 09331b2fd9ccf136e2183b7cd85354cfd56e2ed50260b7aadeed63c7ea450251
Portable 21752d0fc85fb39ec1e65192920461e9ae395a22d9a68abd27f12e638d0fdd07
Right-click a tab: floating context menu with Reload, Duplicate, Group
(submenu: None / Red / Orange / Yellow / Green / Cyan / Blue / Purple),
Add to Bookmarks, Mute (also Unmute; 🔇 shows next to the title when
muted), Close. Menus close on outside click or Escape.
Group state is per-tab. A grouped tab shows a colored dot before the
title and a matching 2-px accent stripe on the top edge, so a cluster
of same-group tabs reads visually. Palette is drawn from existing
provenance colors (err/warn/acid/srv/sia/blue).
Backend IPCs are all tab-scoped (not "active tab"): tab-reload,
tab-duplicate, tab-mute (toggle or explicit boolean), tab-group,
tab-bookmark. emitTabs payload gains muted, group, and url so the
menu can read current state.
Installer wizard branding: 164×314 sidebar BMP with the compass mark
centered + "Theseus / NAVIGATOR" wordmark under it, plus a 150×57
top-strip header with a mini compass on the right. Sharp can't write
BMP directly (only png/webp/etc), so nsis/make-icons.mjs renders raw
RGB via sharp and wraps it in a hand-rolled 24-bit uncompressed BMP
header. Uninstaller reuses the same sidebar.
Silent-install fix: nsis/installer.nsh's AriadnePageCreate now checks
IfSilent BEFORE touching nsDialogs::Create. In /S mode the flag is
zeroed and the function returns cleanly, so the installer no longer
hangs waiting for a page it will never draw. This is why 0.3.2 needed
two builds — the first hung on /S install; the fixed hash is the one
that ships.
Deployed: scp + sia-upload of both trees. Verified VPS hash matches
local 09331b2f. Fresh /S install to D:\Program Files\Theseus Navigator\
placed 0.3.2 with the correct HKCU Uninstall registry entry.
Setup fe59105d2e99a41b7000caeb86601a8e1675846d193e92204034669f5b368d60
Portable 1b6eda55b53894cf9889548116c7b6100888fb160edc84cc1592bb79f9d95b53
The update flow no longer asks the user to click Download. When
checkForUpdate detects a newer version, autoDownloadUpdate() kicks off
session.defaultSession.downloadURL against the setup URL immediately.
will-download recognises the update URL and routes the file to a
fixed %TEMP% path (bypassing the visible downloads panel entirely),
streams updateDownloadReceived/Total into the chip via
emitUpdateAvailable, and flips updateDownloadState to "ready" when
the transfer finishes.
Chip states:
idle first render before the fetch starts — clickable to
trigger the manual download (kept as a fallback).
downloading "↓ 42% — 0.3.2" — no click, just progress.
ready "✓ Install 0.3.2 & restart" — one click.
failed fall back to the pre-0.3.1 explicit-download click.
install-update-now IPC: spawns the cached setup with /S (detached,
stdio ignored), then app.quit() 400ms later so the installer can
overwrite the running exe. Our nsis/installer.nsh detects an existing
Ariadne install via the HKLM registry and skips its Ariadne prompt on
upgrades, so the /S run is fully unattended.
The one-click flow eliminates two long-standing sources of confusion:
- "Download opens a different browser" — Theseus's default session
fetches the installer itself, not a URL handoff to shell.
- "Update requires multiple wizard clicks" — /S skips them.
Extensions aren't touched by this. The framework lives in
addons-host.js + sidebar-preload.js; add-ons themselves live in
%APPDATA%\Theseus Navigator\addons\<id>\ and are a separate layer.
New extensions ship by drop-a-folder, no browser release required.
Deployed: scp + sia-upload, verified 200 + 0.3.1 in the manifest.
Setup bb9d8dec124b8e2a976642ed77051b04edd1d03185581f99db845ef168919836
Portable 1e2d02aea13656574c374440b0c41374290f1bbf85ac6313cec075292cfc34f8
Home cards decoupled from the installer. Theseus now fetches
https://dl.silentmode.st/home-cards.json at boot and every 6h, caches
into <userData>/home-cards-remote.json, and applies it as the default
tier. Priority in loadHomeCards() is:
1. <userData>/home-cards.json (user edits — unchanged, always win)
2. <userData>/home-cards-remote.json (last successful fetch)
3. DEFAULT_HOME_CARDS (code fallback for offline first-boot)
On a successful refresh, main pushes the new list to every home tab
via the home-cards IPC — but only when the user has NO local edits.
The user's edit mode remains authoritative.
Content updates now happen by editing site/home-cards.json and running
scp + sia-upload. No browser build required.
Prometheus.X copy fix: "Decentralized App Marketplace" (was
"marketplace for BCDN-hosted apps" — the marketplace isn't BCDN-
specific). Applied to both DEFAULT_HOME_CARDS (fallback) and
site/home-cards.json (canonical live copy).
Deployed:
- scp installers + manifest + tools/ + releases/ + home-cards.json
to /opt/silent-mode/dl/ and /opt/silent-mode/site/
- sia-upload of both site trees
- verified home-cards.json served 200, manifest 0.3.0 live
Existing installs of 0.2.5 will surface 0.3.0 in the update chip and
also pull the new home-cards.json — so the "Decentralized App
Marketplace" wording lands on ANY current install (0.3.0+) the moment
this commit deploys, without waiting for anyone to install 0.3.0.
Wait — 0.3.0 is what CARRIES the fetch logic. So the auto-pull benefit
only kicks in from 0.3.0 onward. Users on 0.2.5 or earlier need to
install 0.3.0 once; from then on every home-card edit reaches them
without a new browser release.
Setup 50be20d6e3811445817e35c560dce1a4bbcc62004d6b0480848a211a654319ea
Portable 021afe629c61f2f49e95f936261649bdcff58c8b79bd0d903a8d10e6009740b0
Home page card grid gets a brand pass. Each title's TLD suffix (.X,
.bch, ...) is now styled in acid (--acid) with a soft text-shadow
glow. styledTitle() splits the domain on the last dot so capital
labels like "Theseus.X" render as ink + acid.
Card copy tightened per name:
hello.bch A small page on the blockchain itself.
siatest.bch A page with no server, backed by Sia.
SilentMode.X Infrastructure development for a decentralized web.
Theseus.X The Web Navigator - this browser's own address.
Sirius.X Register and manage BCDN names.
Hephaestus.X The forge - Silent Mode's code host.
Prometheus.X An app store - a marketplace for BCDN-hosted apps.
Helios.X Search engine for the decentralized web (in design).
Hermes.X Messaging - end-to-end encrypted over Nostr.
Badges: dropped the generic ".x" tag; each .X card carries its role
label instead. Palette assigns each role a distinct hue drawn from
the existing provenance colors, so the grid reads as families without
adding new brand values:
Infrastructure / Registrar acid
Navigator blue
Code host sia purple
App store srv teal
Search warn amber
Messaging err coral
Descriptions brighten from --mut to --ink on card hover so the ink
color reads as the "acid touch" the user asked for without adding
literal green blocks to every card.
Deployed: scp installers + manifest + tools/ + releases/ pages to VPS,
sia-upload of both trees, verified HEAD 200 + manifest 0.2.5.
Setup 7fd323a87bd32b780e147de18e16ecd82f89960bbd8e9a619c5d25d374597cd2
Portable 3166e64cf56badd7b26c4c793dc79bbed6f9d6c48fbd467d97f845385b91b1dd
Home cards: DEFAULT_HOME_CARDS replaced with the .x sibling grid the
user asked for -- hello.bch, siatest.bch (the "types of BCDN" pair),
then silentmode.x / theseus.x / sirius.x / hephaestus.x /
prometheus.x / helios.x / hermes.x. Existing installs with a saved
home-cards.json keep their edits (defaults only seed fresh profiles).
Search engine picker sync: user reported the toolbar dropdown listed
engines as active that Settings > Search showed differently. Root
cause: settings.searchEngine could be pointing at an id not in the
currently-enabled set (stale settings.json after DEFAULT_ENABLED
changes across versions). loadSettings now normalizes on boot -- if
searchEngine isn't enabled, fall back to enabled[0]; and
installedEngines gets unioned with enabledEngines so the two lists
can't disagree in ways that make toolbar and Settings render
different rows.
Proxy auth support in the framework: setSessionProxy accepts
`{ proxyRules, auth: { username, password } }` or an inline
`socks5://user:pass@host:port` URL. When creds are present, the
handler strips them from the URL, installs a session#login listener
on the default session that answers with them, then calls setProxy.
Chromium's SOCKS5 client doesn't consume proxy auth (known Chromium
limitation), but HTTP proxies work; SOCKS-based extensions need to
gate by IP allowlist at their server. Log line masks the password.
Update chip note: the "download opens in a different browser" was
0.2.0-era behavior. 0.2.1 rewired it to session.downloadURL. Anyone
still seeing it needs to install 0.2.1+ once.
Deployed: scp + sia-upload both trees, verified HEAD 200 + manifest
0.2.4 live.
Setup 3abaeff73afcecc9e4f05f749765e168a97a377890211dbae4345654c84ce2f1
Portable 71017563d6ba107cb25e24be240abcb78e34491d8f7aa42e095421edbc482a85
Brand: the red-N compass from theseus.x is now the taskbar / titlebar /
File Explorer icon everywhere. Source SVG lives at
site-theseus-x/assets/favicon.svg so brand + browser icon stay in sync.
nsis/make-icons.mjs renders it to build/icon.png (512x512) and
build/icon.ico (multi-resolution: 16/24/32/48/64/128/256). Wired into
package.json: build.win.icon, build.nsis.installerIcon + uninstaller
+ header; the ico ships as an extraResource so main.js's BrowserWindow
also uses it at runtime. Icons live in build/ which is gitignored -
run `node nsis/make-icons.mjs` when the SVG changes.
Sidebar: multi-panel picker strip. When 2+ extensions register sidebar
panels, sidebar-preload.js injects a 32-px tab strip at the top of
every panel's document. Click a tab -> ipcRenderer sidebar-open ->
loadFile switch. Preload also injects box-sizing:border-box + a
33-px body padding so height:100% panels don't overflow. Solo-panel
case is unchanged (strip only appears when panels.length >= 2).
Search: Startpage is the new default. Both the default id and the
enabled-list ordering put it first. DDG stays enabled by default too.
Deploy: scp installers + manifest + tools/ + releases/ to VPS,
sia-upload of both trees, verified HEAD 200 and manifest 0.2.3.
Icons regenerated from theseus.x's favicon.svg; the .svg itself
shipped in the 0.1.x window when the theseus.x site went live.
Setup 972f6209639122f32f032d5f2f9fc5a4808e0d4a810f88ae38ec9a1275ac52ac
Portable a9771f054ec36b9aff6ddee958cecf7e84cdacd4d7932aa8385c445aab4d29be
User-visible rename: the Settings tab and its labels say "Extensions"
now instead of "Add-ons". Internal identifiers (disabledAddons, the
addons/ folder, IPC channels, capability strings) stay put — code
churn wasn't worth it, and users only see the user-facing text.
Draggable sidebar. sidebar-preload.js now injects a 5px grip strip
along the LEFT edge of every panel document. mousedown+mousemove
streams delta-x px to main via sidebar-drag IPC; main clamps to
[200, 800] and debounces a save to settings.sidebarWidth. Width is
restored on next launch. The default is still 340. Faint acid-green
highlight on hover so the affordance is discoverable.
New extension capability: session-proxy. An extension whose addon.json
declares "session-proxy" gets api.setSessionProxy(rules) which routes
to session.defaultSession.setProxy — the same primitive Tor already
uses under the hood. Rules can be a string ("socks5://host:port") or
an object matching Electron's setProxy shape; null clears. The
capability is opt-in: an extension without the declaration gets an
error if it tries to call setSessionProxy. This is the framework
surface a private 3-VPS relay extension would build on (extension
folder stays on the operator's disk only; nothing about it appears in
the public build).
Deployed: scp installers + manifest + tools/ + releases/ pages to
VPS, sia-upload of both trees, verified HEAD 200 and manifest 0.2.2.
Setup 67e3cda1b76a6dba966d91a43e78fdb52378f860c0f75a7775d11b96e1b4f5e3
Portable 415c804f75df6fab5072650fbdc3cffa4124ed02f8ff53a91281ebd2dc4775c3
Update chip: the Download button used to hand the URL to the system
browser (shell.openExternal) - user opens Theseus, gets prompted to
update, clicks Download, watches ANOTHER browser wake up and start
the transfer. That's the "why did a different browser open?" report.
Fix: session.defaultSession.downloadURL(url) - triggers the same
will-download handler our own downloads panel already listens on, so
the file lands in the user's Downloads folder AND appears in
Theseus's downloads chip with progress + Show-in-folder. Rejected
protocols unchanged; only dl.silentmode.st and silentmode.st URLs
survive the allowlist.
Ctrl+B toggles the add-on sidebar (matches VS Code). Verified end-
to-end against a fresh <userData>: bundled Notepad was seeded,
Ctrl+B opened its panel, typed text autosaved to
<userData>/addons-data/notepad.json, restart re-mounted and the
text loaded back before the append could clobber it.
Deployed: scp installers + manifest + tools/ + releases/ to VPS,
sia-upload of both trees, verified HEAD 200 and manifest 0.2.1.
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
Setup f2afc14efc63008cbb9dad44176e94146386db4c0afda4459f1d4eb929172b6d
Portable 5d08b1415526934db8de780949a610896064fe9567aa0e5e1702ebabd7eb7df2
Chromium's default 'This site can't be reached' replaced with a Theseus-
themed error page. did-fail-load on every tab's webContents (main frame
only, non-ignorable code) routes the tab to error.html with the
attempt URL, host, error code, and description as query params. The
page keeps t.url pointing at the failed URL so the address bar shows
what the user typed and they can edit + retry - refreshTabUrl's
existing file:// skip means the error page's own path never leaks
back into the bar.
Five kinds, chosen by pickErrorKind(code, host):
name-not-registered BCNR-eligible host + ERR_NAME_NOT_RESOLVED.
Says "no BCDN record on chain, no clearnet host
either." Offers Register on Sirius + Search +
Retry + Home.
name-unreachable ERR_NAME_NOT_RESOLVED on a non-BCNR host. DNS
failed - offers Retry + Search + Register +
Home.
unreachable CONN_REFUSED/RESET/TIMED_OUT/CLOSED/NETWORK_CHANGED.
Offers Retry + Tor guide + Home.
tls ERR_CERT_* range (-200..-299). Offers Retry +
Home.
generic Everything else.
home-preload.js gains `window.errorpage` alongside `window.home`. Both
APIs are sender-URL-gated in main - a random page seeing the shape
can't invoke them (isErrorPageSender / isHomePageSender). The external-
open handler additionally allowlists Silent Mode domains only.
package.json build.files gets error.html + error-preload.js so
electron-builder actually bundles them (GOTCHAS rule: an unlisted
runtime-loaded file silently opens blank).
Ship pages (releases-manifest.json, tools/index.html, releases/index.html,
site-theseus-x/index.html) updated to 0.1.3 with the new hashes.
DEPLOY STATUS - blocked on VPS SSH: my IP was hit with a full-port ban
mid-turn (likely fail2ban from the burst of scp during the 0.1.0-0.1.2
iterations). Site pages/manifest/installers are committed locally but
NOT yet on dl.silentmode.st or the Sia mirror. Live still reads 0.1.2.
User needs to unban 195.184.247.106 on their end, or wait for the ban
to expire, before the ship pages match reality.
Startpage's in-page search form POSTs to /do/search. Our will-navigate
rewriter was calling navigateTab(id, url) which loads via loadURL(url)
- always a GET, no body. So the second search from within Startpage
always landed on /do/search with no query and no results.
Google's search form GETs, so 0.0.5's query-preservation fix was
enough for it. DDG uses history.pushState and never fires
will-navigate at all. Startpage POSTs, so it needs a different fix:
just don't intercept.
Only intercept CROSS-ORIGIN navigations for BCNR re-lookup. A form
submit or subpage link on the current site (same hostname) belongs to
Chromium natively - the site is already loaded from clearnet, its
subsequent navigation stays on clearnet. Preserves POST bodies for
every site, not just Startpage: any login form, comment submit, or
checkout flow that stayed on-origin was silently broken by the same
bug.
Address-picker fix:
Picking a suggestion from the address dropdown loaded the URL but
left the address bar showing the 3-4 letters the user had typed.
Root cause: onTabs's focus guard
if (document.activeElement !== $("url")) $("url").value = d.url
skipped its write while the URL input still held DOM focus, and
clicking a sibling WebContentsView doesn't always deliver the blur
to the chrome renderer in time. Fix: address-pick sends an explicit
address-picked IPC to the chrome, which force-blurs and writes the
full picked URL before the tabs event arrives.
Reload keys:
None of the standard reload accelerators worked (Menu.setApplicationMenu(null)
drops Chromium's default menu accelerators, and hard-reload was never
wired at any layer). Now:
F5 / Ctrl+R -> soft reload
Ctrl+F5 / Ctrl+Shift+R -> hard reload (reloadIgnoringCache)
Shift-click on the toolbar reload button -> hard reload
Same before-input-event hook that carries Ctrl+Shift+M; always targets
the active tab regardless of which view got the key, and skips the
Settings tab. Toolbar tooltip updated to advertise Shift-click.
Merges a parallel session's work with the multi-source BNS story from 0.0.7.
The dApp side (parallel session)
--------------------------------
* bcnr-preload.js — installs `window.bcnr` on every page via contextBridge.
Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(),
getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All
Promises; a missing name returns null (not throw). No signing, no wallet
unlock — that surface is designed but deliberately out of scope for 0.0.8
(see TheseusNavigator/DESIGN-integrated-wallet.md).
* bcnr-origin.js — pure function that computes the eTLD+1 permission origin
for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles
.co.uk / .github.io / etc); BNS names key off the on-chain TLD list so
foo.wallet becomes a public suffix as soon as `wallet` appears there.
Match browser cookie / MetaMask semantics: a grant on pay.merchant.com
covers account.merchant.com but not evil.com.
* dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O.
* main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so
it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers.
* preload.js + chrome.html — small hooks so the shell picks up window.bcnr
the same way regular content does.
* package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`.
Also included
-------------
* AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the
Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse
picked it up), formalising the source now.
* TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled
starter (73 beacon txs, root c37b8596…c54e414ba).
* Site pages + manifest updated to point at 0.0.8.
TheseusNavigator-Setup-0.0.8.exe 95.4 MB
21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02
TheseusNavigator-0.0.8-portable.exe 92.7 MB
2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
Four independent pieces of one story — make cold-start .bch resolution fast,
survive individual electrum outages, and turn adding a Silent Mode BNS server
into a checklist run.
* Argus/src/lib/snapshot-name.js — pure, interpretation-free snapshot format
for the name beacon (raw history + verbose tx cache + deterministic root).
* Argus/src/publish-name-mirror.mjs — mirror the snapshot to Sia S3
(floating + content-addressed) and Nostr NIP-33 (kind 30078,
d-tag bns-name-list). Same pattern as publish-tld-mirror.mjs.
* Argus/src/snapshot-name-to-file.mjs — dump the snapshot to a file for
bundling. Writes to TheseusNavigator/snapshots/bns-name-snapshot.json.
* Argus/src/lib/resolver-web.js — new export buildIndexFromSnapshot() runs
the same REG/UPD reduction as buildIndex() but from a pre-fetched snapshot,
no electrum required. Warm-start path in Theseus depends on this.
* TheseusNavigator/main.js — warmFromSnapshot() and refreshSnapshotFromSia()
wired into app.whenReady(). First .bch navigation returns from a warm index
instead of waiting on a full electrum walk.
* TheseusNavigator/package.json — bundles the starter snapshot as an
extraResource so packaged builds ship with a floor.
* TheseusNavigator/snapshots/bns-name-snapshot.json — first starter snapshot
(70 beacon txs, root 8cc859aa…).
* VPS/BNS-SERVER-PLAYBOOK.md — turn-key checklist for spinning up a second
or third Silent Mode BNS server, distilled from what actually worked on
the silentmode box (BCHN 29 [chip]-not-[chipnet], multi-beacon indexer,
wss:// nginx, on-chain publish via electrum.bch, common gotchas table).
* Argus/src/register-electrum-list.mjs — publish the current chipnet
server pool as `el` on electrum.bch. Fits inside the 200-byte OP_RETURN
cap by sizing the record (drops fallback IP pins in favour of URL-only
tokens until they fit). NAME was chosen deliberately: two-part `electrum.bch`
normalises to the bare distinct name `electrum`, whereas the earlier
candidate `electrum.silentmode.bch` collapsed to silentmode.bch and would
have inherited/overwritten that name's records.
* Argus/src/revert-silentmode-bch.mjs — one-off recovery from the earlier
register-electrum-list revision that hit exactly that subdomain-collapse
bug (broadcast an UPD to silentmode.bch and clobbered its s3 record).
Records recovered from the local snapshot cache; safe to leave in the
tree as documentation of what happened.
Favicons:
- Each tab stores a favicon URL; wired via wc.on('page-favicon-updated')
to Electron's first-emitted icon URL. Included in the emitTabs
payload; the chrome renderer paints an <img class="fav"> in the
same slot the loading spinner uses (spinner while loading, favicon
once page-favicon-updated fires). No proactive clear on navigation
- mainstream browsers keep the old icon until the new one arrives,
which avoids a flash on every subpage click.
Cursor:
- .tab { cursor: default } instead of the previous 'grab'. Grab looked
like the page was always in the middle of a drag. .tab.dragging
keeps 'grabbing', so the grab hand only appears when the user
actually picks a tab up to reorder it.
Right-click on an image now offers:
- Open image in new tab -> createTab(srcURL)
- Save image as... -> wc.downloadURL(srcURL). The tab's
will-download handler leaves savePath
unset, so Electron shows the native
Save As dialog and the download appears
in the tracker.
- Copy image -> wc.copyImageAt(x, y). Puts the decoded
image on the clipboard (paste into any
image-aware target).
- Copy image address -> clipboard.writeText(srcURL).
Only shown when p.mediaType === "image" && p.srcURL, so text /
link right-clicks are unchanged.
Two related navigation bugs, both surfacing when a page inside a tab
tries to submit a search:
1. will-navigate rewriter dropped the query string and fragment.
Every dotted host went through
navigateTab(id, parsed.hostname + parsed.pathname)
which stripped ?q=... The classic-form-GET search engines
(Google, Brave, Bing, Startpage, Yandex, Ecosia, Mojeek, ...) all
silently landed on their /search endpoint with no query, so no
results ever showed. DuckDuckGo only appeared to work because its
in-page search uses history.pushState + XHR and never triggered
will-navigate to begin with.
Fix: pass the whole URL (minus scheme) so query + fragment survive.
2. Address bar showed the previous page's URL until the new page's
did-navigate fired. navigateTab called setLoading() -> emitTabs()
BEFORE assigning t.url, so the chrome renderer received the stale
URL and painted it (goURL() blurs the input on Enter, so the
"don't clobber typed text" guard didn't skip the write).
Fix: assign t.url from host+rest immediately, before setLoading.
Firefox / Chrome-style small pill at the bottom-left of the window
showing the href when the pointer hovers a link, hidden when the
pointer leaves. Fed by webContents.update-target-url on every tab;
auto-sizes to its text via an IPC resize channel.
Cleared on tab switch so a lingering hover pill from tab A doesn't
carry across to tab B. Positioned by layout() so it tracks window
resizes.
New files link-status.html + link-status-preload.js registered in
build.files (GOTCHAS.md rule: missing entries silently omit from
the packaged app).
Not shipped yet — bundles into the next Theseus release.
Bumps version so the chip actually surfaces itself on 0.0.3 installs
(the version-newer check requires a strict semver bump — same-version
rebuilds don't trigger the chip). From this release on, whenever the
manifest names a newer Theseus, users get a one-click download.
Mechanism
- main.js checkForUpdate() fetches https://dl.silentmode.st/releases-
manifest.json on startup (5s timeout, cache: no-store) + every 6h.
Finds the theseus-navigator release, compares version to
app.getVersion() with a numeric a.b.c comparator that handles
"0.10.0 > 0.9.9" correctly.
- On a match → stores { version, setupUrl, portableUrl, setupHash,
portableHash, date } and emits update-available to chrome. Cleared
after the user upgrades + relaunches (same-version → null).
- Re-emits on chrome's did-finish-load in case the fetch beats the
chrome view.
Chip UI (chrome.html)
- Acid-yellow pill between the downloads button and the Tor toggle:
"↓ Update to X.Y.Z" + a ✕. Main body opens setupUrl in the system
browser via shell.openExternal (origin-validated to
https://dl.silentmode.st/ or https://silentmode.st/). ✕ dismisses
for the current session — you'll see it again next launch if still
behind.
Trust anchor
- No signing / no cryptographic verification of the download in this
phase. releases.silentmode.bch publishes the SAME manifest URL, so
users who want to verify can cross-check the manifest hash against
what BCNR returns. The proper auto-updater with signature checks is
the follow-on to this cheap version.
Non-goals in phase 1
- No delta downloads; the user clicks and gets a full installer.
- No auto-install; download → user runs the installer themselves.
- No "check now" button in Settings; the periodic timer suffices.
- No portable-vs-installed detection; the chip prefers setupUrl (the
installer upgrades in place). Right-click for portable is future work.
- password-vault: createVault takes { messengerRootHex } opt; unlockVault
surfaces messengerRoot (null on legacy vaults, so nothing regresses);
saveVault persists it; bindMessengerRoot mutates an unlocked state for a
later attach-mnemonic-to-existing-vault flow.
- main.js password-setup: when the user provides a mnemonic, ALSO compute
seedToPurposeRoot(seed, "messenger/0") and store it. Random-seed vaults
stay as-is (no mnemonic = no messenger root to store).
- main.js hermes-init: two modes now — { mnemonic } (unchanged) or
{ useVault: true } (uses vaultState.messengerRoot directly, no mnemonic).
Response includes source: "vault" | "mnemonic" for the panel to badge.
- main.js hermes-can-use-vault: cheap availability probe used by the panel
to decide whether to show the vault sign-in shortcut.
- hermes.js: nostrKeyFromRoot(root) accepts 32-byte Uint8Array or 64-char
hex; produces the same {sk, pkHex, npub} as nostrKeyFromMnemonic for the
same seed (unit-verified: derivation paths converge on f2c92519...67f4).
- Messages panel: "Sign in with password vault" button appears above the
mnemonic entry iff the vault is unlocked AND was set up from a mnemonic.
The mnemonic path stays as the always-available fallback — bind is
genuinely optional, not required.
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
Uncovered by the 2026-08-13 subdomain-inheritance fix: once
`checkers.game.x` correctly picked the parent's `ip` record instead of
`s3`, the ip branch itself failed. Two reasons:
1. `fetch("http://<ip>/", { headers: { host: name } })` follows the
site's :80→:443 redirect into `https://<name>.<tld>/`, which isn't
in ICANN DNS → "fetch failed".
2. The site's cert is signed by a per-machine BNS root, not a public
CA; standard TLS validation rejects it.
Both are fixed by connecting to the IP with SNI = name, pinning the
presented cert's SHA-256 against the on-chain `tls` record, and only
then issuing the HTTPS request over the same socket. The on-chain
fingerprint is the trust anchor BNS uses everywhere else (see
Argus/src/lib/ca.js).
Gateway (public-gateway.mjs): new pinnedHttpsGet + httpGet + ipRequest
helpers; case "ip" delegates. No silent HTTP fallback on pin failure
(a mismatch means "not the site the chain says it is").
Theseus (main.js): parallel port of the same helpers, Tor-aware
(routes through SocksProxyAgent when Tor is on). serveBns's inner
serveIp() delegates to ipRequest.
Verified live: `curl -sI https://navigate.st/bns/checkers.game.x/`
returns 200 OK with the checkers game (1,179,215 bytes, apex
`game.x` unchanged, served from Sia).
Three follow-up asks from the previous ship:
1. Shield "secure" colour bumped from #4fd1a5 (mint) to #3fb950 — the
GitHub-style saturated green, matches the +N/-N diff colour the user
pointed at as reference.
2. Engine-picker "Search settings…" now opens the Search section
directly instead of General. New IPC channel `focus-section` fires
from main after picker-open-settings, carried through
settings-preload as `onFocusSection`, and the settings.html sidebar
handler exposes showSection(sec) so any section can be focused
programmatically. Works for both a fresh settings tab (fires on
did-finish-load) and an already-open one (fires immediately).
3. Toggle no longer removes an engine from the list. Two-tier state:
INSTALLED (visible in the Settings list) and ENABLED (toggled on in
the toolbar dropdown). Toggling off keeps the row visible with an
.off class (dimmed 55%). Right-click any row → new context menu with
"Remove from list" is what actually removes an engine (built-ins go
back to the catalog, customs are dropped entirely).
Model changes:
- New settings.installedEngines persistent array (defaults to
DEFAULT_ENABLED). enabledEngines becomes a subset of installedEngines.
- isInstalled(id) helper; allEngines() carries `installed: bool` alongside
`enabled`.
- New IPC `remove-from-list` (right-click action); exposed as
removeFromList in settings-preload.
- set-engine-enabled now also INSTALLS when enabling (the catalog "+ Add"
flow), preserves installed state when disabling.
- add-engine (custom URL) auto-adds the new id to enabledEngines too.
- remove-engine (custom delete) prunes from enabledEngines as well.
- Never-empty invariant kept: enabledEngines falls back to ["duckduckgo"]
if everything gets removed.
Settings UI:
- Enabled list shows all INSTALLED engines (was: only enabled), rendered
with toggle reflecting enabled state; rows carry data-builtin so the
context menu picks the right remove IPC.
- Catalog panel and Discover-more pane filter on !installed instead of
!enabled — a toggled-off engine stays in the enabled list, not here.
- Ctxmenu is a floating .ctxmenu div; closes on outside click / Escape.
- .eng.off dims the row and mutes the name colour.
Preview harness stubs updated to include the `installed` field on every
engine + `removeFromList` and `onFocusSection` no-op stubs so
_settings-preview.html renders the new UI accurately.
Captcha-gated testnet faucets in the faucet hub set session cookies with
no SameSite attribute; Chromium defaults those to Lax and withholds them
inside cross-site iframes, so cookie-bound captcha endpoints 500
(tbch.googol.cash /captcha: 500 cookieless, 200 with the session cookie).
applyEmbedCookieShim() rewrites Set-Cookie on an allowlist of embed hosts
to append "; SameSite=None; Secure" so the cookie is frame-eligible.
Allowlist-scoped only — SameSite is CSRF protection, never relaxed globally.
BCDN pages now show https:// in the address bar (was bare 'host/', briefly bns://).
Rationale: BCNR replaces DNS (name resolution), NOT HTTP. Under the hood the
delivery IS HTTPS for s3/ip/p records; the on-chain h record has no transport
at all, but https:// is the least-surprising display. BCDN/ICANN badge is the
sole source-of-truth for which registry served us; scheme stays a convention.
Registry chip is now a proper segmented control:
- Two-chip toggle (collision candidates): single rounded pill, split in half
with a divider — active side filled (acid green for BCDN, blue for ICANN),
inactive side transparent + clickable. Feels like an on/off toggle.
- Single chip (pure ICANN or BCNR-unique TLD): standalone pill in the
registry's colour. No toggle affordance since there's no alternative.
Same acid-glow-green (#d6ff3d) and blue (#4c9eff) as the collision.html prompt
so the whole registry palette is consistent across chrome + interstitial.
Adds a will-prevent-unload handler on each tab. When a page has a beforeunload
handler that returns non-null (typical for forms with unsaved input, in-browser
editors with a dirty document, etc.), Electron would silently cancel any
navigation attempt. Now we show a native two-button dialog — 'Stay on page' /
'Leave anyway' — matching how mainstream browsers behave.
Works for both user-initiated navigation (link clicks) AND our own programmatic
loads (address bar, chip switcher, in-tab collision prompt). Answers the
operator's ask: 'only if there is unsaved work that can be lost, should a
warning appear' (2026-08-02).
Chip switcher now bypasses navigateTab/loadBns entirely and calls loadURL
directly. Rationale: the user explicitly clicked a registry chip; that IS the
choice. Routing through navigateTab -> loadBns was reaching the collision
decision path in some races and could re-show the soft-mode 'Open with...'
prompt as an unwanted extra step. Direct load guarantees the switch is atomic.
Prov is updated inline using entries.get(host) for the BCDN case (source/records/
category), same shape loadBns would have produced. internalNav flag guards the
programmatic load from the will-navigate handler (redirect chains stay clean).
Also: address bar blurs on Enter so the tabs-event handler can update it to
the actual loaded URL (search keyword no longer sticks in the address bar
after search results load).
Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
Two related bugs both caused by the tab's will-navigate handler racing with
programmatic loads:
Bug A (chip switch BCDN -> ICANN shows blank page, BCDN-priority mode):
fallbackToWeb() calls webContents.loadURL('https://<host>/') to serve the
ICANN version. That fired will-navigate, which saw a dotted host, ran
isBnsHost() -> true, and RE-INVOKED navigateTab() recursively — but the
transient='icann' override had already been consumed, so the recursive call
fell back to bcnr-first, canceling the fallback mid-flight. Tab showed blank
because both loads collided.
Fix: mark programmatic loads with t.internalNav so will-navigate skips them.
Bug B ('Ask each time' -> Open BCDN doesn't load):
Was going through a meta-refresh from bns://collision-choose/ to
bns://<host>/?_collision=bcnr. The meta-refresh bypassed navigateTab, so the
chrome/prov state was never updated (address bar, badges stayed stale).
Fix: will-navigate now catches bns://collision-choose/ FIRST, applies the
remember flag, sets t.collisionOverride, and routes via navigateTab so
chrome + prov update correctly.
Removed the serveBns collision-choose handler + the ?_collision URL-param
path in loadBns (both dead now that will-navigate handles it).
Result:
- Soft-mode 'Open with...' -> Open BCDN loads the BCDN site cleanly.
- Chip switcher flips BCDN <-> ICANN with no blank flash, correct chrome.
Three UX fixes based on operator feedback (2026-08-02):
1) 'Open with…' is now a FULL-PAGE in-tab interstitial (was a modal window).
- loadBns loads collision.html via loadFile with query params instead of
opening a BrowserWindow.
- collision.html is navigation-based: buttons redirect to a special
bns://collision-choose/?host=…&choice=…&remember=…&resturl=… URL.
- serveBns handles that URL: persists remember=name/tld choice, then returns
a meta-refresh to the real target. For BCDN it appends ?_collision=bcnr
one-shot marker; for ICANN it redirects to https://<host><path>.
- loadBns strips and honors the ?_collision one-shot marker so BCDN
redirects don't re-trigger the prompt.
- Old modal path (collisionPromptOnce) removed.
2) Auto-switch from the address-bar chip: clicking BCDN/ICANN chip sets a
transient per-tab collisionOverride that loadBns consumes ONCE for this
navigation only, bypassing the soft-mode prompt entirely.
3) Address-bar toggle: chips stay visible during 'resolving' (no more
flash-and-reappear); active chip is colored (acid green for BCDN, blue for
ICANN — matching collision.html's palette); inactive is greyed with border,
clickable to switch. Feels like a proper toggle.
Settings > Registries copy: operator's exact wording adopted (title stays
'Registries', body uses 'BCNR-unique' term for TLDs that only exist on BCNR).
Terminology (user-facing labels now match the product/registry distinction):
- BCDN = Bitcoin Cash Domain Names (the product — what you view)
- BCNR = Bitcoin Cash Name Registry (the on-chain system that backs it)
Chrome badge, popover, Settings section and 'Open with…' prompt all show BCDN
for a resolved name. BCNR stays only where the registry itself is the subject.
'Open with…' prompt (collision.html):
- Larger window (640x520, was 480x340)
- Radio-select pattern with explicit Open / Cancel buttons (was auto-fire on click)
- Enter = Open, Esc = Cancel, keyboard-first
- Body copy per operator spec (no product-name-in-parens; blockchain-generic)
- Card titles kept short: 'BCNR / BCDN' + 'ICANN / IANA'
Address-bar chip:
- Says BCDN (not BCNR) for on-chain names; ICANN for web
- Dropped the '·.tld' suffix — TLD is already in the URL bar
- On collision candidates: shows BOTH chips (BCDN | ICANN) with active
highlighted; clicking the inactive one flips the tab (via collision-switch)
Settings section renamed Naming → Registries. Copy rewritten in the same
BCDN/BCNR frame; policy labels are now 'BCDN first' / 'ICANN first' / 'Ask
each time'.
Passive 'also on BCNR' bar copy updated to BCDN.
Verified: all preload/settings/main/chrome JS + HTML parse; no secrets in
staging.
Theseus soft-mode UX: 'Open with...' modal on collision, per-name/per-TLD
overrides, live per-tab switcher in the site-info popover, and a Naming section
in Settings for policy + reset. Backed by an on-chain root TLD certificate
(tlds.bch) that resolver-web.js discovers via fetchBcnrTlds()/isBcnrNativeTld().
Companion pieces:
- Argus/src/indexer/ELECTRUM-SOURCE-README.md — the featherweight VPS variant
(no BCHN node, no Fulcrum) now live as bns-indexer.service.
- Argus/DESIGN-root-tld-cert.md — clarified: NOT a governance workflow, just
ordinary key management (single wallet MVP -> 2-of-3 multisig). List gates
registration / surgical NRPT / soft-mode classifier — never resolution.
- ROADMAP-IDEAS.md — recorded SiaGit/GitHub.sia + user-friendly Sia UI ideas.
Full spec: Argus/DESIGN-collision-modes.md (already tracked).
Dropped the OpenSearch "+" item from the engine dropdown and the per-page
OpenSearch scan that fed it (it ran a fetch on every page load). Users add
engines via Settings instead.
- Address-bar security badge + popover lock redesigned to match Firefox's
padlock (shackle + rounded body + keyhole).
- Added AI/LLM answer engines to the catalog: ChatGPT, Claude, Phind, You.com
(Perplexity already present) — all accept a URL query. Off by default; enable
in Settings.
- Favicons now load from DuckDuckGo's icon service (one privacy-respecting host,
returns an icon for ANY domain) instead of guessing /favicon.ico per site,
which failed for several engines and fell back to emoji. Verified loading.