Each chip was a fixed 150 px, so the newest ones ran out of the row
and out of sight. Chips now take the full width while there is room
and shrink together as more are saved, down to an icon and a few
letters, before the row overflows.
A bundled add-on that answers cookie consent dialogs, rejecting all but
the essentials by default (or accepting, if the user prefers the banner
simply gone), so pages open without one. Built on DuckDuckGo's
autoconsent (MPL-2.0): its rule bundle covers hundreds of consent
managers, and a reject-button heuristic handles unknown banners in
reject mode. The library runs through the page-inject slot in every
http(s) frame's isolated world; the add-on hands each frame the user's
settings and the rules, and counts what was handled per site for the
panel, which also excludes a site with one click.
build-inject.js assembles inject.js from the library in node_modules
plus the glue, and copies the compact rules and licence into the add-on
so a rule update can ship through the add-on channel.
Host side: page-inject scripts get theseus.evalInPage for the few rules
that need the page's own JavaScript (they already reach the page via
contextBridge, so no new trust tier), and api.tabs is open to
page-inject add-ons as well as request-filter ones.
Theseus had no content blocking at all. Shield blocks requests to known
tracking and advertising hosts on every site, using EasyList and
EasyPrivacy through Ghostery's adblocker engine (the matcher those lists
are written for). The lists ship inside the add-on so blocking works
from the first launch, offline; the compiled engine is cached under the
add-on's data dir (a 22 ms load instead of a 500 ms parse), and the
lists refresh from their publishers about once a day.
The panel shows what was stopped on the current page, a one-click
allow for the site, the global switch, the running total and the rule
versions with an "Update now". Network filters only for now: a blocked
request never leaves the browser, but leftover empty ad boxes are not
hidden yet.
Host side: a "request-filter" capability. Chromium allows one
onBeforeRequest listener per session, so main owns it and consults the
add-ons' filters; a top-level navigation is never blocked, only http(s)
subresources are offered. api.tabs (active tab and a change event) lets
the panel show per-site numbers without seeing page content.
Three complaints, one cause between the first two.
A line of a PDF is rarely one run. pdf.js splits it wherever the file does
— a font change, a kerning adjustment, a colour change — so a heading can
be three spans and an invoice line ten. Replacing the span under the
cursor covered a fragment and left the rest of the line standing, which is
exactly what a replacement that looks like a copy laid over the original
is. A run is now the whole visual line: the spans that share its baseline
and sit close enough to be spacing rather than a second column, with the
spaces the geometry implies put back between them.
And that line is edited on the page. The dialog that used to hold a copy
of the words is gone: the cover goes down first, carrying the line's own
words at the page's own size and colour, and the caret opens on it. The
cover keeps its words hidden while you type, so the original never shows
through the thing covering it. Escape with nothing changed lifts the cover
again and leaves the page as it was found — no mark, no undo step.
The rotate grip was a square like the resize handles, wearing the open
hand that means drag-the-page. It is a disc with a turning arrow now, and
a cursor drawn to match, since no standard cursor means turn. The inline
style that was defeating the stylesheet is gone with it.
Ctrl and the wheel zoom, about the pointer rather than the top-left, so
the words you were reading stay where they were. A plain wheel still
scrolls.
Records what the gateway routes do, the four things that still have to
happen on the servers before the dropdown can light up (UUID pool per
inbound, the pool file, X-Forwarded-For on the vhost, deploy-from-repo),
and — deliberately at the same prominence — that expiry is bookkeeping
rather than enforcement until xray's handler API is wired per box.
Also flags the per-client byte cap as an unverified cheaper mitigation,
marked as needing a test rather than written up as though it works.
Drops the meridian ellipse so the globe is just a round field, and
spends the space that frees on the glyphs: tiles grow from 11.6 to 13.4
units and the A from font-size 10 to 12.4, with the 文 strokes
thickened to match. Checked the same way as last time — rasterized at
true 16px and 18px and magnified nearest-neighbour — against three
candidates (plain globe with tiles, letters bare on the globe, and a
single tile). Tiles won: bare glyphs on blue lose too much edge
contrast at 16px, and the two-tile arrangement is what reads as two
scripts rather than one word.
Same mark in the manifest and in the panel header.
A band starts from the space between things, so that a drag across words
still selects words to copy. On a page that is wall-to-wall text there is
no such space, and the gesture simply did nothing — which is how it failed
on a real document at 276% zoom, where every candidate starting point had
a line of text under it.
Alt-drag starts a band anywhere, text or not. The select hint says so.
Select, Text, Highlight, Draw, Sign — labelled, in that order, at the head
of the toolbar. Each one leads a kit rather than hiding it: the shapes sit
behind Draw, underline and strike-through behind Highlight, and nothing is
offered twice.
Draw arms the pen, which is what Draw means when there is one button for
it. The names drop out below 1180px, where five of them would start
pushing the zoom and colour controls off the end.
The sheet now follows the notification-card pattern: a tone icon in a
tinted circle, the message in bold under a "who says" caption, a
tinted primary action and a quiet Cancel, close in the corner. The tone
is read from the message — delete/remove/error reads as destructive
(red, and the confirm button says Yes), unsaved/required/leave as a
warning, saved/completed as success, anything else as plain info —
since a page hands over only a sentence.
Also decide "who says" from the sender's current URL rather than the
tab's prov, which lags a navigation: a tab that had just left the home
page for a site was still labelled Theseus.
Auto-translate: changing either language only called saveUi(), so the
pane below kept showing the previous language's result with nothing to
say it was stale — you had to notice and press Translate. Both selects
now re-run the translation, guarded on empty input and on
source === target (which would only echo the input back).
Text size: the panes were 13.5px, small for reading a paragraph in a
language you don't know well, which is the entire job. Base is now
15px, driven by a --tsize custom property so both panes stay matched.
Zoom: −/+ either side of a percentage in the header, 70–220% in steps
of 10, clamped with the buttons disabling at each end. Click the
percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either
pane. Persists per-machine alongside the language choice.
Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that
was indistinguishable from every other globe in the dock. Drawn against
the 16px and 18px rasterizations rather than at a comfortable size —
the first pass used a font glyph for 文 and it turned to grey mush at
16px, so the strokes are hand-drawn paths thick enough to survive. Also
drops the hardcoded icon in registerSidebarPanel, which would otherwise
shadow the manifest's mark.
Verified in a harness with a stubbed host API: language change fires
exactly one request, the two guards fire none, zoom clamps and persists,
and the layout holds at sidebar width.
alert / confirm / prompt from a page came up as bare OS message boxes
titled "theseus-navigator" (the package name), with no hint of who was
asking and nothing of the browser's styling — the PDF Editor's "Delete
signature?" was the reported case.
The session preload replaces the page's three functions with wrappers
that hand the call to the isolated world through a DOM event, which
asks main synchronously and writes the answer back; pages see Chrome's
return values (confirm → boolean, prompt → string or null) and no new
global. Main answers from a sheet hanging under the toolbar, in the
same surface as add-on approvals, that names who is asking: the site's
host, the add-on's name for an add-on page or panel (identified by its
path under the profile's extensions directory), or Theseus for its own
pages. The sheet belongs to the tab that asked — hidden while another
tab is in front, back when its tab returns — and a closing tab or
window answers "cancel" so no renderer stays blocked. Windows without
the chrome (installed apps, plain windows) get a native box with a
proper title, and app.name now reads "Theseus Navigator" for whatever
else still shows one.
A wallet imported from a single private key cannot do WizardConnect, and no
amount of work inside Aegis changes that: the handshake ships BIP32 xpubs so
the dapp derives addresses without further round-trips, and a lone key has no
chain code to build one from. Manufacturing a parent whose child equals a
given key means inverting HMAC-SHA512, and even solved for index 0 the dapp's
next index lands elsewhere. The way out is to stop being a single-key wallet.
Promote derives a fresh wallet from the vault on the import's own network and
sweeps the key into it, after which WizardConnect works — and so does every
other thing that assumes a key tree. It reuses plan() + signAndBroadcast(),
the same pair the consolidate flow already spends through, rather than
growing a second money path.
Three things it deliberately does not do:
- The preview costs the sweep by planning a send-max to the wallet's OWN
address, so cancelling leaves nothing behind. Same inputs, same single
P2PKH output, so the fee is identical to the real sweep.
- The imported key is kept, not deleted. The sweep is unconfirmed when the
call returns and anyone holding the old address can still pay into it;
removing the key there would strand those coins. Removal stays a separate
step the user takes once the balance reads zero.
- A promote that fails in plan() takes the just-created wallet back out,
since nothing was broadcast. Past that point the wallet is kept even on
error, because a transaction may already be on the wire and its
destination has to stay visible.
BCH only: the BTC/DGB/ETH/TRX/SOL imported adapters still throw "read-only"
from plan(), and the refusal now names the chain instead of failing vaguely.
Wallet creation is lifted out of the addWallet handler into
createVaultWallet so promote builds its destination through exactly the same
purpose allocation, legacy-purpose carry-over and id numbering as the Add
flow, instead of a near-copy sitting next to a transfer.
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
Drag on empty page and a rubber band gathers every mark it sweeps over.
Touching counts rather than enclosing: a band you have to draw right
around a long arrow is a band you draw twice. Shift-click adds or removes
one, Ctrl+A takes the page.
What the group can then do is move, restyle, duplicate and delete, each as
a single undo step — six marks deleted is one thing the user did, so it
has to be one thing to undo. The selection bar offers only what is true of
every member: a group of shapes gets Fill, a group of stamps gets size and
weight, a mixed group gets neither, and none of them gets Edit, which
needs one mark to put a caret in.
No resize handles on a group. Stretching a mixed selection means deciding
what a stretch does to text, which scales by font size, and to a line,
which has no box at all; until there is an answer worth defending,
offering the grip would promise something this cannot keep.
Each member is outlined as well as the group, because a band that caught
one more mark than you meant is worth seeing before you press Delete.
A group lives on one page. A mark carries its page, and PDF user space
means nothing across two of them, so a band selects within the page it
was drawn on.
One trap found on the way: starting the band on a press means calling
preventDefault, which is also what gives a form field its caret — so the
band now keeps its hands off the annotation layer, and typing into a PDF
form still works.
Three things the editor made you work around.
Text was typed into a dialog and then placed, so you chose a size and a
weight for words you could not see against the page they were going on.
The click now opens a caret where you clicked, in the font, size and
colour the words will have, with the style bar over it; double-clicking a
stamp reopens it in place. Lining a CSS line box up with a PDF baseline is
measured from the font's own metrics, not guessed.
A signature lived in a single slot. There was nowhere to keep initials as
well as a name, nowhere to change the one you had, and reaching for the
tool again simply stamped the first one — which is the same fault three
times: one slot. It is a library now, with redraw, rename and delete, and
the choice is made when the tool is picked up, so placing stays one click.
An existing single signature is carried into it rather than dropped.
The mark you had just drawn could be resized by its handles and not moved
by its middle, because only the select tool let marks be hit-tested at
all. The SELECTED mark now takes a press whatever tool is armed. A press
anywhere else still draws, and an unfilled shape is still grabbed by its
outline — the same rule select has always followed.
Also: words default to dark ink rather than highlighter yellow, which was
unreadable on white and is now impossible to miss, since you watch
yourself type it.
0.3.56 was cut from the Aegis line (WizardConnect auto-detection, Aegis
0.8.x, PDF Editor and VPN updates) on top of 0.3.55; 0.3.57 carries that
plus the install-as-app feature and the two main-process crash fixes.
Tabs keep emitting events while the window is torn down: a hovered
link fires update-target-url, which positioned the link-status pill
against win.getContentBounds() on a destroyed window ("Object has been
destroyed", 2026-09-27). With installed web apps the browser window can
now close while their windows keep the process alive, so this stops
being a quit-time blip and becomes a normal state.
The overlay helpers and layout() now check the window is alive, the
session is captured on close (the quit-time save no longer overwrites it
with an empty list once the tabs are gone), and "closed" drops every
reference to the window's views. A later createWindow() starts from a
clean tab list, so a page opened from an app window after the browser
window was closed brings the window back with the restored session.
Eight decimals of tail sat exactly where the eye lands. The hero now shows
two decimals once there is a whole part, because the whole units already
carry the significant digits, and digs past leading zeros otherwise so
amounts under 1 keep roughly four significant figures:
1204.23234145 -> 1,204.23 0.23234145 -> 0.2323
0.00012345 -> 0.0001234 0.00000001 -> 0.00000001
A flat "two decimal places" rule is the obvious version of this and it is
wrong: it renders 0.00042 BCH as 0.00 and the wallet reads as empty. The
rule here never collapses a non-zero balance to zeros, and it truncates
rather than rounds, so the figure shown is never more than the wallet holds
and sending the displayed amount always clears. An ellipsis marks the cut,
hover gives the exact figure, clicking pins full precision and is
remembered. Only this hero abbreviates — Send, MAX, history and the wallet
strip still call fmtBig, so every number acted on is exact.
Also repairs the regexes in the same function, which lost their backslashes
when the earlier commit was scripted: /^(-?)(\d+)(\.\d+)?$/ had become
/^(-?)(d+)(.d+)?$/, matching the letter "d". Still valid JS, so it parsed
cleanly and every balance quietly fell through to unformatted text — the
grouping and the dimmed decimals introduced in bb639a9 had never once run.
Caught by testing the function body extracted from the file rather than a
transcription of it.
Node's ws aborts the handshake when close() is called on a CONNECTING
socket and emits an error on the next tick; with no listener that is an
uncaught exception, and Electron answers with the modal "A JavaScript
error occurred in the main process". nostr-tools drops its onerror
handler right before closing, which is what the WizardConnect relay
teardown in Aegis runs on every wallet disconnect while a relay is
still connecting. Browser WebSockets ignore the same sequence, which is
why the library gets away with it elsewhere.
Every consumer in the main process shares the one ws module, so
close() now adds a no-op error listener to a connecting socket before
aborting it. Anything else that still escapes to the top of the process
is logged to <userData>/main-errors.log instead of raising the modal;
Electron continued after that dialog anyway, so only the interruption
goes.
Electron ships Chromium's renderer without the browser-side web-app
install machinery, so beforeinstallprompt never fires and every site's
own "Install our app" chip (coin-spectrum.com's, for one) stays hidden
in Theseus. The browser side now exists:
- webapps.js reads a page's <link rel="manifest">, accepts it when it
names an app with a standalone-style display mode and a start_url on
the page's origin, and records the descriptor on the tab.
- The address bar shows an install chip for such pages (filled once the
app is installed: click then opens or removes it); the page context
menu carries the same entry.
- Pages get a synthetic beforeinstallprompt whose prompt() routes to the
Theseus install dialog and resolves userChoice like Chrome, and an
appinstalled event afterwards, so sites' own chips appear and work.
- Installing stores the app under <userData>/webapps/, wraps the
manifest icon into an .ico, writes a Start Menu (optionally desktop)
shortcut that launches Theseus with --app=<start_url>, and opens the
app in a chromeless window with its own taskbar identity. The window
shares the session, BCNR resolution, fingerprint and add-on bridges
with tabs; popups and "open in Theseus" go to the browser window,
Alt+arrows / F5 / Ctrl+R cover navigation without a toolbar.
- Theseus takes the single-instance lock so a shortcut launch lands in
the running browser (second-instance) instead of a second profile
owner; launched cold, --app= opens only the app window and a later
plain launch brings the browser window back.
The network chip sat on its own row holding a single chip and ~300px of dead
space, while the connection state and the per-wallet actions sat on a second
row underneath the balance. Two thin rows sandwiching the number people open
the panel for pushed it into the middle of the header. They are now one row
above it: [network] [state] ......... [Explorer] [Faucet].
Neither half belongs in Settings. The status line is not a hostname label —
it is also where "connecting…", "· syncing", "· <n> unconfirmed" and the
walletd setup prompt are written, so filing it away would hide an unconfirmed
balance and an instruction. Explorer and Faucet are verbs on the selected
wallet rather than configuration, which is why 0.9.2 lifted them out of the
Receive card in the first place.
Only the hostname may shrink. Left to itself the flexbox took width off the
network chip first and clipped "Chipnet testnet" to "Chipnet" — the one label
on that row that must never be ambiguous, since it says whether the next
action moves real money.
First pass of the wallet-app restyle. The panel led with a 22px balance that
carried no more weight than the labels around it, so the one number people
open the sidebar to read had to be hunted for. It is now the hero: 32px,
thousands-grouped, with the fractional part dimmed so magnitude reads before
precision. The ticker and the fiat conversion sit on its baseline as
annotations rather than peers, and wrap to their own line intact once the
amount outgrows a narrow panel.
The Receive/Send/History/Settings bar used an underline for the active tab,
which read as browser chrome and all but vanished at sidebar widths — a 2px
rule under a short label is easy to miss. It is a segmented pill bar now,
active section filled in the accent. The hover tint derives from --ink so it
shows in the light theme too, where a white overlay was invisible.
No charts yet: there is no price history behind Coin-Spectrum to draw, so
nothing here fakes a trend line.
Every chipnet wallet in the picker read "can't pair" with the reason
nowhere on screen: the explanatory note only rendered when NOTHING could
pair, so one working mainnet wallet hid it entirely. The cause now rides
on the row itself ("can't pair (WIF import)") and the note appears
whenever any wallet is blocked. A single private key has no chain code,
so there is no xpub for the handshake to send — the text now says that
and points at the two ways out.
Seed-imported wallets stored the full leaf path (m/44'/1'/0'/0/0) in
accountPath, because that is what derived the one address the strip
shows. WizardConnect was handed that as the BIP44 *account* node and
would derive m/44'/1'/0'/0/0/<branch>/<i>: a tree the user holds no keys
in. Pairing looked healthy and every sign request failed with "no path
for input". Same class as the 0.9.7 chipnet mismatch, one level down.
A dapp drops its pairing code from the DOM once connected, so the
commonest empty scan is a page that is already paired. Sending that user
to "open the Connect dialog" points at a dialog the dapp will not show
again; the message now names the existing pairing instead.
syncHeight queued its measurement in requestAnimationFrame, and frames
stop whenever the window is occluded — so a prompt closing, or a row
hiding, while another window covered Theseus left the chrome at its
old height until the window came back. A zero timeout coalesces the
same way and always runs. The open-prompt allowance now lives in
syncHeight itself, so any re-measure while a prompt is up keeps its
room, and the growth helper simply defers to it.
The chrome view is exactly as tall as its rows, and a fixed-position
prompt mask adds nothing to the height it reports, so any prompt taller
than the toolbar — the new two-field favorites form lost its title and
buttons — was clipped at the view's edge. The view now grows to fit the
open prompt and shrinks back when it closes.
The favorites row's right-click menu could only save the current page.
"Add a page…" opens a two-field form: the address (scheme optional,
must be a real host — Chromium's parser happily percent-encodes junk
like "not a url" into a bogus bookmark, so hosts are checked) and a
title that fills itself from the address, brand-cased like the chips,
until you edit it. Duplicates are refused; the site's /favicon.ico is
used for the chip.
Each .warow was its own grid container, so the `auto` amount column
resolved independently per row: a row holding 0.3066756 got a narrower
column than one holding 0.43789841, and the copy button therefore landed
at a different x on every line. Column widths have to be SHARED to line
up, and nothing was sharing them.
The columns are now declared once on a .walist wrapper and every row
inherits them via `grid-template-columns: subgrid`. Column gaps moved to
the parent, since a subgrid takes its gutters in the subgridded axis from
the grid it inherits and would have ignored them on the row. A
@supports fallback pins the amount column to a fixed 104px for any host
without subgrid, which keeps the copy column straight there too.
Measured across six rows with four different amount lengths, at 260,
320, 400 and 520px wide: copy left edge, amount right edge, ticker left
edge and menu right edge all have 0px spread, with no row overflow and
no clipped amount.
0.9.8 claimed this area was verified, but the check only looked for cell
overlap WITHIN each row and never compared the same column ACROSS rows —
which is exactly the defect it missed.
Electron hands every page an empty window.chrome. Real Chrome's carries
app, csi, loadTimes and runtime, and bot checks — Google's sign-in
botguard among them — look for exactly those to tell Chrome from an
embedded Chromium. The per-tab identity script now fills the object
with the same shapes and return types; a site learns nothing it would
not also learn from stock Chrome.
The main-world bridge was pushed into every https page as a text script.
Sites that enforce Trusted Types refuse that and report the attempt to
their CSP endpoint — Google's sign-in pages among them, which then have
every reason to call the browser insecure. No dapp lives on those
origins: a static list of the big enforcing sites is skipped outright,
any other origin that rejects the bridge once is remembered and skipped
from then on, and where Trusted Types exist unenforced a policy keeps
the assignment clean.
The row's grid template still described the pre-0.9.2 layout — six
columns including an address cell that no longer exists — while the row
renders five. Every cell therefore sat one column left of where it
belonged: copy landed in the label's space, the amount in the old label
column, and the ⋯ in the amount column instead of the edge. That is the
whole cause of "the copy button collides with the amount" and "the last
edit button is not on the edge".
Now five columns for five cells: icon · label · copy · amount · menu,
8px gaps. The label is the flexible one, left-aligned, so it takes the
slack and truncates rather than squeezing the number. Verified at 520,
400 and 300px: no cell overlap at any width, no row overflow, and the
amount never clips — only long labels give way.
The copy glyph was the 📋 emoji, which has no glyph in this platform's
font stack and rendered as a tofu box that read like a stray character
stuck to the balance. Replaced with an inline SVG in both the row and
the address card. Both confirmation flashes now swap innerHTML rather
than textContent, which would have deleted the SVG and left a blank
square.
Address card: the address and its copy button share one row, so the
button sits at the end of the value it copies. The address clamps to two
lines and truncates beyond that instead of growing the card in a narrow
sidebar.
The QR is always visible and the panel is drag-resizable from a grip
under it (pointer events, arrow keys as a non-mouse path, clamped
90–420px, capped against the panel's own width so a size set on a wide
sidebar cannot overflow a narrow one, persisted). That replaces the
0.9.2 show/hide toggle — a size set once beats a binary, and it frees
the row Copy was sharing with a QR button.
drawQr was setting cv.style.width/height, which would have reset the
panel to its intrinsic size on every redraw — i.e. every time the
address changed. It now sets only the backing store and re-asserts the
user's size after drawing.
"Next unused address" is correct at the adapter level on both mainnet
and chipnet (tested: the index advances and the address changes), so the
reported failure is elsewhere. The handler was discarding the error and
flashing a bare "Failed", which is why there was nothing to diagnose; it
now surfaces the real message.
A chipnet BCH wallet derives from m/44'/1'/0', but the WizardConnect
registration fell back to a hardcoded m/44'/145'/0' whenever the entry
had no explicit accountPath — which is the normal case for a wallet
created through the UI. Mainnet's default happens to be that same
literal, so only chipnet was affected.
The consequence was worse than a failed pairing. Pairing SUCCEEDED, the
handshake carried xpubs for an unrelated key tree, and the dapp then
derived addresses this wallet does not own:
wallet's real chipnet address : bchtest:qpezx8qkwpjd4e6pd5aang0ve6fctpjvg5ckp2lwu7
address from the WC xpub : bchtest:qzvpe3w9rqnszk6mntnef6zv6v94zmfvpc49qkxml4
So the dapp saw an empty stranger's wallet, and anything it built spent
inputs the wallet could not match — signing would fail with "no path for
input". Silent, and only reachable on testnet.
Both registration sites (vault-derived and imported) now take the path
from adapter.snapshot().accountPath, which is by construction the tree
the wallet actually derives its addresses from.
Two wrong turns worth recording. defaultAccountPathFor() takes the coin
CONFIG object, not a chain string, so passing entry.chain returned null
and would have stopped WizardConnect registering at all — strictly worse
than the bug being fixed. chainMeta().defaultAccountPath was no better:
BCH has no coinType in COINS, so it is null for every BCH network. The
adapter is the only component that resolves this correctly, which is why
it is now the source.
The Custom box validated for a vless:// prefix and rejected anything
else, so a provider's subscription URL — the thing most people are
handed — got "paste a vless:// URL first" with no hint that the
Subscription card two sections down was what it wanted. Now an
http(s):// paste in that box is detected and routed to the subscription
importer, the placeholder says both are accepted, and the dropdown
option reads "Custom — vless:// or subscription URL".
Also renames the three bundled entries' status from "coming-soon" to
"awaiting-key-issuer". The exits exist and are running xray; what is
missing is a way to hand a client credentials without shipping a shared
secret. DESIGN.md now records why that list stays empty, since this is
the second time the shortcut looked attractive: a vless:// URL is the
credential, so writing one into the tarball (immutable, mirrored) or
onto a public Sia object (mutable but world-readable) are the same
category of mistake. Per-session minting is the fix, because then no
shared credential exists to leak.
Parser verified against plain-text, standard-base64 and url-safe
unpadded-base64 subscription bodies; an HTML error page correctly
yields zero entries instead of a JSON parse crash.
pws=0 (personalisation off) is the parameter rank-tracking scrapers put on
every query, and Google weighs it when deciding to serve the "unusual
traffic" check. With cookies cleared on quit the personalisation it turned
off was already minimal, so the flag bought nothing and made every search
from Theseus look like a bot's. hl and gl stay: they keep Google from
answering with a consent redirect or a region-detect start page.
"+ Add another BCH" created a fresh wallet on the spot. The old comment
argued that being inside a coin's address list made the intent
unambiguous; it isn't. "Add another BCH wallet" is just as often "bring
in the one I already have somewhere else", and guessing wrong is not
harmless — the user gets an empty new address and has to work out for
themselves why their funds aren't in it.
Adds aegisChoose(), a pick-one sibling of aegisConfirm for branches
where the honest answer is a question rather than a yes/no, and puts it
in front of every path that reached addWallet:
- + Add another <TICKER> in the coin drilldown
- + Add on an unowned coin in the browse picker (now routes to the
existing New / Import / Connect chooser, with the coin still
preselected through whichever branch is taken)
- + Add your first wallet on the empty state — the most important one,
since someone arriving with an existing seed was being handed a
create-only flow
The empty-state copy claimed "there's no separate seed to import",
which stopped being true when imports shipped and actively told users
the feature they wanted did not exist.
Verified in a rendered panel: the chooser appears, addWallet is not
called until Create is picked, Import opens the import modal, and
Cancel does nothing.
Pairing already worked; signing would have thrown on the first request
a dapp ever sent. Found by testing against the real relay and the real
@wizardconnect/wallet library rather than reading the code.
Two bugs in wc-sign.js, both fatal:
- The WC message nests the whole WcSignTransactionRequest under
`.transaction`, so the tx is at request.transaction.transaction and
the spent outputs at request.transaction.sourceOutputs. We read
request.transaction as the tx and request.sourceOutputs as the
outputs, so tx.inputs was undefined. index.js already read the nested
request.transaction.userPrompt for the approval dialog, so only the
signer had it wrong. The flat shape is still accepted.
- generateSigningSerializationBCH takes TWO positional arguments,
(compilationContext, {coveredBytecode, signingSerializationType}).
We passed one merged object, leaving coveredBytecode undefined and
throwing inside libauth. For P2PKH the covered bytecode is the spent
output's locking script.
Now verified end to end: a two-input transaction spending from two
different derivation paths signs, decodes, and passes
createVirtualMachineBCH().verify() — consensus-valid, with
SIGHASH_ALL|FORKID|UTXOS (0x61) on every input as the protocol
requires.
Also: RelayStatus is an object ({status: "connected" | "reconnecting" |
"disconnected" | "session_deleted"}), and the snapshot read a
non-existent `.kind`, so every connection reported the literal
"[object Object]". Reads `.status` now, uses the documented
getConnections() accessor instead of the private connections Map, and
carries the library's own `label` ("dapp name once known, otherwise
Connecting…"). The panel shows a tag for anything other than connected
— "reconnecting" is the difference between a pairing that will see the
next signature and one that is dead, which was invisible before.
0.9.3 merged ✎ and 🗑 into one ⋯ in the coin drilldown but left the
coins summary row with the old pair, so the two views disagreed about
the same idea. Both now carry a single ⋯ opening the manage modal.
Found by rendering the panel against stubbed host state over HTTP
rather than reading the diff — the file:// preview never executes
panel.js, so earlier checks could only confirm the markup existed, not
that it drew correctly.
Drops the now-unreachable removeWalletWithConfirm helper, the
data-wremove handler and the .wactdel style that went with them.
Address row is now icon · label · amount · one button. ✎ and 🗑 were two
controls for what is really one idea ("change this wallet"), and the
manage modal already holds rename, derivation path AND remove — so a
single ⋯ opens it. That also stops Remove sitting one stray click away
from Rename. Default/legacy wallets show a lock instead.
The per-address amount is back unconditionally: 0.9.2 hid it when a coin
had one address, but the row reads as a breakdown and the gap looked
like missing data. The duplicate that actually mattered — the drilldown
subtitle — stays gone.
The Assets card only ever had branches for SOL, BCH and TRX, so ETH fell
through to a hidden card despite the adapter returning tokens in the
same shape. The generic branch is now keyed on the DATA rather than a
list of chain ids: any chain whose snapshot carries `tokens` renders,
which means ETH works today and a chain added later works for free. Only
the label ("TRC20" / "ERC20" / "Tokens") varies by chain.
Dropped the "Pick a coin" title row from the coin picker — the search
field's own placeholder already says what the pane is, so the title was
a line of chrome restating it and pushing the list down. Search and
close now share the top row.
Token history stays in History despite the request coming from tokens
being mistaken for transactions: a wallet that had only ever moved USDT
still showed an empty history without it.
Receive was ordered QR → address → tokens, so 200px of always-on QR sat
above the thing people came for and pushed the asset list off screen.
- Address first, with Copy and a QR button; the QR expands inline and
the choice sticks, because someone who receives by QR wants it every
time and someone who copies never does.
- Explorer and Faucet moved up to the header status row. They act on the
selected wallet, not on the act of receiving, and down there they
competed with Copy for the one row that gets used.
- Assets card renamed from Tokens and now leads with the native coin, so
"what does this wallet hold" is one list rather than two places.
- "+ Add another <TICKER>" moved below the address list.
The balance appeared three times — header, drilldown subtitle, address
row. Now once in the header; the subtitle keeps only the per-unit price,
and the per-address amount returns when a coin actually has more than one
address to compare. The address row drops its truncated address (the full
one is at the top of Receive) and keeps the wallet name.
The per-address asset list added in 0.8.8 duplicated the Assets card and
is removed — assets live in one place.
Token amounts were unreadable: an 18-decimal balance rendered as
60000000.000000005435817984. Capped to 8 decimals with thousands
separators, exact value on hover.
A symbol claimed by more than one contract now carries a LOOK-ALIKE tag.
The test wallet holds four different contracts all calling themselves
"Test USDT" — spam mints borrowing a trusted ticker so a careless send
lands on the wrong one. We can't tell which is genuine, so we mark every
member of the clash rather than guessing.
History showed native transfers only, so a wallet that had only ever
moved USDT looked empty. TRC20 transfers are merged in newest-first, each
carrying its own decimals and ticker (rendering a token against the
chain's scale would be off by orders of magnitude). Both feeds are on by
default; the checkboxes narrow rather than opt in, and the last one
checked can't be unchecked into an empty list.
Clicking the dock raised a native file browser, which was the right answer
while the editor had exactly one thing to offer an empty tab. It is the
wrong answer now: a file browser can only ask which PDF, and the answer is
sometimes none of them.
So the dock opens the editor, and the empty editor says what it can do.
The drop zone stays, and learns to read what it is given — pictures become
pages, several PDFs become one document. Beside it sit the three ways in
as buttons.
Not included: compress, which cannot be done honestly without re-encoding
the images, and split, which is the page rail plus Save a copy.
A document built from pictures or joins has never been on disk, so it is
marked unsaved from the moment it opens — otherwise closing the tab would
bin it without asking. An empty editor also stops claiming to hold a file
called document.pdf.
Mounting an imported TRX/ETH/SOL wallet read the optional custom RPC as
String(stored || undefined), so a wallet with no override got the literal
"undefined" as its server: every history and token fetch went to
"undefined/v1/accounts/…" and the panel showed "undefined" under the
balance. Only a real https URL overrides the network default now, and the
adapter itself rejects anything that does not look like one.
Completes the three detection paths. The injected provider shipped in
0.8.8; these two needed host support, because nothing in the add-on API
could reach the active tab's content (captureTab is pixels, not DOM).
wiz:// links (main.js)
A click on a wiz:// anchor is intercepted in will-navigate and in the
window-open handler (target="_blank" lands there instead), and routed
to the wallet with the offering page's origin attached, so the
approval names the real site. The tab never navigates. This needs
nothing from the dapp beyond rendering the URI as a link, so it works
for third-party dapps that will never adopt a Silent Mode API.
scan-page capability (addons-host.js + main.js)
New capability backing api.scanActiveTabForUris({scheme, limit}).
Deliberately NOT a "read the page" API: the host runs the match and
returns only the URIs found, so an add-on holding this still cannot
see page text, markup or form values. It sits well below page-inject
on the trust ladder — it learns that a page offers a wiz:// code and
nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the
result count is capped.
The matcher also accepts WizardConnect's QR-alphanumeric spelling
(WIZ://%3FP%3D…), which is frequently the only form present when a
dapp renders its pairing code as a QR, and decodes it. Verified
against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and
QR-decoded alike.
Regex sources are built host-side and passed as JSON rather than
assembled inside the injected string — hand-escaping backslashes and
quotes through two levels of literal was both wrong on the first
attempt and unreviewable.
Aegis
Declares scan-page, adds the wcScanPage handler and a "Scan page"
button next to Connect. A scan fills the URI field and stops there
rather than pairing outright: the user still chooses which wallet
signs and still presses Connect, because a scan that silently paired
would carry far more consequence than the button implies. Older hosts
without the capability get a clear "update Theseus" message instead of
a dead button.
Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL
wallets showed a native balance and nothing else, because the JSON-RPC
endpoints they poll have no history or token concept at all.
- ETH history + ERC-20 balances via Blockscout, which needs no API key
(Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was
answering 525 with an HTML error page — that parsed as a JSON error and
showed as a 0 balance.
- SOL history via getSignaturesForAddress and SPL balances via
getTokenAccountsByOwner, both keyless on the public RPC.
Three things the live testing turned up:
- A Blockscout mempool entry is {result:"pending", status:null}. Reading
that as "not ok, therefore failed" showed pending sends as failures.
Now carries a distinct pending state through to the row.
- History `delta` is now a number, a decimal string, or null. ETH wei
needs the string (18 decimals overflows a JS number, and Math.abs was
silently rounding it); Solana's signature feed carries no amount at
all, and null >= 0 is true, so unknown amounts were rendering as a
"+" that claimed a receive we cannot verify. Unknown now renders as a
neutral row instead.
- A real address came back with 855 ERC-20s and 3078 SPL mints, nearly
all airdrop spam, some with blank, zero-width or bidi-override
symbols that render as an empty row borrowing trust from its
neighbours. Token text is sanitised and lists are capped at 50, sorted
so named tokens survive the cap.
Also: the first-run setup screen forced text-align:left on the form, so
its helper copy ran ragged under a centred mark, title and description.
The form now inherits the centred alignment; the mnemonic box stays
left-aligned on purpose, since centring wrapped seed words makes them
harder to check.