Commit graph

209 commits

Author SHA1 Message Date
Local Dev
83b65498a2 translate 0.1.3 → 0.1.4: bigger letters, plain globe
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.
2026-09-27 18:54:34 +02:00
Local Dev
333f7988a8 fix(pdf-editor): a page of solid text had nowhere to start an area selection
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.
2026-09-27 18:34:07 +02:00
Local Dev
cdad2907cb feat(pdf-editor): the five things you open a PDF to do, named and in front
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.
2026-09-27 18:21:55 +02:00
Local Dev
13a4dd692d translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change
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.
2026-09-27 18:00:46 +02:00
Local Dev
e0246f8520 feat(aegis): 0.11.0 — promote a WIF import to an HD wallet
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.
2026-09-27 16:19:24 +02:00
Local Dev
ce066bb4ca feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
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.
2026-09-27 15:09:17 +02:00
Local Dev
62460cd3aa feat(pdf-editor): select an area, and treat what it caught as one thing
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.
2026-09-27 13:45:04 +02:00
Local Dev
10c83a1431 feat(pdf-editor): type on the page, keep more than one signature, move what you drew
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.
2026-09-27 13:16:07 +02:00
Local Dev
9730b246a1 Merge release/0.3.56 into release/0.3.57
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.
2026-09-27 11:25:19 +02:00
Local Dev
18f2839bd4 feat(aegis): 0.10.0 — shorten the balance by default, full precision on demand
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.
2026-09-27 10:17:15 +02:00
Local Dev
570e9e6213 feat(aegis): 0.10.0 — one status row above the balance instead of two around it
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.
2026-09-27 01:09:49 +02:00
Local Dev
546b8f5767 feat(aegis): 0.10.0 — lead with the balance, and make the section bar read as an app
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.
2026-09-27 00:47:57 +02:00
Local Dev
464d83316b fix(aegis): 0.9.10 — say why a wallet can't pair, and stop imports advertising the wrong key tree
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.
2026-09-27 00:01:39 +02:00
Local Dev
e761500a2f fix(aegis): 0.9.9 — address columns share one grid so they line up
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.
2026-09-24 23:05:08 +02:00
Local Dev
4754fb948b fix(aegis): 0.9.9 — keep the dapp bridge off Trusted-Types sites
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.
2026-09-24 22:50:36 +02:00
Local Dev
4c7d8e3f56 fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy
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.
2026-09-24 22:30:10 +02:00
Local Dev
36012e7c26 fix(aegis): 0.9.7 — chipnet WizardConnect advertised the wrong key tree
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.
2026-09-24 04:56:12 +02:00
Local Dev
cee120cb23 vpn 0.1.3 → 0.1.4: an https:// paste imports as a subscription
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.
2026-09-23 23:21:51 +02:00
Local Dev
f07df2b39e fix(aegis): 0.9.6 — adding a wallet asks create-or-import
"+ 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.
2026-09-23 08:33:29 +02:00
Local Dev
7b9049f6fc fix(aegis): 0.9.5 — WizardConnect signing actually works
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.
2026-09-23 03:23:41 +02:00
Local Dev
e9335f5e6b chore(aegis): 0.9.4 — merge the coins-row buttons too
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.
2026-09-23 03:05:22 +02:00
Local Dev
4956f8f6c1 chore(aegis): 0.9.3 — one row shape, assets for every coin
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.
2026-09-23 02:49:05 +02:00
Local Dev
1be352dad6 chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history
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.
2026-09-23 01:17:33 +02:00
Local Dev
acc433805e feat(pdf-editor): the dock opens the editor, and the editor offers more than one way in
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.
2026-09-23 00:51:45 +02:00
Local Dev
9d9c651b30 Merge branch 'claude/sleepy-maxwell-251ee6-b' into HEAD
# Conflicts:
#	TheseusNavigator/addons-host.js
#	TheseusNavigator/main.js
2026-09-23 00:25:36 +02:00
Local Dev
7a71b08cf7 fix(aegis): 0.9.1 — an unset custom RPC no longer becomes the text "undefined"
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.
2026-09-23 00:24:42 +02:00
Local Dev
8174fccba0 feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan
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.
2026-09-23 00:16:49 +02:00
Local Dev
64bc26ecf6 chore(aegis): 0.8.9 — ETH/SOL imported history + tokens, centred setup screen
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.
2026-09-23 00:05:14 +02:00
Local Dev
903831c741 release(pdf-editor): 0.3.1
Ships the fix from 04b38cb: a run lifted out of the document is text, so
the selection bar now offers Edit, size, bold and italic on it, and the
size picker carries the document's own size rather than rounding an 11 pt
line up to 12 on the way out.
2026-09-22 23:49:20 +02:00
Local Dev
c4dbdb25fb fix(pdf-editor): a replaced run is text, so let it be edited like text
Testing 0.3.0 on a real install showed the gap immediately: select a run you
had replaced and the selection bar offered duplicate and delete and nothing
else. The one mark made entirely of words was the one with no way to change
them, double-clicking it did nothing, and the size stepper and the bold and
italic buttons all stayed hidden. Everything else that holds text could be
reopened; this could not.

The cause was three places testing `kind === "text"` where the question was
really "does this mark hold words". A replacement holds words.

Reusing the dialog exposed a second, quieter fault. A run lifted out of a
document is whatever size the document set — 11 pt, 9.5 pt — while the size
picker lists round numbers. Selecting a value the list does not contain leaves
the select empty, and the size on the way out fell back to 12. Editing the
wording of an 11 pt line would silently have resized it. The dialog now adds
the document's own size as an option for as long as it is open, and falls back
to the size it started with rather than to a guess.
2026-09-22 23:09:58 +02:00
Local Dev
1b74b10fc5 chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page
Wallet strip:
- Clicking a coin opens that coin's page (addresses, price, totals, back
  and close) instead of only flipping the selection and leaving the list
  sitting there. The page already existed but was reachable only via the
  small count chip.
- The per-coin second action was a gear that selected the wallet and
  opened the global Settings tab — the same destination for every coin,
  so it read as a per-coin control that wasn't one. It is now Remove,
  behind a confirm, with the default/legacy wallet showing a lock
  instead since it gates legacy funds.
- Each address in the drilldown can expand to show what THAT address
  holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via
  tokenBalances. walletSummary now carries both per wallet, so the view
  no longer has to borrow the selected wallet's assets.

WizardConnect — the Connect pane was effectively unusable:
- The locked-vault branch told the user to unlock and gave them nothing
  to click. It is reachable without the lock screen ever appearing,
  because a mounted imported wallet makes overallPhase read "ready".
  It now carries the same unlock form the lock screen uses.
- Imported BCH wallets were never registered with the WC manager —
  startForWallet ran only in the vault-derived mount branch. They mount
  as ready, so they appeared in the "Sign with" picker and then failed
  on pair. They now register from their stored seed. WC derives a child
  key tree, so single-key (WIF) imports genuinely cannot pair; those are
  disabled in the picker with the reason, rather than failing on click.
- Adds window.wizardconnect so a dapp can hand over the wiz:// URI it
  already generated instead of making the user copy it between tabs.
  The protocol is Nostr-relay pairing designed for phone-scans-QR, and
  the SDK has no in-page discovery at all, so this is our own surface:
  connect() + isReady(), plus a wizardconnect:announceProvider event
  shaped like EIP-6963 so several WC wallets can coexist. Pairing always
  goes through the approval modal; the URI is validated before any UI
  shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00
Local Dev
c4f7251860 feat(pdf-editor): convert a PDF to Word, and say what that costs
A PDF does not contain paragraphs. It contains glyphs with coordinates, and
there is no heading, no list, no table and no guaranteed reading order —
only runs of characters that happen to sit near each other. Converting to
Word means working out where the paragraphs were, from geometry. That
inference is the whole feature, and it is sometimes wrong, so this is called
a conversion and never an edit, and the dialog reports what it found before
anything is written.

Lines are grouped by baseline, runs joined with the spaces a PDF only implies
by leaving a gap, and paragraphs ended where the next line sits unusually far
below, is indented, or where the previous one stopped short of the measure.
Headings come from size relative to the body — which is the most common size
on the page, not the average, because a page of 11 pt under a 28 pt title
averages to something that is neither. Bold and italic come from the font's
name, the only place a PDF records them.

What it refuses to fake is as important. A page set in columns is reported,
not silently interleaved. A page with no text says so, and says why: it is an
image of writing, and reading that needs character recognition this editor
does not have. Tables become plain paragraphs rather than an invented grid,
because a wrong table is harder to repair than no table.

The .docx is written here rather than by a vendored builder: a Word file is a
zip of five XML parts, and the subset that can honestly be produced —
paragraphs of styled runs — is about two hundred lines. Vendoring a document
library would have added another megabyte on top of the four pdf.js and
pdf-lib already weigh, to generate markup we would still have to get right.
Entries are stored rather than deflated, which keeps a compressor out of the
add-on; the CRCs are the part that cannot be skipped, since Word calls the
file corrupt rather than naming the part that upset it.

Text replaced in place converts as replaced. Converting would otherwise hand
back the words the user had just edited away.

Checked by taking the output apart — every CRC verified, both XML parts run
through a real parser — and then, because that is still marking my own
homework, by opening the result in the Word editor extension, where mammoth
reads it with none of my code involved.
2026-09-22 22:06:54 +02:00
Local Dev
8785ecc7cf chore(aegis): 0.8.7 — imported Tron wallets get history + TRC20 tokens
Imported Tron wallets pointed at api.nileex.io, which only serves the
/wallet/* JSON-RPC family and 404s all of /v1/. That REST family is
where transaction history and the trc20 balance map live, so an
imported Nile wallet showed a native balance and nothing else. The
built-in Tron adapter was already on nile.trongrid.io, which is why
only imports were affected.

Switches the imported Nile endpoint to nile.trongrid.io and fills in
the two features that were never implemented for imported account-
model wallets:

- History via /v1/accounts/<addr>/transactions, with the signed delta
  computed by comparing owner_address against the wallet's own address
  in 41-hex form (the feed returns hex regardless of visible:true).
- TRC20 balances via /v1/accounts/<addr>, joined against token_info
  harvested from recent trc20 transfers to recover symbol + decimals.

Both are best-effort so a chain with no keyless feed can't blank a
wallet whose balance fetch succeeded. Contracts with no registry entry
render as "Unknown token" with a raw amount rather than a number
invented from assumed decimals, and named tokens sort above them so
airdrop spam can't bury real holdings.
2026-09-22 21:52:54 +02:00
Local Dev
d625bd25a1 feat(pdf-editor): replace the document's own text, on its own baseline
Until now "editing" a PDF here meant laying things over it. You could put a
word on top of a word, but the document underneath never changed, and the
result read like a sticker because it was one. This adds the thing the word
Edit actually promises: click a line of the document's text, type different
words, and they land where the old ones were, in the old size and the old
colour.

The position and size come from pdf.js's text layer, which has already placed
a span over every run and carries that run's size in unscaled PDF points — so
the size is right whatever the zoom, which reading it off the rendered box
would not be. The colours come from the rendered page, because nothing in the
text API reports them: the background is the average of the most common colour
bucket in the run's box, since type is a minority of the pixels even when it
is dense, and the ink is whatever sits furthest from that background. On the
test fixture it recovers the marker's red exactly.

Two things that look like details and are not. The bucket only chooses WHICH
pixels are background; the colour itself is their average, because rebuilding
it from the bucket index rounds white down to #f8f8f8 and a not-quite-white
patch on a white page is a visible seam. And the cover reaches below the
baseline by a quarter of the font size, because pdf.js sizes its spans to the
em box: cut the cover to the span and every descender in the original line
survives as a little hook under the replacement.

A replacement is a cover plus text, so it is a mark like any other — movable,
resizable, undoable, and rendered on screen from the same numbers the writer
uses, which is what makes the preview trustworthy.

Said plainly in the dialog and again in the save summary: this hides the
original, it does not remove it. The old glyphs are still in the content
stream underneath. Redact is the tool that takes text away, and it says so
too.
2026-09-22 21:49:37 +02:00
Local Dev
30fbe73d21 chore(aegis): 0.8.6 — in-panel confirm/alert instead of native dialogs
window.confirm/alert render as chrome-owned, Theseus-branded OS boxes
outside the sidebar, which breaks the illusion that Aegis is one
coherent surface — and they can't carry an icon, a danger-styled
button, or formatted copy.

Adds aegisConfirm() / aegisAlert(): the same overlay shell the manage
and import modals already use, resolving like confirm() so callers
just await it. Escape cancels, Enter confirms, click-outside cancels.

Swapped at all four confirm sites (remove wallet from the picker,
remove wallet from Settings, remove PIN, sign out) and all seven
alert sites.
2026-09-22 21:44:03 +02:00
Local Dev
05ac67067a fix(pdf-editor): a shape can be resized the moment it is drawn
Drawing a rectangle left it selected with its handles showing, and then
refused to let you touch them. The handles were only live under the select
tool, so sizing the shape you were still looking at meant a trip to the
toolbar and back — for a gesture the editor had already drawn the grips for.

Handles are now grabbable whatever tool is armed. They cannot be confused
with drawing: a handle is a nine-pixel square that only exists while
something is selected, nobody lands on one by accident, and Escape drops the
selection if the space is wanted back for drawing. The original gate was
protecting against a collision that does not really happen, at the cost of
one that does.

Making them universal opened a trap in the release path, fixed here too: the
text-markup tools return early from onUp to commit a text selection, which
would have stranded a resize half-done — applied to the live mark, never
journalled, with the drag still set so the next press behaved oddly. A
handle drag is now finished first, whatever tool is armed.
2026-09-22 21:33:48 +02:00
Local Dev
f3fb31e651 chore(aegis): 0.8.5 — Coin-Spectrum price parser reads body.asset.price_usd
Every Coin-Spectrum poll silently returned an empty map because we
were reading body.price_usd (top-level) while the API wraps its data
under body.asset. Every chain's Number(undefined) came back NaN, so
Settings › Prices showed "✓ 0 coins" and majority-vote reconciliation
had one fewer source than intended.

Now reads body.asset.price_usd (with a top-level fallback in case the
API is ever flattened).
2026-09-22 21:31:43 +02:00
Local Dev
e03c728116 chore(aegis): 0.8.4 — network chip on its own row + coin-catalogue search
Two follow-ups on 0.8.0's currency picker after a testing pass:

- The network label was a tiny gray suffix next to the wallet name in
  the header, so "which network am I on" wasn't obvious at a glance.
  Now it's a separate row of one clickable chip under the coin ticker;
  clicking jumps into the browse:chain view where the network strip
  actually switches network.

- Clicking the header opened a "Pick a coin" pane that only listed
  coins the user already had a wallet for — a first-time user with a
  single BCH wallet would see one row and no way to add more. Now it
  shows every supported coin behind a search box; owned coins jump to
  the wallet list, unowned coins jump straight into the create-wallet
  flow with that coin's network group pre-expanded.
2026-09-22 21:19:36 +02:00
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
b32b353db7 chore(aegis): 0.8.3 — default BCH explorer switches to bchexplorer.cash
Blockchair's BCH explorer is slow and ad-heavy; bchexplorer.cash is
the Bitcoin Cash community's own instance, faster on tx pages and
with a proper mempool view. Same /tx/ + /address/ path scheme
(address takes the bitcoincash: prefix as-is), so no other code has
to change.

Chipnet explorer stays on chipnet.imaginary.cash — bchexplorer.cash
is mainnet-only.
2026-09-22 20:44:44 +02:00
Local Dev
cb7ca53b01 chore(aegis): 0.8.2 — sectioned Settings tab
Settings grew tall enough that the user had to scroll past a dozen
cards to reach Prices, Sites or About. Split it into six named
sections (Security, Session, Wallet, Prices, Sites, About) with a
chip nav row at the top; only one section is visible at a time and
the choice persists across restarts.

Adds an About card that names the wallet, the aegis.x front-door
site and the silentmode.st umbrella, so support triage has a
one-click way to reach either from within the panel.

Includes the accumulated 0.7.x-0.8.1 wallet work that was already
shipping on OTA (CashTokens/BCMR, imported-wallet spend, siascan
integration, consolidate, currency picker, footer update chip).
2026-09-22 20:37:28 +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