- pdf-editor 0.1.1 → 0.1.2: manifest.icon is a data:image/svg+xml red PDF
document badge (dock renders it as <img>). Same on toolbar-menu.icon.
Item-level icons dropped — those go through the native OS menu that
doesn't render data URIs.
- translate 0.1.0 → 0.1.1: default LibreTranslate mirror was
translate.argosopentech.com, which is now a dead domain — panel just
said "Failed" on every request. Switched default to
translate.disroot.org (currently up), added a datalist of known
mirrors, and a one-shot migration off the dead default so existing
installs recover on next load.
- docx-editor 0.1.0 → 0.1.1: manifest.icon is a data:image/svg+xml blue
DOC document badge, index.js no longer overrides it with 📝, and the
panel header uses the same inline SVG. Same rationale as pdf-editor —
every extension was rendering as either 📄 or 📝, so PDF and Word
were visually identical to the Notepad.
The extensions page could only hand out tarballs; installing meant going
to Settings › Extensions › Community and finding the entry again. Pages
now get window.bcnr.installExtension(id) and Theseus intercepts
theseus://extensions/install/<id> links (page clicks, target=_blank and
the address bar). The page only names a catalog id: Theseus fetches the
catalog and package itself, asks in a native dialog the page cannot draw
over, verifies the publisher signature against the name's current owner
and activates the add-on — the same path a Settings install takes. One
prompt at a time; an already-installed version says so instead of
offering a no-op update.
The site shows the button inside Theseus (feature-detected on the
bridge), a "update Theseus" hint on older builds and a download hint in
other browsers.
The profile folder was Electron's default from the product name
("Theseus Navigator") and add-ons lived in addons\ under it. Now:
%APPDATA%\Theseus\extensions\ installed extensions
%APPDATA%\Theseus\extensions-data\ per-extension storage + scratch
%APPDATA%\Theseus\extensions-backups\ replaced copies
%APPDATA%\Theseus\extensions-staged\ staged updates
Both moves are one-time migrations on the first start that finds the old
layout: the profile folder is renamed (same volume, instant) or copied
when a rename is refused, with the old folder left in place in that case;
the four sub-folders are renamed before the extension host first reads
them. Nothing is deleted. THESEUS_USER_DATA still overrides everything.
The host now hands each extension its data folder as api.dataDir; the
Screenshot and PDF editor add-ons used to rebuild the old path from their
own folder for scratch files (so they recreated addons-data\ after the
move) and now use the field, with versions bumped so the bundles reseed.
A .docx editor is a megabyte of vendored library. Bundling it would charge
that to everyone who wanted a browser, including the people who will never
open a Word document in it.
So it leaves the build: out of bundled-addons/, out of extraResources, absent
from a fresh profile. It arrives the way anyone else's extension does —
Settings › Extensions › Community, from the catalogue the gateway builds, and
listed on theseus.x/extensions alongside everything else published there.
That also means it is signed by the owner of a BNS name rather than by the
operator key, which is the right trust story for something that isn't part of
the browser.
`npm run pack` produces the tarball the publish page takes; the signature
needs the publisher name's wallet, so it isn't something the repo can do.
The end-to-end test now installs the extension into a throwaway profile the
way the community installer would, and asserts up front that a fresh profile
doesn't already have it — the bundling is what was being removed, so it is
worth a test that would notice it coming back.
A PDF that needs a signature, a highlight or a page removed currently sends
the user out to a desktop application or, worse, to a web service that wants
the document uploaded first. Both are poor answers for a browser whose point
is that nothing has to leave the machine. This is a full-tab editor that opens
a PDF, marks it up, fills its forms and saves a new copy, entirely locally.
Two engines, vendored rather than installed, because an add-on ships as a
self-contained folder over the signed update channel and nothing runs a
package manager on the way: pdf.js reads and renders, pdf-lib writes. They
share no state. Everything in between lives in PDF user space — points,
origin bottom-left — which is the one coordinate vocabulary both speak, so a
mark survives zooming, rotating and reordering with no conversion table and
save-time needs to know nothing about how a page happened to be displayed.
The page strip is built from pdf.js's PDFPageView components rather than its
PDFViewer, which renders pages in the file's own order and cannot hide,
reorder or individually rotate one — three of the features here. Text layers
are ours and stay attached for every page, drawn or not, because Theseus's
find bar is Chromium's findInPage over the live DOM and a torn-down text layer
is a page Ctrl+F cannot see. Canvases are virtualised; a letter page at 100%
is 3.4 MB of bitmap.
Redaction is the part worth being careful about. A black box over text hides
nothing — the text stays in the content stream and comes straight out of a
copy-paste — so the editor says so in a modal before the tool can be used,
and on save rebuilds each redacted page as an image, which genuinely removes
it. Pages that were not redacted are untouched. Form widgets and links are
kept, since they were never the leak.
Saving never writes over the original: every save reloads the source bytes and
replays the session onto a fresh copy, so a botched save cannot poison the
next one.
Out of scope for this first version: editing the text that is already in the
document, and writing XFA forms back (pdf-lib cannot, so those are fill-and-
print only, and the editor says so on open).
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.
Three things keep this one honest.
The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.
The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.
Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.
Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
Adds a bundled add-on `translate` with a sidebar panel + a right-click
"Translate selection" menu item. Two swappable backends:
- LibreTranslate (default) — free MIT engine; the panel's Settings tab
lets the user point at any instance (public or self-hosted) and drop
in an API key if one's required.
- Google (unofficial free endpoint at translate.googleapis.com/
translate_a/single) — no key, wide coverage, but unofficial and
Google can break it any time. Opt-in fallback.
Flow: user selects text on a page, right-clicks -> "Translate
selection". Add-on's context-menu handler stashes the selection under
storage.__pending and calls api.revealSidebar("main"); the panel
loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter
in the input textarea also translates. Source + target language
choices, browser-language default target, swap button, copy-to-
clipboard on the output, settings gear.
Depends on a new "context-menu-item" capability + api.revealSidebar
hook in addons-host.js / main.js. Those wiring changes are prepared
but not committed here — a parallel session is refactoring the same
functions concurrently, so the safe path is to land translate/ first
and let the wiring go in alongside the next host-facing commit. Until
the wiring lands, the manifest's "context-menu-item" cap is silently
dropped (per validateManifest's unknown-caps policy) and the sidebar
panel + the panel's translation UI still work standalone — the
right-click entry point is what's gated.
Tabs keep a readable minimum width (76px). Once they overflow, the row
scrolls: the earliest tabs slide out on the left and an arrow at each end
moves the row by 60% of its width, disabled at its end of travel and
hidden while everything fits. The wheel scrolls the row too, the selected
tab is brought into view when the selection changes (never while the user
is scrolling), and the + button stays outside the row. Moves are immediate
rather than animated: the chrome view has no smooth scrolling, and frame
callbacks stop while the window is occluded, which stranded a frame-driven
slide at its start.
With many tabs open, the last ones slid under the minimise / maximise /
close overlay: the strip reserved that space as padding, and padding does
not stop overflowing flex items. The tabs now live in their own row that
clips at its own edge, shrink down to icon + close before anything is
hidden, and the row scrolls sideways with the wheel once they hit that
minimum, keeping the selected tab in view. The + button sits outside the
row so it stays reachable no matter how many tabs are open.
The home / new-tab page declared no icon, so those tabs sat blank in the
strip; the compass mark is now inline in home.html as a data: URI (no file
path, no network). The silentmode.st copy of the Theseus page pointed its
icon at /assets/favicon.svg, which only exists on theseus.x — page-relative
now, so the tab shows the mark there too.
Holding Back or Forward for 450 ms (or right-clicking it) pops a native
menu of the tab's history entries in that direction — nearest first, up to
15, titled with the page title and host — and picking one jumps straight
to it. A hold swallows the click that would otherwise fire on release, so
a long press never also goes back one page.
Anyone who owns a BCDN name can now publish a Theseus extension, and every
Theseus can install it with the publisher's signature verified locally.
Gateway (Argus/src/gateway/public-gateway.mjs):
PUT /api/ext/<name>/<id>/<version> takes the gzipped tar, checks two BCH
message signatures against the name's current NFT owner (one authorises
the upload, one is stored in the channel), inspects the package
(addon.json at the root, id/version/main match, 8 MB cap), enforces
first-publisher ownership of an id and monotonic versions, and writes the
tarball, the extension's updates.json and community/catalog.json to Sia.
GET /api/ext/catalog reads the catalog back with CORS.
Theseus:
lib/publisher-sig.mjs recovers the signer of a channel entry; main.js
compares it with the publisher name's owner from Theseus's own chain
index before installing or updating, so neither the relay nor a tampered
catalog can pass off code under a trusted name. addon-updater.js gains
installCommunity() and accepts publisher-signed entries in the regular
update check (operator Ed25519 entries unchanged). Settings › Extensions
shows the community catalog with Install / Update; Settings › Plug-ins
links to theseus.x/plug-ins.
theseus.x:
/plug-ins/ is a separate page for the first-party plug-ins (Aegis,
Ariadne's Thread) with live versions and hashes; /extensions/ lists the
bundled extensions, the community catalog, and how to build and publish;
/extensions/publish/ signs and uploads a package in the browser with the
wallet that holds the publisher's name (session helper + wallet bundle
copied alongside).
- Address bar: the chrome view keeps document.activeElement on the URL
input after the user clicks into the page (focus moved to the tab's own
view), and the "don't clobber typed text" guard then froze the bar until
something blurred the field. The guard now requires real focus
(document.hasFocus()), and the field is blurred when the view loses focus.
- Tab strip: rebuilt with innerHTML on every tabs event, which recreated
every favicon <img> — a blink on the other tabs whenever one tab loaded,
reloaded or changed title — and dropped drag state mid-gesture. Elements
are now keyed by tab id (chips by group colour), updated in place, and
moved into order; the strip is never rebuilt.
- Settings › Extensions links to theseus.x/extensions. That page now has a
card per bundled add-on with the current signed version, tarball and
hash read from each add-on's updates.json at load (it still claimed
Screenshot 0.2.4 while the channel serves 0.6.5).
A year-old engine is now a bot signal in itself: DataDome blocked
estore.asus.com for Theseus on Chromium 130 while the same request claiming
Chrome 152 went through, and Chromium 130 carries a year of unpatched
renderer bugs. Electron 44 boots the app unchanged; verified on the new
engine: local files, HTTP auth prompt, tab strip in the title bar, BNS
sites and window.bcnr, all bundled add-ons, the Tor toggle
(check.torproject.org via the SOCKS agent), and a full NSIS + portable
build (artifacts grow from ~99 MB to ~132 MB with the larger engine).
session.setPreloads is deprecated from 35 on; preloads are registered
with registerPreloadScript when available, with the old call as fallback.
estore.asus.com (DataDome, "AI Threats Detection") served its block page to
Theseus while a plain Chromium on the same connection got the product page.
Three things in our identity were wrong:
- The client-hint brand list was hand-written with "Google Chrome" first —
a permutation real Chrome never sends. It is now computed the way Chromium
does it (GREASE brand from the major version, per-major brand order).
- The page-side navigator.userAgentData still said "Chromium" only, so
headers and JS disagreed. The same metadata is now installed per tab via
Emulation.setUserAgentOverride, so both sides match.
- Accept-Language went out as "en-US,en;q=0.8;q=0.9": we appended a q-value
and Chromium appended another. Chromium now gets a plain language list.
That makes the identity self-consistent, but DataDome still blocks on the
version: Chromium 130 (Electron 33) is a year old, and claiming Chrome 152
(THESEUS_CHROME_VERSION, added here for exactly this test) loads the page.
The real fix is a current Electron; this commit removes the other tells.
Three tab-strip changes from use:
- A tab opened from a link (target=_blank, middle-click, the context menu,
Duplicate) now goes right after the tab it came from — and after any
siblings that tab already opened — instead of at the end of the strip.
The + button, session restore and add-on requests still append.
- The selected tab gets an accent stripe and outline on top of its brighter
fill; with a dozen same-size tabs the fill alone was easy to lose. A
grouped tab keeps its group colour on the stripe.
- On Windows the tab row is the title bar: the native frame is hidden, the
minimise/maximise/close buttons are drawn as an overlay over the chrome
(colours follow the theme), the row is a drag region with every control
in it opted out, and 140px (or the overlay's real width when the API is
exposed) is kept clear on the right. The page gains the old title bar's
height. Other platforms keep the native frame.
Two annotation-editor asks:
- Line tool: same drag flow as the arrow, no arrowhead. New toolbar
button between arrow and rect, shortcut L.
- Text box is resizable: swapped the single-line input for a textarea
with resize:both and a drag corner. Enter still commits, Shift+Enter
inserts a newline, blur commits. Multi-line rendering steps the
fillText baseline by 1.15x the font size per line so the baked
pixels match the live layout.
The textarea swallows its own pointer events so drag-resizing the
corner doesn't leak to the canvas underneath.
Two reasons the Plug-ins card looked dead ("only a Refresh button"):
1. The state check ran Get-ScheduledTask, whose module import took 8–10 s
cold, and only then fetched the release manifest. Every button is hidden
during "checking…", so for 10–15 s the card showed nothing but Refresh.
Task state now comes from the Task Scheduler COM object (numeric, locale-
independent — schtasks.exe prints localized words on non-English
Windows) and the manifest fetch runs in parallel: ~2 s.
2. The Inno installer's AppId is written as {{…}}, which Inno registers as
{…}}_is1 (doubled closing brace). Theseus looked for the single-brace
key, never found it, and so never knew the installed version — no Update
button, no Uninstall button. The entry is now found by DisplayName.
Owners can now publish a signed _records.json (A/AAAA/MX/TXT/CNAME/NS)
beside their Sia content; the gateway verifies it against the current NFT
holder and serves it as GET /api/dns/<name>. Every BCDN resolution now
starts a background fetch of that answer (3 s cap, 30 s cache, seq rollback
guard) and attaches it to the entry as entry.dns. Navigation never waits
for it — on-chain h/s3/ip/p/u stay authoritative — except when a name has
no content record at all and a signed A is the only way to reach it. Only
registered names are looked up, so ICANN hosts never reach the gateway.
Exposed as window.bcnr.dnsRecords(name) for add-ons (TXT verification, MX
for mail bridges), on resolveName() as .dns, and as a "Signed DNS" row in
the site-info popover.
df181d9 and b2c6f62 were staged hunk-by-hunk from a working tree that also
carried unrelated uncommitted edits, and the context-free hunks landed a
few lines off: the local-file check ran after the search rewrite (so paths
still went to the search engine in the committed file), the loadBns header
sat inside loadLocalFile's comment, and the refreshTabUrl comment was split
by the auth block. Content is unchanged; only placement is corrected.
Sites behind Basic/Digest auth (silentmode.st/guardian/admin) rendered the
server's 401 page because nothing listened for Electron's login event,
which cancels every challenge by default. A modal sign-in prompt now asks
for the credentials and answers the challenge; Cancel leaves the 401 page.
Concurrent challenges for the same host and realm share one prompt while it
is open, and a rejected answer re-prompts instead of replaying the same
credentials until Chromium gives up with ERR_TOO_MANY_RETRIES.
Also: THESEUS_NO_UPDATE_CHECK skips the release check, for throwaway dev
instances — the one-click install chip they show targets the real install.
A typed or pasted path such as D:\Dev\x\page.html has no dotted host, so
the URL-vs-search heuristic handed it to the search engine. Paths (drive,
UNC, file://, and absolute/~ on POSIX) now load as file:// URLs before the
heuristic runs. Local-file tabs keep their file:// URL in the address bar
(normally suppressed because our own home/error pages are file://), show a
"Local file" badge, and hide the registry button since no name resolution
is involved. A missing file lands on the error page with a matching badge.
Theseus core:
- addons-host: manifest.category ("plugin") propagates through snapshot(); new
addon API surface checkAndStageSelfUpdate() + restartApp() so a plug-in
can offer in-panel "update now → restart to apply" without pushing the
user to Settings.
- main.js: wires the two new hooks into the AddonHost constructor.
- settings.html: Extensions listing filters out category==="plugin"; those
add-ons live in Plug-ins instead, single source of truth.
Aegis 0.6.31:
- BTC picker trimmed to Signet only; testnet3 hidden (adapter kept so any
existing wallet still loads).
- Wallet strip groups by chain, not chain:network; ticker gets a ▾ chevron
and a dropdown listing every subnetwork with its own totals. Mainnet
reads as the plain ticker; testnets carry a small Chipnet/Signet/Sepolia
pill inline.
- Per-unit price sits directly under the ticker; amount + fiat mirror on
the right — one glance covers name/price/holding/value.
- + Add and ⋯ More promoted from the strip into the header's action row,
next to the new ✎ chip (was the redundant top ⋯). Duplicate "Manage
current wallet" entry removed from the More menu.
- Footer update chip is a two-step flow via the new API: stage → restart.
Falls back to opening Settings on any Theseus that lacks the hooks.
- Manifest declares "category": "plugin".
seedBundledAddons reseeded whenever the user copy's version differed from
the bundled one. promoteStagedUpdates runs just before it, so a signed
over-the-air update that had just been promoted (e.g. Aegis 0.6.14 over
the bundled 0.6.2) was backed up and replaced by the older bundle on the
same boot — every OTA add-on update silently reverted at the next launch.
Reseed now only when the bundle is newer, using the same version compare
the promoter uses.
A bns:// fetch that fails after the name resolved (relay unreachable, DNS
stalling, the site's own server down) used to answer with the bare text
"Theseus error: fetch failed", which reads as a broken browser. The
handler now returns a styled page that names the host, the upstream it
tried (navigate.st, the p-record origin or the ip record), the error and
its cause code, explains the likely reason per cause (unreachable vs DNS),
and offers a retry.
A 0.3.44 → 0.3.45 auto-update on 2026-09-11 left the install without
app.asar and ffmpeg.dll ("ffmpeg.dll not found" at launch). The setup was
hash-verified; the old-version uninstaller had moved the whole old install
into its temp folder when both NSIS processes died ~8 s after the spawn,
and the install step never wrote a file. The killer was not identified, so
every overlap with the app's own lifetime is removed instead:
- install-update-now no longer spawns the setup; it records the path and
quits. will-quit writes <userData>\update-helper.cmd and starts it as a
detached cmd.exe (verified to outlive the app; not a child of ours).
- The helper waits for our PID to be gone (child powershell Wait-Process),
gives Chromium's children a grace period, runs the setup directly, and
runs it once more if resources\app.asar is missing afterwards — the
installer is idempotent, so a second pass repairs a torn install. The
helper deletes itself.
- Zone.Identifier is stripped from the verified download so nothing that
starts it through the shell raises a mark-of-the-web prompt.
Console-less cmd.exe traps discovered and designed around (see the module):
child console programs' redirected stdout is empty (no tasklist|find
probing), `start /wait` on a .cmd hangs, a detached powershell.exe
started straight from Node does nothing, `timeout` needs a console.
Scenario tests: setup starts only after the process exits, once with
app.asar present, twice without, helper gone afterwards.
A standalone page window on the same session (cookies, bns:// protocol,
session-wide bcnr preload) with Theseus's fingerprint + WebRTC policy and
no toolbar. Loads BCNR-first like a tab: a dotted host with a BCNR record
goes over bns://, otherwise clearnet; collision names follow the configured
policy without the "Open with…" interstitial. Cross-host navigations inside
the window stay BCNR-first; popups go to the main window's tabs. Its own
context menu offers open-in-tab / open-in-window / copy link and
back/forward/reload. Add-on page bridges (wallet inject) are tab-scoped and
don't run in these windows. openLinkWindow is exported for the test harness.
Verified in the dev app: coinspectrum.x opened as bns://coinspectrum.x with
the page title; navigate.st stayed https.
The publishing steps pointed at scp to /opt/silent-mode/site/addons/, which
never existed; the screenshot channel (and now aegis) is served from
s3://bns/theseus/extensions/<id>/ through navigate.st/bns/theseus.x/….
Also: sign from a git-archive copy so uncommitted edits don't ship.
Theseus (Settings › Plug-ins › Ariadne's Thread):
- The elevated start/stop script was embedded in a double-quoted outer
PowerShell string, so `$t` was interpolated away before the elevated
shell saw it. It received `foreach ( in …)`, failed to parse, and the
outer shell still exited 0 — "Turn on/off" reported success while doing
nothing, in every shipped build. The script now goes across as
-EncodedCommand. Off = Stop + Disable (the daemon task has an
at-startup trigger, so a plain stop came back on reboot); on = Enable +
Start. Exit 2 = daemon task missing, surfaced as a clear error.
- Version/update check now reads dl.silentmode.st's releases manifest,
the same one the Theseus updater uses. The silentmode.st copy lagged a
day behind (still listing Theseus 0.3.31), so a new Ariadne release
published to dl would not have been offered.
- Install/update/uninstall now propagate the installer's exit code
(-PassThru; exit $p.ExitCode) instead of always reading as success.
Resolver package (needs a new installer build to reach users):
- uninstall.ps1 removed only the ".bch" NRPT rule; install.ps1 adds one
per advertised TLD. Sweep every "BNS .<tld> resolver" rule.
Verified: daemon resolves BNS names and passes ICANN A/AAAA through when
run unprivileged on port 15353; the encoded-command construction runs
intact and propagates exit codes 0/2 in an unelevated reproduction.
- Per-tab page zoom on Chrome's ladder (25–500 %) via setZoomFactor, so
Chromium keys it per host: every tab on a site shares the level and it
persists across navigations and restarts. Ctrl +/=/numpad+ in,
Ctrl -/numpad- out, Ctrl 0 reset, Ctrl+wheel via zoom-changed. A
percentage chip appears in the address bar when a tab isn't at 100 %;
clicking it resets. Settings and add-on tabs never zoom.
- Address bar / search bar drag ratio floor lowered from 30 % to 20 %,
so the split runs 80/20 to 20/80 (pixel floors still apply).
- The collapsed extension-dock button and its dropdown printed a data:
URI icon as text ("data:image/svg+xml…"). One addonIconHtml() renderer
now serves the dock buttons, the collapsed button and the dropdown.
Rolling every user report from the 0.6.3 rollout into one bundle:
Sounds — the Web-Audio synth palette matches the metaphor now:
- Screenshot: Polaroid shutter — sharp metallic tick + curtain-close click
chained to a film-advance whir (band-passed noise sweeping 900→400 Hz).
- Copy: printer "chika-chika-chika" — three descending percussive noise
bursts pinned by short sine ticks. Reads as a print-head sweep.
- Discard: paper crumple — three overlapping band-limited noise beds
with per-sample random-amplitude crackle, descending centre freq. No
more descending sine "boop".
- Save: soft "photo dispensing" hiss (Polaroid ejects) + a small click.
- Both the panel and editor share the design so nothing sounds different
depending on which surface fired it.
UI polish:
- Discard button now carries a trash-can icon so it's obviously not the
same as the close-sidebar X (they both used to be plain X's).
- Toolbar drawing tools centre themselves via a new .tool-cluster
wrapper (flex:1 1 auto, justify-content:center); the Copy/Save actions
stay right-anchored via margin-left:auto on their own tgroup. Fixes
the maximized-sidebar case where the drawing groups all crowded the
left with a big empty gap before Copy/Save on the right.
- Filename moves out of the topbar into a dedicated footer strip under
the canvas board, alongside a new "Open in folder" button. The topbar
is now flex-wrap:nowrap and holds only fixed-width window controls,
so a long filename can never push discard / sound / max / close onto
a second row (the filename ellipsises instead).
- "Open in folder" invokes a new "openFolder" addon message that calls
Electron's shell.showItemInFolder() to open the OS file explorer with
the specific scratch PNG highlighted (falls back to shell.openPath()
on the scratch dir when no capture is named).
Version bump so the OTA update endpoint picks it up on the next tick.
Four issues from the user's report on 0.6.2:
- Recent captures had a global "clear all" but no way to drop a single
screenshot. Each tile now grows a small × button (visible on hover;
drops in behind the thumbnail preview so it never obstructs the
content). Clicking the × invokes clearRecent({name}) and removes both
the ring entry and the scratch PNG on disk. Bubble-guarded so the ×
click doesn't also trigger the tile's "load into preview" handler.
- Select tool button removed — clicking it did nothing visible, so users
read it as broken. The internal "select" mode still exists as the
no-tool state; you get back to it now by clicking the same drawing
tool a second time (toggle-off) or hitting Escape. The active-drawing-
tool button flips its border when armed.
- Text tool made unmistakable: input paints with a 2 px acid border, a
glowing acid halo, dark background, and the visible ink colour on the
text itself. Focus attempt is three-layered (sync, rAF, timer) to
outrun any Chromium build that drops the mid-pointer-event focus. Non-
Enter/Escape keys get stopPropagation so a stray document listener
can't steal the focus mid-typing.
- Panel header's sound / max / close cluster kept nudging inward when
the status text was empty. The parent's `justify-content: space-
between` distributed the row unevenly. Force-anchor the cluster with
`#btn-sound { margin-left: auto }` so the three window-control icons
hug the right edge regardless of what fills the middle.
Bundled but not shipped separately — parent session signs and pushes.
Layout reorganisation from user's diagram:
- Copy + Save move out of the topbar into the toolbar as their own
right-anchored tgroup (margin-left:auto). On wide sidebars they sit at
the end of the drawing-tool row; when the sidebar is narrow, the
actions cluster wraps as its own row on the right instead of nudging
the drawing tools around. Toolbar switches from justify-content:center
to flex-start so the leading tool groups pack left and the actions
group can find the right edge cleanly.
- Topbar right cluster is now Discard / Sound / Maximize / Close — Copy
and Save are gone from the topbar entirely so the right edge reads
as controls-only, not action-mixed.
- Discard button (X icon, danger red on hover) throws away the current
capture — silentmode.invoke("clearRecent", {name}) removes it from the
ring and unlinks the scratch file — then navigates back to the panel.
Distinct from Back, which is non-destructive.
- Close button already existed from 0.6.1 but stays in the same
right-edge position for continuity.
Bundled but not shipped separately — parent session signs and pushes.
- Link-status pill: it measured its own width inside a view already
capped at 100 px, so it could never grow and long hrefs were cut short.
An off-screen twin now reports the natural width; main caps it to the
tab area (never under the sidebar) and the pill ellipsises past that.
- Address bar at narrow widths: the URL input's intrinsic minimum width
pushed the registry chips and the star out past the bar. #url now has
min-width: 0 and the trailing controls are fixed-size flex items.
- The BCDN/ICANN segmented chips are replaced by one Ariadne's Thread
icon (spiral + tail) at the end of the bar: acid when served from BCDN,
blue for ICANN, caret when the name exists on both. Click opens a
native menu (registry-menu-popup): switch registry, remember per name /
per TLD, forget choices, collision policy, and a jump to the Plug-ins
settings section. Reuses the existing switch / remember / policy paths
(collision-switch body extracted to switchRegistry, open-settings to
openSettingsTab). preload's openSettings now forwards a section slug.
Users saw a blank window with a white strip across the top for seconds
on launch. Root cause: every part of startup, including session restore,
waited for chrome.html's did-finish-load. That event also waits for the
page's subresources, and the bookmarks bar loads its favicons over
bns:// — a BNS lookup plus a network fetch each — so a slow link held the
whole boot. On top of that, seven hidden overlay renderers, every restored
tab, the BNS index build and three network fetches all started in the
same tick and stalled the main thread ~1 s while the toolbar tried to
paint.
- Continue boot at chrome.html's dom-ready (toolbar scripts have run, IPC
listeners exist) instead of did-finish-load; 8 s fallback timer.
- Window and chrome view get the toolbar's --bg for the active theme so
the pre-paint frame is never white.
- Overlay pages (site info, engine picker, downloads, suggestions,
password fill, link status, approval) load 250 ms after the toolbar or
on first use; the approval modal awaits its page so a dapp request
can't hang.
- Session restore is staggered: active tab first, then one background
tab per 150 ms slotted into its saved strip position. Session file v2
records the active index; v1 arrays still load (active = last, as the
old loop effectively did).
- AddonHost gains api.whenUiReady(); Aegis 0.6.2 defers its heavy
dependency loading (noble precompute, bitcoinjs, libauth, WizardConnect)
behind it.
- BNS snapshot warm-up still starts right after createWindow (bookmark
favicons need it); Sia refresh, update check and home-card fetch move
to the post-paint phase.
Measured on a clone of the real profile with nine restored tabs: toolbar
usable at ~0.7 s instead of ~1.5 s, main-thread stall during toolbar load
down from ~1.1 s to ~0.2 s.
Four user reports from the 0.6.0 rollout:
- Text tool never committed. openTextInput placed the box correctly but
a couple of Chromium quirks stopped a normal type-Enter cycle:
focus() called synchronously right after appendChild lost the race
in some builds, and the input's own mousedown / click was bubbling
through to #base and re-firing openTextInput on every subsequent
keystroke click-through, so what looked like "nothing happens" was
actually "a new empty box spawned on top of the last one every time".
Now: focus after requestAnimationFrame, contain pointerdown / mousedown
/ click inside the input so they don't bubble to the canvas, track
the font size on the state so commit uses the same one openTextInput
measured against, and preventDefault on the base pointerdown so
Chromium doesn't reset focus back to <body>.
- Toolbar wrapped one dot at a time when the sidebar was narrow (a
lonely thin/medium/thick width would jump to a second row while the
swatches stayed above it). Toolbar items are now wrapped in
`<div class="tgroup">` per category — tools / swatches / widths /
undo-redo — with `flex: 0 0 auto`, so a whole row wraps as a unit
and lands cleanly under the previous one. `gap: 10px / row-gap: 6px`
keeps the visual grouping obvious.
- No way to close the sidebar without hunting for the dock icon. Added
an X button in the top-right of both the sidebar panel and the
editor toolbar. Both wire through a new `silentmode.sidebar.close()`
preload method that calls the existing `sidebar-close` IPC.
- Tightened the pointerdown text branch so preventDefault + explicit
focus-after-frame make the click-through races impossible.
Bundled but not shipped separately — parent session signs and pushes.
The resize grip lived along the sidebar's left edge as a 5-px transparent
hover target — you couldn't see it existed until the pointer landed on it.
Users on both light and dark backgrounds reported the seam between the
tab area and the sidebar as invisible.
Paint a 2-px semi-opaque mid-gray line (`rgba(140,150,170,.55)`) at rest so
the boundary is legible on every panel background; hover ramps to acid
green, active-drag ramps brighter. The visible band is narrower than
before (2 px vs 5 px) so it reads as a subtle divider rather than
competing chrome; the pointer-catch zone stays wide via an invisible
outline extension, so drag-to-resize still catches slack.
scripts/push-split.sh pushes one monorepo subdirectory to its matching
split repo on Hephaestus via an ephemeral git-subtree-split branch, so
history is preserved on the forge side. The remote name is derived from
the directory (Navigator/Resolver suffix stripped) or passed explicitly.
TheseusNavigator/test-installer.wsb is a Windows Sandbox profile that
maps dist-public read-only and launches the setup exe on logon, for
clean-machine installer checks.
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
Editor:
- Crop tool restored — drag to select, marquee sits with a dashed acid
border and a dimmed backdrop for the area you'll discard, then the
topbar shows Apply crop / Cancel. Applying trims #base to the rect,
resets undo (dimensions changed), and drops back into the select tool.
Enter / Esc keyboard shortcuts while a crop is pending.
- Blur / mosaic redaction tool back — drag a rectangle, editor
downsamples that region of #base to ~12-block granularity and paints
the blocks back nearest-neighbour. Commits directly (no confirm step).
- Sidebar-window controls (Back, name, Copy, Save, Sound, Maximize)
reflow: Back + name on the left, Copy + Save + Sound + Maximize on
the right so the "put the sidebar back to normal size" affordance
lives where users expect it. Toolbar's drawing tools stay centred.
- Back arrow icon swapped from a chevron to a proper flat arrow
(line + arrowhead), matching the new browser back/forward glyphs.
Sounds — modeled on Firefox Screenshots' feedback rather than beeps:
- Shutter is now a real photoshoot click: two mirror-slaps built from a
band-passed noise burst (metallic ping) plus a very short square-wave
thud each. Sounds like a camera, not a beep.
- Copy is a two-chirp "printer feed" — filtered noise burst on top of a
sine chirp per beat, staccato ascending pair. Same shape Firefox Easy
Screenshot uses for "copied to clipboard".
- Save keeps its ascending triad; Discard keeps its descending pair;
new small ascending pair for Apply crop.
Chrome:
- Browser Back / Forward chevrons (M10 3 L5 8 L10 13 — two segments
meeting at a point, no shaft) replaced with straight-arrow glyphs
(line + arrowhead). Reads as a navigation arrow, not an angle bracket.
Bundled but not shipped separately — parent session OTA-signs and pushes.
Cloudflare Bot Fight Mode / Turnstile flag 'UA claims Chrome but client
hints don't confirm it' as bot. Electron's default sec-ch-ua reads
'Chromium';v='130', 'Not(A:Brand';v='99' — no 'Google Chrome' brand
(that's closed-source Google branding open Chromium doesn't carry).
Combined with a UA that's already stripped of the Electron token
(stockChromeUA), the mismatch itself is the fingerprint. This is what
whybitcoincash.com and other CF-fronted sites tripped on: server
returned 503 to Theseus while returning 200 to any curl variant.
Brave, Vivaldi and Opera solved this the same way — ship their own
sec-ch-ua that INCLUDES Chrome-family brands so CF's allow-list catches
them. New applyClientHintsSpoof() registers a session-wide
onBeforeSendHeaders that rewrites the sec-ch-ua family on every
outbound request:
sec-ch-ua: 'Google Chrome';v=<major>, 'Chromium';v=<major>, 'Not?A_Brand';v='99'
sec-ch-ua-full-version-list: same trio with real Chromium version
sec-ch-ua-mobile: '?0'
sec-ch-ua-platform: actual OS name (Windows / macOS / Linux)
Major comes from process.versions.chrome so the story stays internally
consistent — nothing to fingerprint from a Chrome/version mismatch.
Runs alongside applyEmbedCookieShim which uses onHeadersReceived; the
two hooks are separate so no listener collision.
The Extensions page had a "Pending updates" strip at the top listing the
staged versions AND a "Check for updates" button that dumped a summary of
every extension's status into a global status blob just below the button.
Two places to look for what a single card was doing.
Fold both surfaces into the extension card itself:
- Each card grows a small update line under its description: green ↻
"Update vX.Y.Z staged — restart to apply" when a staged tarball is
waiting, red "Update failed" (with the addon-updater's detail) when
the last check-updates run couldn't advance the version, plain "Up to
date" when it could and there was nothing newer.
- The top strip is gone. The "Check for updates" button now just prints a
one-line summary (N staged / N failed / all up to date) — the detail
lives on each card.
- listStagedAddonUpdates fires on tab visit and after Reload, so the
card badge reflects the background poll without needing the user to
click Check.