Commit graph

69 commits

Author SHA1 Message Date
Local Dev
806a976647 vpn 0.1.2 → 0.1.3: subscription import + landing page mockup
Addon:
- Paste any https:// URL that returns a list of vless:// (either
  newline-separated or base64) and the extension fetches, decodes,
  parses, and adds every server to the dropdown. The full vless URL
  never leaves the panel — the addon holds it in its own storage and
  passes an opaque "sub-<hash>" id back for selection.
- Subscription CRUD on the addon side (listSubscriptions,
  addSubscription, refreshSubscription, removeSubscription). A refresh
  is a no-op inside the 6-hour TTL to avoid pounding the provider.
- Merges subscription servers with the baked-in three and gateway
  overlay by id; the dropdown groups them under one banner.

Site:
- silentmode.st/vpn landing page: three-plan grid (Free, Pro at $1/mo
  BCH, Max at $4/mo BCH), how-it-works four-step block, "the three
  servers" strip with per-tier availability, why-this-VPN cards, FAQ.
  Priced in USD, paid in BCH via the oracle at pay-time — same pattern
  as the marketplace's USD-listing covenant, no reintroduction of
  fiat/card processors.
2026-09-22 20:59:50 +02:00
Local Dev
3d955720cb vpn 0.1.1 → 0.1.2: server-list dropdown + gateway overlay
Every commercial VPN client stores its server catalog as a JSON on the
backend and lets the panel pick from a dropdown; this pulls that shape
into the extension.

- server-list.json: baked-in default the tarball ships with. Three
  Silent Mode slots (sm-1..sm-3), status "coming-soon" until the VLESS
  URLs land — the toggle stays disabled for any entry whose status is
  not "ready", so a placeholder cannot be selected by accident.
- Gateway overlay: index.js fetches
  https://navigate.st/api/vpn/servers on activation (with a 6-hour TTL
  and a "refresh" button in the panel) and merges by id — remote wins,
  new remote entries append. Cached to per-addon storage so an offline
  boot still has the last-good catalog.
- turnOn now accepts { serverId } or { vless }. Server id is resolved
  through the catalog inside the addon; the panel only sees a public
  view (label, flag, country, ready/coming-soon), never the raw URL.
- Panel: dropdown of servers + a "Custom vless://" option that reveals
  the paste box. Selection persists per-machine, refresh button forces
  a re-fetch, disabled toggle explains why in the hint area.

No behavioural change for anyone with a saved vless:// paste — that
path is now "Custom" in the dropdown and still works identically.
2026-09-22 19:56:01 +02:00
Local Dev
b5641b33c8 chore(docx-editor): 0.2.0
Since 0.1.1: Save as… with a real file dialog, PDF export through Chromium's
print pipeline, several documents open at once as tabs with their own close
buttons and menus, a page that scales to fill the window, and Ubuntu and
Fraunces bundled so the ribbon can offer fonts Windows does not have.

The description gains the parts a user would look for before installing.
2026-09-22 19:24:19 +02:00
Local Dev
2f8eaa62ff feat(docx-editor): the page fills the window, and two fonts ship with it
The page sat marooned in the middle of a wide window with dark space either
side of it. It is now drawn at the size the document actually claims — A4
stays A4, margins come from its own sectPr — and CSS `zoom` scales that to
fit, defaulting to Fit width with a control in the footer and Ctrl +/-/0.

Scaling rather than widening is deliberate. A page stretched to the window
would break every line somewhere different from where the printed page
breaks it, and an editor whose whole claim is that it shows you the document
should not lie about where the lines end. `zoom` also beats a transform
here: it affects layout, so the board scrolls correctly and ProseMirror's
coordinate maths keeps working.

Ubuntu and Fraunces now ship in fonts/, because Windows has neither and a
font offered in the ribbon that the machine lacks is a font the user picks
and then cannot see. Fetched once by `npm run fonts` and committed, never at
runtime: an extension in a browser built around not phoning home should not
ask a font CDN what a document looks like every time one is opened.

Two things had to be worked around. On file:// Chromium registers @font-face
rules and then refuses to fetch the files — the family appears in
document.fonts and every glyph still renders in the fallback — so the add-on
reads the woff2 and hands the page a stylesheet with them inlined as data
URLs. The PDF export needed the same treatment for a different reason: its
print window runs from a temp folder, where a relative url() resolves to
nothing, which would have quietly undone the one-stylesheet-for-both promise
that lib/doc-css.js exists to keep. If either path fails, the ribbon labels
those families "(not available)" rather than implying otherwise.

About 700 KB, most of it Ubuntu's Cyrillic and Greek — kept because the
documents this is used on are not all English. Licences ship alongside.
2026-09-22 19:12:56 +02:00
Local Dev
f189b48102 feat(docx-editor): documents are tabs, each with its own close button and menu
Opening a second .docx used to mean a second browser tab: a whole ribbon,
banner and footer repeated, with one ✕ at the far end of a row that also
held the file's name. The name looked like a tab and nothing about it
behaved like one.

Now the editor holds documents the way the browser holds pages. A strip
under the toolbar carries one tab per open document — icon, name, unsaved
dot, its own ✕ — plus a + to open another. Middle-click closes, Ctrl+W
closes, Ctrl+Tab cycles, and right-click (or the caret on the tab under the
pointer) drops a menu: Duplicate, Open in the default app, Show in folder,
Close others, Close. The gestures are the browser's because that is the tab
strip every user of this editor already knows.

Under it, one ProseMirror view is handed a different state per document
rather than one view per tab, and the module-level "current document"
variables are marshalled in and out on a switch. That keeps the change out
of every function that touches the current document, at the price of one
list — DOC_FIELDS in captureActive/adoptDoc — that has to stay complete. A
variable missed there leaks one document's state into another, which would
look like the editor corrupting a file, so it is called out in a comment.

Closing the last document closes the editor tab, the way closing a
browser's last tab closes the window; an empty ribbon staring at the user
is not a state worth having.

The add-on hands a newly opened document to the editor that is already up
and fronts it, falling back to opening a tab if no editor acknowledges
within 900ms — so a crashed or closed editor degrades to exactly the old
behaviour rather than swallowing the document.
2026-09-22 08:35:06 +02:00
Local Dev
e8e603902c feat(pdf-editor): the dock click is the "open a file" gesture, so answer it with one
Opening a PDF cost four clicks across two screens: the dock icon, a dropdown
with a single entry in it, an editor with an empty drop zone, and finally the
Open button that produced a file browser. Three of those were the program
asking the user to confirm what they had already said by clicking a PDF icon.

The dock item now raises the file browser itself. The dialog is native and
raised from the add-on's Node side, because a file:// page cannot open one
without a user gesture of its own and a freshly-opened tab has none to spend.
The chosen file goes through the same scratch handoff a right-clicked link
already used, so the editor opens with the document in it rather than with an
invitation to find one. Cancelling still lands in the empty editor, which is
where drag-and-drop works and is probably still where someone who changed
their mind about the file wants to be.

Second click removed in the chrome: a toolbar menu holding exactly one item is
not a menu, it is a button wearing a dropdown, so it now dispatches directly.
Falls through to the popup if the renderer has no addonMenuSelect.

Not yet exercised end to end — TheseusNavigator/node_modules is empty while
another session reinstalls it, so the CDP suites cannot boot Electron. Both
files parse; run scratchpad/verify-pdf-editor/drive.mjs once the tree is back.
2026-09-22 01:44:15 +02:00
Local Dev
0da15818ad feat(docx-editor): the blue DOC badge, kept as the generated source
Picks the mark from 95c93f9 over the one I drew: at the 16–18px the dock
actually renders, a solid fill reads and an outlined page does not, and
pairing with pdf-editor's red badge makes the two editors obviously a set.

It moves into icon.svg rather than living only as a data URL in the
manifest, so make-icons.mjs keeps deriving the PNGs and addon.json's copy
from one drawing. Same picture as before, one source instead of three
places to forget.
2026-09-22 00:10:56 +02:00
Local Dev
7724dde2ef vpn 0.1.0 → 0.1.1: pin sing-box 1.14.1 sha256s
Binaries uploaded to bns/theseus/vpn-binaries/<platform>/, served
through the gateway at navigate.st/bns/theseus.x/vpn-binaries/.
Manifest hashes match a curl-fetched copy through the gateway.
2026-09-22 00:09:18 +02:00
Local Dev
18eee9dbc7 feat(docx-editor): blue page with a pilcrow, matching pdf-editor's badge
The mark now shares pdf-editor's silhouette, corner geometry and weight, in
blue against its red, so the two editors read as a pair in the dock. It
carries a pilcrow rather than a format label: at 16px "DOC" is a smudge
while the paragraph mark is still a glyph, and it says word processor rather
than file extension.

Drawn as paths, not <text>. An icon that needs a particular font installed
to make sense is an icon that eventually renders wrong somewhere.

Still nothing of Microsoft's: their blue sheet carries a white W, and the
page-with-a-folded-corner is the universal document glyph rather than
anyone's property.

Also adds docs/DESIGN-one-extension-catalogue.md, which writes down the
larger point this kept bumping into — that bundled and community extensions
are two systems for one kind of thing, with two trust rules and two lists in
Settings, and that "ships by default" should be a line of configuration
rather than a separate distribution path. Proposed only; the awkward part is
keeping a fresh offline install usable, which the note answers by treating
the shipped copies as a pre-seeded cache that still verifies like everything
else.
2026-09-22 00:06:50 +02:00
Local Dev
676424db87 feat(theseus/vpn): VPN extension scaffold — sing-box under our own UI
Ships the extension small (~50 KB tarball). No binaries in it — the
platform-matched sing-box is downloaded on first "Turn on" from
bns/theseus.x/vpn-binaries/<platform>/, sha256-verified against the
manifest that ships inside this operator-signed tarball, and cached
under extensions-data/vpn/bin/. Every subsequent launch re-verifies
before spawning; a mismatch redownloads rather than trusts what is on
disk.

Config generator produces a sing-box config from a vless:// URL (the
shape a 3x-UI VLESS+Reality inbound produces), plus a SOCKS5 inbound
on 127.0.0.1:<ephemeral>. api.setSessionProxy points every Theseus
request at that port while the tunnel is up; child.on("exit") clears
it if sing-box dies. Off again clears the proxy back to whatever the
browser had.

Panel is a big on/off toggle with a status pill, a paste-and-save
endpoint box, and an Advanced disclosure with "auto-on at browser
start", "re-download binary" and "clear cache". Any user with a
vless:// URL can flip it on today; the free tier and the Silent Mode
exit inbound are the server-side half, documented under DESIGN.md.

Binary manifest ships with PENDING sha256s until the binaries are
uploaded to Sia — ensureBinary refuses to activate on a platform whose
sha256 is PENDING, so a user cannot flip it on against an unverified
download.
2026-09-21 23:39:32 +02:00
Local Dev
56b3f2f206 feat(docx-editor): ship it in the build, updated over the first-party channel
Moving it out of the build left it with no way in. Settings can only install
from the community catalogue, so a first-party extension that isn't bundled
has a working update channel and no first copy for anyone to update — the
mechanism was all there and the front door was missing.

So it goes back beside screenshot, aegis and pdf-editor: seeded into every
profile by the build, listed under "Built into Theseus", and kept current
between releases by the operator-signed channel at
theseus.x/extensions/docx-editor/. That is the arrangement docs/ADDON-UPDATES.md
describes, and the one the signing script was written for.

About 400 KB compressed in the installer, most of it the vendored editor
libraries — next to the ~4 MB of pdf.js that pdf-editor already ships, the
weight argument for keeping it out didn't survive contact with the numbers.

The end-to-end driver goes back to checking that a fresh profile seeds it,
which is the property that actually matters now.
2026-09-21 22:47:13 +02:00
Local Dev
2b8e3a7eb6 translate 0.1.1 → 0.1.2: fetch from the addon's Node side, not the panel
The panel was doing the HTTP call itself, which meant any mirror sitting
behind a Cloudflare-style anti-bot check returned "<!doctype html>…" for
a POST from Origin: file:// and JSON.parse choked on it. Moving the
fetch into index.js gets rid of the whole class of browser-context
interceptors (CORS preflights, captive portals, anti-bot pages) and
lets the addon look at the response body before trying to parse it —
an HTML body is now reported cleanly as "server returned an HTML page
instead of JSON".

While there, chain a small mirror list — translate.disroot.org,
translate.plausibility.cloud, lingva.ml — so a single mirror being down
does not take the feature with it. A user whose saved URL points at a
mirror that stopped resolving (translate.argosopentech.com is the
notable case) now transparently gets a translation from the next mirror
in line instead of a stack trace.

Verified end-to-end from Node against both a working URL and a dead
one; the dead one falls through to disroot as expected.
2026-09-21 03:34:14 +02:00
Local Dev
0684906f13 feat(pdf-editor): a mark you placed is something you can still work on
Everything the editor put on a page was final. A text stamp could not be
corrected without deleting it and typing it again, nothing could be resized,
and the only way to remove a mark was a Delete key nobody had been told
about — the selection drew a dashed box and offered no action at all. Placing
a stamp also left its tool armed, so the next click stamped a second copy.

Marks are now editable objects. Selecting one gives it grab handles and a
small bar pinned above it: delete and duplicate for anything, and for text an
edit button, a size stepper and bold and italic. Double-clicking text reopens
it for rewriting in place rather than adding a second one. Placing a text
stamp or a signature drops straight back to the select tool with the new mark
live, which is both what people expect and what puts it immediately within
reach of a nudge.

Resizing is one function over every mark type rather than a special case per
kind: a handle drag produces a new bounding box, and the mark is mapped from
its old box into that one. Text scales by font size instead of stretching its
glyphs, signatures keep their aspect on a corner, and lines offer their two
endpoints instead of a box that would let you stretch them in ways you never
aimed at. A whole gesture lands on the undo stack as one step.

Selecting a thin mark used to mean clicking its outline exactly — about one
screen pixel. Each stroked mark now carries an invisible fat copy of itself
purely to catch the pointer.

New marks to go with it: underline and strike-through, which share the
highlight's text-selection geometry and differ only in where the rule sits; a
plain line; and a fill toggle for rectangles and ellipses. Bold and italic
mean three more Helvetica variants embedded at save time, since a PDF treats
them as separate fonts rather than as a style.

Double-click is detected from the pointer stream rather than from a dblclick
listener, because selecting a mark calls preventDefault() on the pointerdown
and that suppresses the compatibility mouse events the browser would have
synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
Local Dev
862e92543c extensions: real PDF/DOC icons + fix translate default URL
- 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.
2026-09-21 02:41:16 +02:00
Local Dev
bd6aab02fe feat(theseus): profile at %APPDATA%\Theseus, extensions under extensions\
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.
2026-09-21 01:55:25 +02:00
Local Dev
5e4bd22609 refactor(docx-editor): ship it as a community extension, not part of the browser
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.
2026-09-21 01:06:58 +02:00
Local Dev
6b2c4b25c0 feat(pdf-editor): read, mark up and reshape a PDF without leaving the browser
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).
2026-09-20 20:58:21 +02:00
Local Dev
92ef408ac5 feat(docx-editor): edit Word documents without quietly eating what Word put in them
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.
2026-09-20 20:46:29 +02:00
Local Dev
501c7cf273 feat(theseus/translate): new sidebar add-on — right-click Translate selection
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.
2026-09-20 17:50:06 +02:00
Local Dev
cd098bc9f3 screenshot 0.6.5: line tool + resizable text box
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.
2026-09-18 01:02:56 +02:00
Local Dev
f46e9112b7 chore(theseus): 0.3.47 — plug-in category + panel-driven addon self-update, aegis 0.6.31
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".
2026-09-14 02:30:51 +02:00
Local Dev
ee5e53512a docs(theseus/prompts): tool-agnostic phrasing in session-prompt templates 2026-09-10 22:21:19 +02:00
Local Dev
7b8539fb8d feat(theseus/screenshot): 0.6.4 — Polaroid sounds, trash icon, centred cluster, filename footer + open-in-folder
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.
2026-09-09 22:34:47 +02:00
Local Dev
8eda0433d7 feat(theseus/screenshot): 0.6.3 — per-tile delete, no Select button, text tool halo, right-anchor panel controls
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.
2026-09-09 22:01:06 +02:00
Local Dev
744574ba96 feat(theseus/screenshot): 0.6.2 — Copy/Save move to toolbar, Discard, right-anchored topbar
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.
2026-09-09 11:46:21 +02:00
Local Dev
c9a3db26ce fix(theseus/boot): paint the toolbar first — stop gating startup on chrome.html's load event
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.
2026-09-09 11:40:45 +02:00
Local Dev
bf6bcfade2 feat(theseus/screenshot): 0.6.1 — text tool fix, category wrap, sidebar close X
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.
2026-09-09 10:55:21 +02:00
Local Dev
992c02ea89 feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
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.
2026-09-09 10:33:21 +02:00
Local Dev
7405e444e7 feat(theseus/screenshot): 0.6.0 — crop + mosaic redaction, right-anchored sidebar controls, real shutter+print sounds
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.
2026-09-09 10:24:03 +02:00
Local Dev
71b803e020 fix(theseus/screenshot): 0.5.1 — hide "Loading capture" for real + centre the tool bar
Two things the shipped 0.5.0 got wrong:

- `.empty { display: flex }` overrode the plain `[hidden]` attribute the
  init flow sets after the image draws, so the "Loading capture…" pill
  stayed visible on top of the finished capture. Global rule
  `[hidden] { display: none !important }` takes it out.

- Tool bar was left-aligned; older editor iterations grouped the drawing
  tools / swatches / widths / undo-redo in the centre of the bar, which
  read better in a narrow sidebar. Adds `.toolbar { justify-content: center }`;
  the topbar's back / max / sound / name / save / copy stay edge-anchored.

Version bump so the OTA update endpoint picks it up on the next tick.
2026-09-09 02:33:45 +02:00
Local Dev
81d276f655 feat(theseus/screenshot): 0.5.0 — sidebar-first editor, direct save/copy, sounds
Two problems the old editor kept hitting:
- __pending drain race: opening the editor a second time (refresh, back-and-
  forth navigation) found the storage entry already consumed and bailed to
  a blank canvas silently.
- Cross-origin img loading: editor.html at file:///…/addons/screenshot/
  loading a scratch PNG at file:///…/addons-data/ counts as cross-origin
  under Chromium's file-URL policy; setting crossOrigin="anonymous" made
  the load fail outright.

Rebuilt editor v2:
- Load path is idempotent: silentmode.invoke("getBytes", {name}) → addon
  reads the scratch file and returns a data URL. No __pending drain, no
  cross-origin trickery — data: URLs are same-origin and never taint the
  canvas, so getImageData / toBlob keep working.
- Two-canvas model (#base + #over, over is pointer-events:none) so live
  previews don't cost a full re-composite per mousemove.
- Tools: cursor, arrow, rect, ellipse, pen, text. 6 swatches, 3 widths,
  undo / redo (25-deep). Copy + Save at the top bar. Back and Maximize
  buttons in the same top bar so navigation controls stay reachable when
  the toolbar wraps at narrow widths.
- Keyboard: A/R/O/P/T select tool, Esc = cursor, Ctrl+Z/Shift+Z undo/redo,
  Ctrl+S save, Ctrl+C copy.
- Toast surface for save/copy/error feedback.

Sidebar panel gains a direct raw-save path so the user can copy or save the
capture without entering the editor:
- Two-row actions: [Copy] [Save] on top, [Discard] [Edit] below.
- Copy uses navigator.clipboard.write(ClipboardItem); Save uses
  <a download> with a Blob URL — same path Chromium's will-download
  tracker already handles, so the file lands in Downloads and the chip
  updates like any other save.

Inline "clear all" confirmation replaces the native confirm() — the old
system-modal opened over the tab area (out of the sidebar's visual
context) and looked like Windows 95. Now a compact red strip appears
under the Recent header with Cancel / Delete buttons.

Sounds + a sound-on/off toggle in both surfaces:
- Web Audio oscillator-synthesized (no .wav shipped): shutter click on
  capture, two-tone bloop on copy, descending pair on discard/back,
  ascending triad on save.
- Preference stored in silentmode.storage under "soundOn" (default on),
  shared between the panel and the editor.

Simplifications:
- Dropped the addon's "arm" onMessage handler (superseded by getBytes).
- Manifest capabilities: sidebar-panel + capture-tab (no open-tab,
  no toolbar-menu).

Bundled but not shipped — parent session handles the OTA sign + push.
2026-09-09 00:56:41 +02:00
Local Dev
1b10afa1ca feat(theseus/aegis): canonical coin logos from cryptocurrency-icons
Replace the hand-drawn approximations with the official SVGs from
github.com/spothq/cryptocurrency-icons — the permissive-licensed set most
exchanges, block explorers, and other wallets standardised on. Users see
the same BCH / BTC / DGB / SC / TRX / ETH / SOL marks in Aegis they
already recognise from Coinmarketcap, Coingecko, Trezor, MetaMask, etc.

- BCH: green disc with the tilted Bitcoin-Cash B
- BTC: orange disc with the classic Bitcoin B glyph
- DGB: blue disc with the DigiByte D + swash
- SC:  brand-green disc with Siacoin's stylised S
- TRX: red disc with the geometric Tron triangle-net
- ETH: purple disc with the two-triangle Ethereum rhombus
- SOL: mint disc with the three-slash Solana mark

All SVGs are inlined in panel.js — no network fetches at panel load.
Bumped addon 0.4.3 → 0.4.4 so seedBundledAddons reseeds the new panel
on next launch.
2026-09-08 22:47:43 +02:00
Local Dev
344c71b315 fix(theseus/aegis): drop registerSidebarPanel icon override so dock inherits brand shield
The toolbar dock still showed the 🛡 emoji even after chrome.html learned
to render data-URI icons — because registerSidebarPanel({icon}) is the
per-panel icon that overrides manifest.icon, and Aegis was passing "🛡"
verbatim. Dropping the override lets addons-host's `icon = manifest.icon`
default kick in, so the dock button pulls the branded aegis.x/brand
shield the manifest now advertises.

Version bumped 0.4.1 → 0.4.3 to force seedBundledAddons to reseed the
new index.js on next launch.
2026-09-08 22:40:48 +02:00
Local Dev
523832cd72 feat(theseus/screenshot): 0.4.0 — editor lives inside the sidebar, maximizable
User report: the sidebar preview lands correctly, but the moment the editor
opens in its own tab the picture is blank. Rather than chase that class of
handoff race again, put the editor in the same webContents as the panel:
the sidebar view navigates panel.html ↔ editor.html in place. Same
document object, same silentmode.storage surface, no cross-tab __pending
transfer at all.

- panel.html "Edit" button now calls silentmode.invoke("arm", …) — the
  add-on rewrites __pending with the currently-previewed capture's bytes,
  and the panel does location.href = "editor.html?name=…". Sidebar view
  loads the editor with the same preload; editor.js's storage-based load
  path pulls the pending entry out and paints.
- editor.html gains a "Back" arrow (returns to panel.html) and a
  maximize / restore icon.
- discard() now navigates to panel.html instead of closeTab() — there is
  no tab to close.
- Manifest drops the "open-tab" capability entirely (no more full-tab
  editor); keeps sidebar-panel + capture-tab.

Framework: new silentmode.sidebar.{maximize, restore, toggleMax, isMax,
onMaxChange}. main.js honours them via new sidebar-maximize / -restore /
-toggle-max / -is-max IPCs, remembering the pre-maximize width so a
restore drops back exactly. The sidebar drag-grip auto-exits maximize
mode on any user drag, so pulling the edge always lands on the pre-max
value plus/minus the delta. sidebar-preload exposes the surface;
chrome.html renderer is untouched — this is a per-panel affordance.

Editor tools (crop / arrow / rect / ellipse / pen / text / mosaic /
undo / redo / copy / save) unchanged. Save still goes through Chromium's
<a download> path, so the file lands in Downloads and appears in the
download chip like any other save.

Bundled but not shipped — leaving version bump + deploy to parent session.
2026-09-08 22:18:41 +02:00
Local Dev
4c01ec7b7d feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab"
0.3.33 still ships blank screenshots because the whole toolbar-menu → auto-
open-editor path can't be made race-free: the moment the editor tab opens
it becomes the active tab, and a snapshot of the editor's own tab (before
its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG.
The lastCapturableTabId fallback I added in a279864 catches the second
click, but the first click can still land on the addon-owned tab whenever
the user re-triggers before setActive has settled.

Rebuild the UX so this class of race can't happen at all:

- Drop the toolbar-menu capability. Manifest is back to sidebar-panel +
  capture-tab + open-tab, so the dock icon opens the panel (never the
  editor directly). No dropdown, no clip-under-tab-view issue, no auto-
  jump into an addon-owned tab.

- Sidebar has the three capture buttons + a preview <img> + a "Open in
  editor tab" button. The preview is fed a data:image/png URL returned
  straight from api.captureTab, rendered inside the sidebar's own
  document — same origin, no file:// cross-directory gotcha, and the user
  can see immediately whether the shot actually landed.

- Editor.html tab opens only on an explicit "Open in editor tab" click.
  The addon rewrites __pending at that moment (so the editor always sees
  the just-selected capture even if a prior editor tab drained the entry),
  then api.openTab("editor.html", {name}). The editor's storage-based
  load path is unchanged.

- Recent captures ring is kept and now exposed as a horizontal thumbnail
  strip in the sidebar; clicking a tile re-previews that capture and
  arms "Open in editor tab" for it.

Editor page (editor.html/js/css) unchanged — same crop / arrow / rect /
ellipse / pen / text / mosaic-redact / undo / redo / copy / save.

Bundled but not shipped — leaving version bump + deploy to the parent
session.
2026-09-08 20:26:48 +02:00
Local Dev
22be28ed17 fix(theseus/aegis): retire bchwallet on every launch + branded dock/list icon
Two follow-ups from the on-device test.

Two Aegis addons showing up (bchwallet + aegis):
- migrateAegisRename previously only ran when addons/aegis/ didn't exist,
  which meant any bchwallet copy the signed OTA update endpoint reinstalls
  after the first migration stays there forever, and AddonHost loads both
  as separate wallets. Rewritten to always retire addons/bchwallet/ when
  it's present, regardless of whether aegis/ is already installed. The
  storage-copy (bchwallet.json → aegis.json) still only runs the first
  time so a downgrade doesn't clobber fresh 0.4+ state.
- Also flushes any stray addons/siawallet/ that comes back the same way.

Sidebar dock and Extensions list icons showed the raw 🛡 emoji:
- chrome.html's dock-button renderer and settings.html's extensions-list
  renderer now accept `data:image/svg+xml…` values for manifest.icon and
  render them as <img> instead of text. Emoji strings still render as
  before.
- aegis addon.json's icon is now the exact hex-aspis mark from
  aegis.x/brand/favicon.svg (URL-encoded inline). Version bumped to
  0.4.1 so seedBundledAddons reseeds the new addon.json on next launch.
2026-09-08 18:42:00 +02:00
Local Dev
2c7b82ad60 refactor(theseus/aegis): rename bundle bchwallet→aegis + retire standalone siawallet
Cleans up the naming that leaked from the wallet's origin story (BCH-only)
into the actual bundle layout. Aegis is one integrated addon now:
- Bundle folder: TheseusNavigator/bundled-addons/aegis/ (was bchwallet/).
- Addon id:      "aegis" (was "bchwallet"). Vault-derive still accepts
                 legacy "bchwallet/*" and "siawallet/*" paths via the
                 absorbs list, so no on-chain funds move.
- Version:       0.4.0 (bumped to trigger seedBundledAddons's reseed).
- Retired:       TheseusNavigator/bundled-addons/siawallet/. Sia is
                 folded into Aegis as a chain adapter (lib/sia/*.js
                 already in-tree) and Aegis's manifest lists siawallet
                 under absorbs so pre-Aegis SC keys derive identically.

main.js migrateAegisRename() runs before seedBundledAddons on every
launch. First run does the move; subsequent runs are no-ops:
- addons/bchwallet/  -> addons-backups/bchwallet-migrated-<stamp>/
- addons-data/bchwallet.json COPIED to addons-data/aegis.json (kept
  copied not moved so a downgrade to 0.3.x can still boot).
- addons/siawallet/  -> addons-backups/siawallet-migrated-<stamp>/
  (addons-data/siawallet.json left untouched — its walletdUrl is
  per-user config Aegis's Sia wallet takes fresh via Settings).

settings.html Aegis update card now matches either "aegis" (new id) or
"bchwallet" (pre-rename) so upgraders coming from 0.3.x see the same
one card while the OTA endpoint's next signed bundle catches up.

Internal purpose paths inside index.js/chain-*.js are unchanged —
LEGACY_BCH_PURPOSE stays "bchwallet/mainnet/0" and every purposePrefix
still starts with "bchwallet/*". The addon absorbs its own former id,
so those paths keep resolving to the same seed the shipping Aegis has
been using since 0.3.14.
2026-09-08 18:17:44 +02:00
Local Dev
2e54bf5e5a Ship Theseus 0.3.28 5d15508b (Aegis update card + DevTools in tab sidebar + real favicons)
Setup    5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643
Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4

Bundled since 0.3.27:

32d4b75 - Aegis (bchwallet) gains its own update card in Settings >
General beside Ariadne. Check for updates hits the same signed OTA
endpoint the boot timer uses; Restart to apply appears when a signed
newer version is staged. Uses the existing addons-check-updates + a
new app-restart IPC. New Aegis versions ship without a Theseus release.

32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to
the right of the tab (mode: 'right') instead of a detached window.
Matches stock Chrome. Users who prefer detached can drag out via the
DevTools own toolbar.

b71c925 - Search-engine favicons in Settings > Search now use Google's
/s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough
hosts (Brave, Bing, Yandex, etc.) that half the list was falling
through to the emoji placeholder.

Deployed. Verified LIVE 0.3.28.
2026-09-08 18:17:25 +02:00
Local Dev
ce53db3063 feat(theseus/aegis): official brand favicon + discoverable Add-wallet UX
Two fixes off the first-launch feedback: users didn't see how to add a
wallet, and the branded shield from aegis.x/brand hadn't landed in the
panel.

- panel.html + panel.js: swap the ad-hoc shield SVG for the exact mark
  from aegis.x/brand/favicon.svg — hexagonal aspis with dark fill +
  acid stroke + boss ring + centre point. The panel's tab favicon
  (<link rel="icon">) and the "Aegis" fallback badge in the header
  now render byte-close to what a user downloads from the brand kit.
- Add-wallet discoverability: an always-visible "+" chip lives in the
  header next to the picker caret; clicking it opens the picker with
  the coin list pre-expanded. When the panel is genuinely empty (a
  vault Aegis hasn't seen before), the gate now shows a big primary
  "+ Add your first wallet" button plus copy that spells out the
  seed source — Aegis derives every wallet from the Theseus password
  vault, no separate seed to import.
- addon.json bumped to 0.3.1 so seedBundledAddons() picks up the fresh
  panel files on the next Theseus launch (bundleVer === userVer would
  otherwise skip the reseed and users would keep loading the old
  panel from their addons/ dir).
2026-09-08 13:19:29 +02:00
Local Dev
2e42783dfe fix(theseus/screenshot): 0.2.4 — deliver capture via addon storage, not a cross-origin file://
Blank editor + broken buttons root cause: index.js was writing the
capture to <userData>/addons-data/screenshot-scratch/<name>.png and
passing "?src=file://<that path>" to editor.html. The editor lives at
file:///<userData>/addons/screenshot/editor.html — different directory
tree under file://. Chromium's file:// origin policy treats those as
different origins and quietly refuses the <img> load, so init()'s
loadImage() rejects, the canvas never gets an image, and every tool
after that operates on a still-empty 300×150 default canvas — the
tools appear to work but produce no visible output because the base
image never landed. The sidebar version we replaced set
`previewImg.src = dataUrl` (a base64 data URL) directly, which has no
origin and just worked; the tab version regressed by adding the file
hop.

Fix keeps the scratch file for the recent-captures ring but hands
the raw capture through the add-on's per-add-on kv store
(`__pending` key). Same store, same origin scoping, no
cross-directory read: index.js writes via api.storage.set from main;
editor.js reads via window.silentmode.storage.get through the tab
preload (packaged since 0.3.27). Fallback path retained for
"openRecent" callers still passing ?src=… — those will need their
own fix in a follow-up.

Bumped to 0.2.4 and signed for the OTA endpoint — first real
independent add-on ship: no Theseus release needed to fix this,
0.3.27 installs pick up 0.2.4 via the boot-time signed-update poll.
2026-09-08 12:57:46 +02:00
Local Dev
638aa4d326 feat(theseus/addons): CDP capture + editor Discard + manual update controls
Three tied-together fixes:

1) captureTab moves from WebContents.capturePage() to CDP
   Page.captureScreenshot for every mode (visible / full / region).
   Blank-screenshot symptom: after a toolbar-menu selection, the OS
   popup teardown left the tab view marked occluded for a few frames
   on some Windows setups, so capturePage() snapshotted a
   stale/transparent frame at the correct dimensions — no 0x0, no
   retry hit. CDP forces a fresh composite regardless of occlusion
   state (same path the "Full page" mode was already using) and
   returns a base64 PNG directly; PNG dimensions come out of the
   IHDR chunk (bytes 16-24). Attach only when nothing else has, and
   detach after only if WE attached, so an open DevTools stays
   attached.

2) Editor gets a Discard button. Toolbar picks up an "×" glyph next
   to Save/Copy that closes the editor tab and drops the working
   screenshot. Top-level Escape now falls through the same path
   after unwinding an in-flight text placement or crop rectangle. A
   new "addon-tab-close" IPC lets an add-on's own tab close itself
   (main matches the sender's webContents id against the tab list,
   so a page can only close its own tab); window.silentmode.closeTab()
   exposes it from addon-tab-preload.js.

3) Manual update controls in Settings > Extensions. New "Check for
   updates" button at the top of the Extensions surface calls the
   same signed-update polling the boot timer runs; the result is
   surfaced inline ("All extensions are up to date" / "N updates
   staged; restart Theseus to apply"). A "Pending updates" box
   below lists what's in <userData>/addons-updates-staged/ so the
   user knows what will be promoted on next restart.

Toolbar-menu popup settle bumped from 120 ms to 250 ms with an
explicit win.focus() in the popup close callback — the previous
window wasn't enough on slower Windows setups. CDP capture no longer
depends on this delay anyway, but the settle still helps any add-on
that does DOM work in its click handler before capture.

Screenshot add-on bumped 0.2.2 → 0.2.3 (Discard button; capture
fixes come from the host, not the add-on).
2026-09-08 02:27:36 +02:00
Local Dev
7806e3f31c fix(theseus/light): sweep hardcoded acid → var(--acid), Theseus button uses BCH dark
Two follow-ups on the light-mode acid work:

1) Every hardcoded #d6ff3d and rgba(214,255,61,X) in the browser
   chrome and every addon panel now goes through var(--acid), so the
   light-mode BCH-teal (#0AC18E) takes effect everywhere — not just
   where var(--acid) was already used. Hex-with-alpha (#d6ff3d55 etc.)
   converts to color-mix(); rgba() converts to rgb(from var(--acid)…)
   for the same alpha with the current --acid hue. Chromium 128+
   supports both. Files touched: chrome / settings / error / home /
   approval / bchwallet / siawallet / screenshot (html + css).
   Screenshot editor.js's #d6ff3d stays — that's the drawing colour
   swatch, not UI chrome.

2) The Theseus button (.logo) and update chip (.upchip) become dark
   BCH-navy chips (#253A49 background, #F8FDFF text) in light mode.
   Previously the .logo hardcoded #d6ff3d text on a bright-acid tint —
   invisible on a light toolbar. The dark chip stands out and gives
   the light theme a distinct accent using the BCH secondary from
   whybitcoincash.com's palette.
2026-09-08 01:06:40 +02:00
Local Dev
f8c58538d0 fix: use BCH-primary #0AC18E in light mode + strip Electron token from UA
Two related visibility fixes:

1) Light-mode --acid → #0AC18E (Bitcoin Cash brand primary, from
   whybitcoincash.com's palette per user). Direct swap from #088A66
   (darkened variant) to the on-brand primary. Applied across chrome /
   settings / error / home / approval / messages / bchwallet /
   siawallet / screenshot editor. Dark mode's #d6ff3d is unchanged.

2) User-Agent no longer includes 'theseus-navigator/<ver>' or
   'Electron/<ver>' tokens. Cloudflare's WAF was returning HTTP 503
   'Service Unavailable' to any request carrying those (verified
   directly against whybitcoincash.com — same URL, same headers, only
   the UA differed; plain Chrome UA got 200, Theseus UA got 503).
   Strip both tokens via a stockChromeUA() helper called from
   applyAcceptLanguage(), which whenReady already invokes at boot.
   Standard practice: Brave, Vivaldi, Slack all do the same.

Verified via CDP: navigator.userAgent now reports
  Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
  (KHTML, like Gecko) Chrome/130.0.6723.191 Safari/537.36
— indistinguishable from stock Chrome.
2026-09-08 01:04:27 +02:00
Local Dev
9d81c29656 fix(theseus/light): light-mode acid → BCH-teal #088A66 (brand-family, AA on white)
Prior light-mode --acid was #3a5c00 (dark olive-green) — legible but
off-brand. The Bitcoin Cash brand primary is #0AC18E (a teal-leaning
green already used in bchwallet's --bch variable). Darken it a step to
#088A66 for AA text contrast on white (~5:1) while staying in the BCH
family — the light-mode accent now reads as "Bitcoin Cash green,
darkened for legibility" instead of an arbitrary olive.

Applied across every chrome page + addon panel that carries the light
override (chrome / settings / error / home / approval / messages /
bchwallet / siawallet / screenshot editor). Dark mode's #d6ff3d
untouched.
2026-09-08 00:49:46 +02:00
Local Dev
6922ed72ff feat(theseus/screenshot): 0.2.2 — 3 extra swatches, updateURL points at live theseus.x endpoint
Bundled screenshot addon bump:
- version 0.2.1 → 0.2.2
- palette grows from 5 to 8 colors: adds Orange (#ff9500), Blue
  (#0a84ff), Purple (#bf5af2) alongside acid/red/yellow/white/black —
  common annotation colors that were conspicuously missing
- updateURL swings from the aspirational addons.silentmode.st (which
  never resolved) to the live gateway URL
  https://navigate.st/bns/theseus.x/extensions/screenshot/updates.json,
  where the operator's first signed update entry is now published

The gateway URL is deliberate over the bare `theseus.x/...` form: the
add-on updater runs from Node's main-process https module, which uses
the OS resolver. On installs without Ariadne's Thread the OS can't
resolve theseus.x (BNS-only TLD), so the poll would silently fail;
the navigate.st gateway resolves via standard DNS and forwards to the
same BNS-backed Sia content, so every install reaches the endpoint.

First signed update is live at:
  https://navigate.st/bns/theseus.x/extensions/screenshot/updates.json
  https://navigate.st/bns/theseus.x/extensions/screenshot/screenshot-0.2.2.tar.gz
signed 59a35370fdbc9d1e24834fa26c7765d27e8763fe928bfa23b202ca666a6a6973
by the ops key baked into 0.3.19. End-to-end verified via
scratchpad/decoupling-test/verify-live.mjs against the live endpoint:
fetch, sig-verify, download, sha-verify, extract, stage, promote,
backup — all pass.

Installs polling the previous updateURL (addons.silentmode.st) get
this new URL only after their bundled copy is refreshed to 0.2.2,
which means either a Theseus release with 0.2.2 bundled (0.3.20+) or
a signed update at the old URL that carries the URL change (impossible
because addons.silentmode.st doesn't resolve). Ship a Theseus release
that bundles this 0.2.2 to activate the update path on existing
installs; from then on the endpoint self-perpetuates via the theseus.x
URL.
2026-09-08 00:22:49 +02:00
Local Dev
b5f7552277 feat(theseus/aegis): SPL token support (view balances + send)
SPL tokens now show up in the Solana wallet — balances on the Receive
card, an asset picker on Send that flips the amount input into the
token's own units. Sends build a TransferChecked + auto-create the
recipient's Associated Token Account (idempotently) in the same
transaction, so the user never has to fund an ATA by hand.

- lib/sol-spl.js: SPL primitives that don't need @solana/web3.js.
  TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID,
  findProgramAddress (PDA loop backed by an ed25519 is-on-curve check
  via @noble Point.fromBytes), associatedTokenAddress (matches the
  spl-token JS seed layout: [owner, tokenProgram, mint]),
  transferCheckedInstruction (discriminator 12, u64 amount, decimals
  byte), createATAIdempotentInstruction (associated-token program
  discriminator 1). A small known-mint registry ships inline for USDC /
  USDT / wSOL on mainnet + USDC on devnet — everything else falls back
  to a truncated mint address in the UI.
- Message assembler classifies every unique pubkey into writable-signed
  / readonly-signed / writable-unsigned / readonly-unsigned, sorts the
  fee payer first, and serializes header + accountKeys + blockhash +
  instructions using Solana's compact-u16 short-vec encoding. Same
  wire shape @solana/web3.js produces from Transaction.serializeMessage.
- lib/chain-sol.js: snapshot() now carries a tokens[] array of
  {mint, symbol, name, decimals, balance, tokenAccount, tokenProgram,
  isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against
  both the classic Token program and Token-2022. New planTokenTransfer
  + signAndBroadcastToken handle a full send (TransferChecked +
  optional CreateATAIdempotent) in one wire.
- Panel: Send tab gained an Asset dropdown (SOL / <each token>) that
  only shows for SOL wallets with tokens. Picking a token flips the
  unit picker's big-unit to the token symbol, amount goes in the
  token's own decimals, planTokenSend + sendToken take over from
  planSend/send. Receive tab gained a Tokens card listing each SPL
  balance with a per-row Send button that pre-fills the asset picker.
- Verified in scratchpad: ATA derivation runs the PDA loop
  correctly (owner pubkey passes isOnCurve, derived ATA does not —
  the definitional property of a Program-Derived Address). Cross-check
  the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS
  and the value matches.

Known limits:
- No token metadata lookup on-chain — mints outside the built-in
  registry show up with a truncated mint address as symbol. Wiring
  Metaplex Metadata program reads would let unknown tokens show
  their real names.
- Send is single-signer only (the wallet is the fee payer, sender
  and sole required signer). Multi-sig SPL transfers work via the
  dapp bridge (window.solana.signAndSendTransaction, which already
  handles partial signatures).
2026-09-07 23:55:09 +02:00
Local Dev
1b29706ba4 feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain
Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp
that wants to route through Polygon (or Base, or Arbitrum, or any other
EVM the Silent Mode user hasn't added yet) calls the pair the industry
already wrote for it — Aegis registers the chain, provisions a wallet on
it under the same vault seed, auto-connects the origin, fires
chainChanged, and hands the dapp back a provider pointed at the new
chain. No sidebar detour, no Custom RPC copy-paste. Users still see
every chain in the picker post-add and can revoke sites in Settings.

- lib/chain-eth.js: EthWallet accepts a customNetwork override
  ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}).
  When present it replaces the NETWORKS lookup so mainnet+Sepolia
  ship built-in and every EIP-3085 chain is a runtime override the
  addon persists. The ticker flows into snapshot() so the send
  approval reads MATIC / BNB / whatever the chain's native currency is,
  not a hardcoded ETH.
- index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl,
  explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`.
  Persisted under api.storage.customEthChains, so an added chain
  survives Theseus restarts. chainMeta("eth", "custom-<chainId>")
  synthesizes the meta from storage so the panel renders custom
  chains without needing them in COINS at module-load time.
- eth.addChain handler (EIP-3085): approval overlay shows chain
  name, decimal + hex chain id, native ticker, RPC and explorer
  URLs (the phishing-signal quartet). On approval, persist config +
  create wallet with a custom-<chainId> network + auto-grant the
  origin readAddress on this chain. No-op success if the chain is
  already added.
- eth.switchChain rewritten to be EIP-3326 correct: look up any
  ready ETH wallet whose adapter reports the requested chainId,
  make it the selected wallet, fire chainChanged. When no wallet
  matches, throw with .code = 4902 (the standard 'chain not
  added' code) so wagmi / RainbowKit / any 3326-aware dapp does
  the fallback wallet_addEthereumChain call in the same click.
- eth.state handler: cheap {address, chainIdHex, networkVersion}
  peek for the origin's currently-connected wallet (no approval,
  no key access). The main-world bridge calls it after every
  switch/add to emit chainChanged + accountsChanged locally — the
  events MetaMask fires and RainbowKit listens for.
- wallet-inject.js: routes wallet_addEthereumChain via
  eth.addChain, preserves the 4902 code across the postMessage
  boundary on switch failures, calls pullEthStateAndEmit() to fire
  the post-switch/add events.
2026-09-07 22:26:27 +02:00
Local Dev
8fcc0e2433 feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send
Two follow-ups to the dapp bridges. Both change wire shape only — no new
UI, existing wallets keep signing byte-identically for the flows they
already covered.

- lib/eip712.js: full EIP-712 typed-data encoder — encodeType with
  alphabetically-sorted transitive sub-types, typeHash, encodeValue for
  string / address / bool / uint*/int* (any width) / bytes / bytesN /
  nested structs / dynamic and fixed arrays, hashStruct recursion,
  digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct).
  Verified against the spec §"Ether Mail" test vector — hashStruct on
  both the domain and the message plus the final digest all match the
  canonical values byte-for-byte (see scratchpad/verify-eip712.mjs).
- chain-eth.js: exposes signTypedDataDigest(digest32) that signs the
  precomputed digest with r||s||v (v = 27+recid), the same envelope
  personal_sign uses. Aegis computes the digest server-side (in the
  addon) so a bug in the encoder can't be tricked by a malicious dapp
  into signing over data the user never saw.
- index.js: eth.signTypedData handler shows domain (name · version ·
  chainId), primary type, and a truncated JSON preview of the message
  in the approval overlay — every classic phishing signal (mismatched
  domain, unexpected primary type) is in front of the user before they
  hit Sign. Accepts either an already-parsed typedData object or the
  JSON-string form older MetaMask specs used.
- wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same
  payload shape) route to eth.signTypedData. v1's flat "type[]" form
  is unwired — dapps that still use v1 should upgrade.
- Solana signAndSend: bridge now passes the FULL wire (from
  tx.serialize({requireAllSignatures:false, verifySignatures:false}))
  instead of just the message. The addon parses compact-u16 signature
  count, finds this wallet's pubkey in the message's account-key list,
  signs the message, and patches ONLY its own slot in the signature
  array — any partial signatures the dapp had already filled with
  tx.partialSign() (session keys, escrow co-signers, permissioned
  authorities) are preserved. Multi-signer flows work now; single-signer
  is the degenerate case of sigCount=1.
- Approval overlay for sol.signAndSend now shows required-signer count
  and the wallet's slot index so multi-signer requests are visibly
  distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
Local Dev
5880ba3507 feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet
Aegis now integrates with the two dapp-wallet APIs the wider ecosystem
actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style
window.solana for Solana — plus BTC signet as a third Bitcoin network
alongside mainnet + testnet3.

- wallet-inject.js: adds a main-world bridge, installed via a one-shot
  <script textContent=…> appended to <head> and immediately removed.
  Electron's contextBridge shallow-copies args and strips methods, which
  means BCH- and Tron-shaped params (plain data) work in the isolated
  world but Solana's wallet-adapter dapps — which pass @solana/web3.js
  Transaction objects and expect .serializeMessage()/.addSignature() to
  fire on them — need code that lives in the same world as the dapp.
  Bridge talks back to the isolated world via window.postMessage on a
  namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to
  theseus.invoke. Same pattern MetaMask + Phantom use.
- window.ethereum (EIP-1193): request({method, params}), on(),
  removeListener(), chainId, networkVersion, selectedAddress. Handles
  eth_requestAccounts, eth_accounts, eth_chainId, net_version,
  personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain
  (rejects with "use the Aegis picker"), wallet_addEthereumChain
  (rejects, chains come from Settings), wallet_get/requestPermissions.
  Every other eth_*/net_*/web3_* method passes through to the wallet's
  configured RPC via a new eth.rpc handler. EIP-6963 announceProvider
  event fires so wagmi / RainbowKit / any 6963-aware dapp discovers
  Aegis alongside MetaMask instead of racing for window.ethereum.
- window.solana (wallet-adapter shape): connect(), disconnect(),
  publicKey (with toString/toBase58/toBytes/equals — the PublicKey
  interface dapps check), signMessage(u8) → {publicKey, signature: u8},
  signTransaction(tx) → mutates + returns the same tx with the
  signature added, signAndSendTransaction(tx) → returns {signature: txid},
  signAllTransactions([tx]), request({method, params}). isPhantom flag
  set true so dapps that gate on it pick us. on/off events for connect
  / disconnect / accountChanged.
- Handlers in index.js registerPageMessages: eth.requestAccounts,
  eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc,
  sol.connect, sol.signMessage, sol.signAndSend. Every write path is
  per-origin gated + goes through api.approvalModal with the wallet
  label + network in the row list so the user always knows which
  Aegis wallet is about to sign.
- Signet added to chain-btc.js — signet shares testnet3's address
  format and SLIP-44 coin type (BIP-325 only changed consensus/signing),
  so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only
  the electrum pool (aranguren + wakiyamap) + explorer (mempool.space
  /signet) + faucet (signetfaucet.com) differ. Registered as
  btc:signet in COINS with per-network coinType lookup.

Known limits (follow-ups in the same shape as existing chains):
- SOL signAndSendTransaction is single-signer only; dapps that combine
  the wallet's sig with co-signer sigs need the wire assembled on the
  dapp side.
- ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set
  covers personal_sign only.
2026-09-07 22:08:56 +02:00
Local Dev
cffb956a4c feat(theseus/addons): signed add-on update endpoint, à la Firefox XPI
Decouples bundled-add-on updates from Theseus releases. An add-on
whose addon.json declares an updateURL can be republished at any time
without shipping a new Theseus installer; existing installs pick it up
on the next boot's +30 s background check.

Client flow (main-process only, no UI touchpoints in this commit):

    initAddons()
    ├── promoteStagedUpdates()   # promote signed stage if newer
    ├── seedBundledAddons()      # bundle wins over on-disk if newer
    └── AddonHost.discoverAndActivate()
    30 s later:
    └── checkAndStageUpdates()   # fetch, verify, download, stage

Signature: Ed25519 over
"silentmode.addon-update-v1|<id>|<version>|<tarball-sha256>",
verified against a hardcoded set of operator pubkeys living in
addon-update-pubkeys.js. Domain-separated so the operator key can't
be tricked into signing a message with a different purpose. Empty
pubkey array is the shipping default — checkAndStageUpdates() then
short-circuits and no outbound requests are made, which is the safe
posture until the operator ceremonies a key in.

Payload: gzipped tar, extracted with the system tar (present on
Win10 1803+, macOS, Linux). Path traversal defended by tar's default
refusal of `..` entries; the extracted manifest's id + version are
re-checked against the signed values before staging.

Staged updates go to <userData>/addons-updates-staged/<id>-<version>/.
Promotion into <userData>/addons/<id>/ reuses seedBundledAddons's
backup dance: existing folder moves to
<userData>/addons-backups/<id>-<oldver>-<timestamp>/ so any local
edits survive.

New files:
- addon-updater.js — client
- addon-update-pubkeys.js — hardcoded pubkeys (empty; edit + rebuild to rotate)
- scripts/generate-update-keypair.mjs — one-time keygen
- scripts/sign-addon-update.mjs — operator packager+signer
- docs/ADDON-UPDATES.md — operator brief + threat model

Wired into main.js at boot; screenshot add-on's addon.json advertises
the reference updateURL for when the endpoint goes live.
2026-09-07 21:58:30 +02:00