Commit graph

266 commits

Author SHA1 Message Date
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
53965db74d feat(addons): let an add-on talk to its own full-tab pages
api.emit() only ever reached the sidebar, even though addon-tab-preload has
always exposed silentmode.on() — so an add-on could hear from its panel but
had no way to tell its own editor tab anything. An add-on that wants a
second document to land in the editor already open, instead of a third tab
full of ribbon, needs exactly that.

Two small pieces:

- emitToPanel now also delivers to every tab owned by the same add-on. The
  sidebar keeps its existing condition; tabs are additional, not instead.
- addon-tab-focus, the counterpart of addon-tab-close: a page asking for its
  own tab to be fronted. Needed because the click that hands an open page
  something new usually happens somewhere else — the sidebar — and the
  result would otherwise appear in a tab nobody is looking at.

Both derive the tab from the sender's webContents, the way the close handler
already does, so a page can only front or close the tab it is itself in.
2026-09-22 08:34:25 +02:00
Local Dev
aee4b44d15 feat(theseus): extension dock — drag to reorder, right-click menu
The extension buttons sat in registration order with no way to change
it, hide one, or switch an add-on off without opening Settings. Buttons
are now draggable (drop side follows the pointer, same feedback as tabs
and bookmark chips) and the order is persisted per profile. Right-click
opens a native menu: open/close the panel, move left/right, hide from
the toolbar, turn the add-on off, and Manage extensions; the dock's own
right-click offers Show hidden. Main owns the prefs (dockOrder,
dockHidden) and the actions, so the chrome only renders. Add-on
rediscovery now pushes a fresh dock state to the chrome, which it never
did before — turning an add-on off or installing one from a page
updates the toolbar at once.
2026-09-22 08:30:40 +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
ed98d0280c chore(theseus): 0.3.52 — one-click install from theseus.x, plug-in row with Aegis and Tor 2026-09-22 00:51:29 +02:00
Local Dev
fbde2f7650 feat(theseus): Tor toggle joins the plug-in row
The Tor chip was the widest control in the toolbar and competed with
the address bar for space. It now sits on the favorites row beside the
Aegis chip, under the Theseus menu, at the same chip height — the
network-level switch next to the wallet, both always visible. State
tints (off / connecting / on) are unchanged.
2026-09-22 00:50:05 +02:00
Local Dev
3d9b05ba25 docs: scope the signing allow-list per extension, not per app
Multiple signing keys already work — PUBKEYS_HEX is an array and
verifySignature accepts any entry that verifies, which is an allow-list
already. What it isn't is scoped: every add-on is checked against the same
array, so a key added for one extension can sign an update for any other.
The canonical message binds a signature to an extension; nothing binds an
extension to a signer.

The publisher path doesn't have that problem, because a name answers "who
may sign this" per extension and rotates on chain without an app release.
So the note now argues for generalising that rather than replacing it:
per-extension key bindings, several acceptable keys each, the chain
preferred over a compiled-in array, and co-signing considered only for the
default set — the extensions that reach every fresh profile without anyone
choosing them.

Records the open questions honestly too: where a binding can live without
being restatable by whoever controls the manifest, and that refusing to
update an extension is a much smaller decision than refusing to run it.
2026-09-22 00:47:00 +02:00
Local Dev
ed2d2e1d5a feat(theseus): plug-in dock under the Theseus menu
Aegis sat among the extension buttons and drifted as add-ons were
installed or removed. First-class Silent Mode components (manifest
category "plugin") now get a labelled chip at the right end of the
favorites row, directly below the Theseus menu — always visible, never
collapsed into the extensions overflow. The add-on host tags each
sidebar panel and toolbar menu with the category so the chrome can
split the two docks; the extensions dock keeps its behaviour.
2026-09-22 00:31:18 +02:00
Local Dev
0da15818ad feat(docx-editor): the blue DOC badge, kept as the generated source
Picks the mark from 95c93f9 over the one I drew: at the 16–18px the dock
actually renders, a solid fill reads and an outlined page does not, and
pairing with pdf-editor's red badge makes the two editors obviously a set.

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

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

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

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

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

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

Binary manifest ships with PENDING sha256s until the binaries are
uploaded to Sia — ensureBinary refuses to activate on a platform whose
sha256 is PENDING, so a user cannot flip it on against an unverified
download.
2026-09-21 23:39:32 +02:00
Local Dev
8d02a2464b theseus 0.3.51: profile relocation looks for the actual old dir name
0.3.50's relocateProfile checked for %APPDATA%\Theseus Navigator\, but
Electron's userData path is derived from app.getName(), which reads
package.json's top-level "name" ("theseus-navigator") because there is
no top-level productName — the "productName": "Theseus Navigator" in
this file lives under "build", where electron-builder reads it for the
installer, not where Electron reads it for the runtime path. So the
folder the user's Theseus writes to is %APPDATA%\theseus-navigator\,
never %APPDATA%\Theseus Navigator\.

On 0.3.50 that meant relocateProfile found nothing at its search path,
returned the new Theseus\ location, and Electron happily created a
fresh empty profile there. The user's addons, vault, bookmarks and
settings stayed in theseus-navigator\ but the running Theseus was no
longer looking at them. Losing the vault is not something the user
can recover from.

Check both candidate names — the one the code was written for and the
one that actually exists — and migrate whichever is present. If the
new Theseus\ already exists (Windows fresh installs after 0.3.51), we
leave it alone.
2026-09-21 22:52:57 +02:00
Local Dev
56b3f2f206 feat(docx-editor): ship it in the build, updated over the first-party channel
Moving it out of the build left it with no way in. Settings can only install
from the community catalogue, so a first-party extension that isn't bundled
has a working update channel and no first copy for anyone to update — the
mechanism was all there and the front door was missing.

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

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

The end-to-end driver goes back to checking that a fresh profile seeds it,
which is the property that actually matters now.
2026-09-21 22:47:13 +02:00
Local Dev
59593033a1 docs(theseus): one extension directory, not bundled versus community
Extensions read as two kinds today, and the division is not in the engine:
they already share one directory under the profile, one discovery pass and one
enable/disable switch. The split is imposed in four places, and this writes
down where and what each costs to remove.

The load-bearing one is that removal cannot stick. Seeding copies back any
pre-installed extension whose folder is missing, so deleting one is undone on
the next launch — which is why the UI marks them unremovable, and why there is
no uninstall action for anything at all. A tombstone list fixes both.

The urgent one is that the two publishing channels have one install path
between them. Everything installs through the community catalogue; the
operator channel has no index and no install route. So the Install buttons on
the shipped extensions' cards do not work, and moving an extension off the
community channel makes it uninstallable rather than better distributed.

Also records what not to flatten: one catalogue and one lifecycle, but the
signer stays visible, because an extension signed by the operator key and one
signed by a stranger's wallet get the same access once installed.
2026-09-21 22:46:11 +02:00
Local Dev
4af11d28fb chore(docx-editor): first-party update channel, not the community catalogue
This is our own extension, so it belongs on the channel the operator key
signs — theseus.x/extensions/docx-editor/ — alongside screenshot, aegis and
pdf-editor. The community catalogue is for extensions published by whoever
owns a BNS name, and routing ours through it would have meant asking a name
owner to vouch for code we wrote.

Drops the community packing script: with one channel there is one publish
path, and it is the one already written down in docs/ADDON-UPDATES.md.

Worth stating plainly, because it is currently true and awkward: Settings
can only *install* from the community catalogue. An extension that is
neither bundled nor catalogued has a working update channel and no way for
anyone to get the first copy. Either it goes back into the build or it needs
a first-party entry point.
2026-09-21 22:28:30 +02:00
Local Dev
74bf9d4e50 theseus: bump to 0.3.50 for the extensions-list fixes ship
Cuts the shipping trigger for the changes accumulated since 0.3.49:
translate is bundled (so a fresh install has it out of the box, and
seedBundledAddons reseeds the folder on any install that lost it),
Settings > Extensions no longer double-renders installed extensions
in a separate Community section, and the PDF/DOC data-URI icons the
dock now paints as <img>s ship alongside their addons.
2026-09-21 22:20:31 +02:00
Local Dev
13a6e3ca1c feat(docx-editor): Save as…, PDF export, and a mark of our own
Three gaps, one theme: the editor could produce a file but not decide where
it went, what format it was in, or look like anything in the dock.

**Save as…** opens a real file dialog, and the extension typed there picks
the format. Save then writes to that file instead of dropping another copy
in Downloads every time. The renderer never names a path: the dialog returns
an opaque token, and the add-on will only write to a path a dialog actually
returned. An extension page is the least trusted thing in the add-on, and
"write these bytes anywhere" is not a capability it needs.

**PDF** goes through Chromium's own print pipeline in a hidden window — the
same engine as Ctrl+P — on the paper size read out of the document's own
sectPr. For that to match what the user was looking at, the page's
typography had to stop living in editor.css, which the export window can't
reach: it moves to lib/doc-css.js and both surfaces read the one string. The
result embeds subsetted fonts, keeps images, and turns hyperlinks into real
PDF link annotations.

**The icon** is ours. Microsoft's Word mark is a trademark and borrowing it
to look official is not something a browser that talks about sovereignty
should do. icon.svg says "text document" in its own words — a turned corner,
a heading rule, body lines, a pilcrow badge in Silent Mode green — and
`npm run icons` derives the PNGs and addon.json's copy from it, so there is
one drawing rather than several that drift.

Also: the scratch folder follows the profile rename to extensions-data/ via
the api.dataDir the host now provides, instead of creating a stale
addons-data/ beside it.
2026-09-21 03:35:55 +02:00
Local Dev
2b8e3a7eb6 translate 0.1.1 → 0.1.2: fetch from the addon's Node side, not the panel
The panel was doing the HTTP call itself, which meant any mirror sitting
behind a Cloudflare-style anti-bot check returned "<!doctype html>…" for
a POST from Origin: file:// and JSON.parse choked on it. Moving the
fetch into index.js gets rid of the whole class of browser-context
interceptors (CORS preflights, captive portals, anti-bot pages) and
lets the addon look at the response body before trying to parse it —
an HTML body is now reported cleanly as "server returned an HTML page
instead of JSON".

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

Verified end-to-end from Node against both a working URL and a dead
one; the dead one falls through to disroot as expected.
2026-09-21 03:34:14 +02:00
Local Dev
9fbba50999 theseus: drop Community section from Settings > Extensions
Discovery of new community extensions belongs on theseus.x/extensions,
which has room to describe each one and lists them by publisher. Settings
just shows what's installed on this machine — one card per extension,
whether bundled or community. The Install button on the web page invokes
the same addons-install-community IPC, and Theseus refreshes the
installed list after each install, so the two lists were mirroring the
same state with a docx-editor card appearing on both sides.

Removes the Community h2, its list container, the Refresh catalog
button, loadCommunity() and its wiring — kept only the trust warning as
a note directly under the installed list.
2026-09-21 03:28:04 +02:00
Local Dev
0684906f13 feat(pdf-editor): a mark you placed is something you can still work on
Everything the editor put on a page was final. A text stamp could not be
corrected without deleting it and typing it again, nothing could be resized,
and the only way to remove a mark was a Delete key nobody had been told
about — the selection drew a dashed box and offered no action at all. Placing
a stamp also left its tool armed, so the next click stamped a second copy.

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

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

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

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

Double-click is detected from the pointer stream rather than from a dblclick
listener, because selecting a mark calls preventDefault() on the pointerdown
and that suppresses the compatibility mouse events the browser would have
synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
Local Dev
cb7ffafcce theseus + site: fix duplicate extension cards + real-icon rendering
- Settings > Extensions: the Community list showed an entry even for
  extensions already listed above under Installed, so docx-editor
  appeared twice as soon as it landed in the gateway catalog. The
  render now filters out any catalog item whose id is installed —
  the Installed list handles updates via the poll + "Check for
  updates" button, so the Community list only needs to surface
  things you don't have. Empty-state copy branches on whether the
  catalog is empty vs "everything is installed".
- theseus.x/extensions catalog renderer accepts data:image/... icons
  in addition to emoji, so an extension whose manifest.icon is an
  SVG data URI (pdf-editor, docx-editor) renders as an <img> instead
  of the raw URI text.
- Static PDF Editor entry on that page now uses the same inline SVG
  badge the browser dock renders.
2026-09-21 02:43:10 +02:00
Local Dev
862e92543c extensions: real PDF/DOC icons + fix translate default URL
- pdf-editor 0.1.1 → 0.1.2: manifest.icon is a data:image/svg+xml red PDF
  document badge (dock renders it as <img>). Same on toolbar-menu.icon.
  Item-level icons dropped — those go through the native OS menu that
  doesn't render data URIs.
- translate 0.1.0 → 0.1.1: default LibreTranslate mirror was
  translate.argosopentech.com, which is now a dead domain — panel just
  said "Failed" on every request. Switched default to
  translate.disroot.org (currently up), added a datalist of known
  mirrors, and a one-shot migration off the dead default so existing
  installs recover on next load.
- docx-editor 0.1.0 → 0.1.1: manifest.icon is a data:image/svg+xml blue
  DOC document badge, index.js no longer overrides it with 📝, and the
  panel header uses the same inline SVG. Same rationale as pdf-editor —
  every extension was rendering as either 📄 or 📝, so PDF and Word
  were visually identical to the Notepad.
2026-09-21 02:41:16 +02:00
Local Dev
b791871390 feat(theseus): one-click "Install in Theseus" from theseus.x/extensions
The extensions page could only hand out tarballs; installing meant going
to Settings › Extensions › Community and finding the entry again. Pages
now get window.bcnr.installExtension(id) and Theseus intercepts
theseus://extensions/install/<id> links (page clicks, target=_blank and
the address bar). The page only names a catalog id: Theseus fetches the
catalog and package itself, asks in a native dialog the page cannot draw
over, verifies the publisher signature against the name's current owner
and activates the add-on — the same path a Settings install takes. One
prompt at a time; an already-installed version says so instead of
offering a no-op update.

The site shows the button inside Theseus (feature-detected on the
bridge), a "update Theseus" hint on older builds and a download hint in
other browsers.
2026-09-21 02:40:05 +02:00
Local Dev
bd6aab02fe feat(theseus): profile at %APPDATA%\Theseus, extensions under extensions\
The profile folder was Electron's default from the product name
("Theseus Navigator") and add-ons lived in addons\ under it. Now:

  %APPDATA%\Theseus\extensions\          installed extensions
  %APPDATA%\Theseus\extensions-data\     per-extension storage + scratch
  %APPDATA%\Theseus\extensions-backups\  replaced copies
  %APPDATA%\Theseus\extensions-staged\   staged updates

Both moves are one-time migrations on the first start that finds the old
layout: the profile folder is renamed (same volume, instant) or copied
when a rename is refused, with the old folder left in place in that case;
the four sub-folders are renamed before the extension host first reads
them. Nothing is deleted. THESEUS_USER_DATA still overrides everything.

The host now hands each extension its data folder as api.dataDir; the
Screenshot and PDF editor add-ons used to rebuild the old path from their
own folder for scratch files (so they recreated addons-data\ after the
move) and now use the field, with versions bumped so the bundles reseed.
2026-09-21 01:55:25 +02:00
Local Dev
5e4bd22609 refactor(docx-editor): ship it as a community extension, not part of the browser
A .docx editor is a megabyte of vendored library. Bundling it would charge
that to everyone who wanted a browser, including the people who will never
open a Word document in it.

So it leaves the build: out of bundled-addons/, out of extraResources, absent
from a fresh profile. It arrives the way anyone else's extension does —
Settings › Extensions › Community, from the catalogue the gateway builds, and
listed on theseus.x/extensions alongside everything else published there.
That also means it is signed by the owner of a BNS name rather than by the
operator key, which is the right trust story for something that isn't part of
the browser.

`npm run pack` produces the tarball the publish page takes; the signature
needs the publisher name's wallet, so it isn't something the repo can do.

The end-to-end test now installs the extension into a throwaway profile the
way the community installer would, and asserts up front that a fresh profile
doesn't already have it — the bundling is what was being removed, so it is
worth a test that would notice it coming back.
2026-09-21 01:06:58 +02:00
Local Dev
6b2c4b25c0 feat(pdf-editor): read, mark up and reshape a PDF without leaving the browser
A PDF that needs a signature, a highlight or a page removed currently sends
the user out to a desktop application or, worse, to a web service that wants
the document uploaded first. Both are poor answers for a browser whose point
is that nothing has to leave the machine. This is a full-tab editor that opens
a PDF, marks it up, fills its forms and saves a new copy, entirely locally.

Two engines, vendored rather than installed, because an add-on ships as a
self-contained folder over the signed update channel and nothing runs a
package manager on the way: pdf.js reads and renders, pdf-lib writes. They
share no state. Everything in between lives in PDF user space — points,
origin bottom-left — which is the one coordinate vocabulary both speak, so a
mark survives zooming, rotating and reordering with no conversion table and
save-time needs to know nothing about how a page happened to be displayed.

The page strip is built from pdf.js's PDFPageView components rather than its
PDFViewer, which renders pages in the file's own order and cannot hide,
reorder or individually rotate one — three of the features here. Text layers
are ours and stay attached for every page, drawn or not, because Theseus's
find bar is Chromium's findInPage over the live DOM and a torn-down text layer
is a page Ctrl+F cannot see. Canvases are virtualised; a letter page at 100%
is 3.4 MB of bitmap.

Redaction is the part worth being careful about. A black box over text hides
nothing — the text stays in the content stream and comes straight out of a
copy-paste — so the editor says so in a modal before the tool can be used,
and on save rebuilds each redacted page as an image, which genuinely removes
it. Pages that were not redacted are untouched. Form widgets and links are
kept, since they were never the leak.

Saving never writes over the original: every save reloads the source bytes and
replays the session onto a fresh copy, so a botched save cannot poison the
next one.

Out of scope for this first version: editing the text that is already in the
document, and writing XFA forms back (pdf-lib cannot, so those are fill-and-
print only, and the editor says so on open).
2026-09-20 20:58:21 +02:00
Local Dev
92ef408ac5 feat(docx-editor): edit Word documents without quietly eating what Word put in them
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.

Three things keep this one honest.

The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.

The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.

Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.

Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
2026-09-20 20:46:29 +02:00
Local Dev
decf118c88 feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:

  {
    "capabilities": ["context-menu-item", ...],
    "context-menu-items": [
      { "id": "translate-selection", "label": "Translate selection",
        "when": "selectionText", "icon": "🌐" }
    ]
  }

The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.

Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.

api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.

Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.

This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
Local Dev
501c7cf273 feat(theseus/translate): new sidebar add-on — right-click Translate selection
Adds a bundled add-on `translate` with a sidebar panel + a right-click
"Translate selection" menu item. Two swappable backends:

- LibreTranslate (default) — free MIT engine; the panel's Settings tab
  lets the user point at any instance (public or self-hosted) and drop
  in an API key if one's required.
- Google (unofficial free endpoint at translate.googleapis.com/
  translate_a/single) — no key, wide coverage, but unofficial and
  Google can break it any time. Opt-in fallback.

Flow: user selects text on a page, right-clicks -> "Translate
selection". Add-on's context-menu handler stashes the selection under
storage.__pending and calls api.revealSidebar("main"); the panel
loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter
in the input textarea also translates. Source + target language
choices, browser-language default target, swap button, copy-to-
clipboard on the output, settings gear.

Depends on a new "context-menu-item" capability + api.revealSidebar
hook in addons-host.js / main.js. Those wiring changes are prepared
but not committed here — a parallel session is refactoring the same
functions concurrently, so the safe path is to land translate/ first
and let the wiring go in alongside the next host-facing commit. Until
the wiring lands, the manifest's "context-menu-item" cap is silently
dropped (per validateManifest's unknown-caps policy) and the sidebar
panel + the panel's translation UI still work standalone — the
right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
Local Dev
b7b705827a feat(theseus): Firefox-style tab overflow — scroll arrows, earliest tabs slide out
Tabs keep a readable minimum width (76px). Once they overflow, the row
scrolls: the earliest tabs slide out on the left and an arrow at each end
moves the row by 60% of its width, disabled at its end of travel and
hidden while everything fits. The wheel scrolls the row too, the selected
tab is brought into view when the selection changes (never while the user
is scrolling), and the + button stays outside the row. Moves are immediate
rather than animated: the chrome view has no smooth scrolling, and frame
callbacks stop while the window is occluded, which stranded a frame-driven
slide at its start.
2026-09-20 16:50:24 +02:00
Local Dev
f5877bc890 fix(theseus): tabs never run under the window controls; the strip scrolls
With many tabs open, the last ones slid under the minimise / maximise /
close overlay: the strip reserved that space as padding, and padding does
not stop overflowing flex items. The tabs now live in their own row that
clips at its own edge, shrink down to icon + close before anything is
hidden, and the row scrolls sideways with the wheel once they hit that
minimum, keeping the selected tab in view. The + button sits outside the
row so it stays reachable no matter how many tabs are open.
2026-09-20 16:36:35 +02:00
Local Dev
5dc90f20b4 theseus: home page carries the Theseus icon; silentmode.st/theseus favicon path
The home / new-tab page declared no icon, so those tabs sat blank in the
strip; the compass mark is now inline in home.html as a data: URI (no file
path, no network). The silentmode.st copy of the Theseus page pointed its
icon at /assets/favicon.svg, which only exists on theseus.x — page-relative
now, so the tab shows the mark there too.
2026-09-20 16:29:35 +02:00
Local Dev
a8e8e5e9cf chore(theseus): 0.3.49 — Electron 44, community extensions, title-bar tabs, HTTP auth, local files, signed DNS 2026-09-20 16:08:50 +02:00
Local Dev
605a4d870c feat(theseus): hold or right-click Back/Forward for the history list
Holding Back or Forward for 450 ms (or right-clicking it) pops a native
menu of the tab's history entries in that direction — nearest first, up to
15, titled with the page title and host — and picking one jumps straight
to it. A hold swallows the click that would otherwise fire on release, so
a long press never also goes back one page.
2026-09-20 16:07:34 +02:00
Local Dev
2b52fcef2f settings: the extensions link reads just "Extensions" 2026-09-20 15:33:33 +02:00
Local Dev
8ff8bc51ff feat: community extensions — publish with a BCDN name, install from Settings, theseus.x catalog
Anyone who owns a BCDN name can now publish a Theseus extension, and every
Theseus can install it with the publisher's signature verified locally.

Gateway (Argus/src/gateway/public-gateway.mjs):
  PUT /api/ext/<name>/<id>/<version> takes the gzipped tar, checks two BCH
  message signatures against the name's current NFT owner (one authorises
  the upload, one is stored in the channel), inspects the package
  (addon.json at the root, id/version/main match, 8 MB cap), enforces
  first-publisher ownership of an id and monotonic versions, and writes the
  tarball, the extension's updates.json and community/catalog.json to Sia.
  GET /api/ext/catalog reads the catalog back with CORS.

Theseus:
  lib/publisher-sig.mjs recovers the signer of a channel entry; main.js
  compares it with the publisher name's owner from Theseus's own chain
  index before installing or updating, so neither the relay nor a tampered
  catalog can pass off code under a trusted name. addon-updater.js gains
  installCommunity() and accepts publisher-signed entries in the regular
  update check (operator Ed25519 entries unchanged). Settings › Extensions
  shows the community catalog with Install / Update; Settings › Plug-ins
  links to theseus.x/plug-ins.

theseus.x:
  /plug-ins/ is a separate page for the first-party plug-ins (Aegis,
  Ariadne's Thread) with live versions and hashes; /extensions/ lists the
  bundled extensions, the community catalog, and how to build and publish;
  /extensions/publish/ signs and uploads a package in the browser with the
  wallet that holds the publisher's name (session helper + wallet bundle
  copied alongside).
2026-09-20 15:26:30 +02:00
Local Dev
68735ad490 fix(theseus): address bar follows the page; tab strip stops blinking; extensions page
- Address bar: the chrome view keeps document.activeElement on the URL
  input after the user clicks into the page (focus moved to the tab's own
  view), and the "don't clobber typed text" guard then froze the bar until
  something blurred the field. The guard now requires real focus
  (document.hasFocus()), and the field is blurred when the view loses focus.
- Tab strip: rebuilt with innerHTML on every tabs event, which recreated
  every favicon <img> — a blink on the other tabs whenever one tab loaded,
  reloaded or changed title — and dropped drag state mid-gesture. Elements
  are now keyed by tab id (chips by group colour), updated in place, and
  moved into order; the strip is never rebuilt.
- Settings › Extensions links to theseus.x/extensions. That page now has a
  card per bundled add-on with the current signed version, tarball and
  hash read from each add-on's updates.json at load (it still claimed
  Screenshot 0.2.4 while the channel serves 0.6.5).
2026-09-20 14:39:07 +02:00
Local Dev
3d6f53e359 chore(theseus): Electron 33 → 44 (Chromium 130 → 152)
A year-old engine is now a bot signal in itself: DataDome blocked
estore.asus.com for Theseus on Chromium 130 while the same request claiming
Chrome 152 went through, and Chromium 130 carries a year of unpatched
renderer bugs. Electron 44 boots the app unchanged; verified on the new
engine: local files, HTTP auth prompt, tab strip in the title bar, BNS
sites and window.bcnr, all bundled add-ons, the Tor toggle
(check.torproject.org via the SOCKS agent), and a full NSIS + portable
build (artifacts grow from ~99 MB to ~132 MB with the larger engine).

session.setPreloads is deprecated from 35 on; preloads are registered
with registerPreloadScript when available, with the old call as fallback.
2026-09-20 14:19:20 +02:00
Local Dev
05cc2dd60f fix(theseus): present a consistent stock-Chrome identity to bot filters
estore.asus.com (DataDome, "AI Threats Detection") served its block page to
Theseus while a plain Chromium on the same connection got the product page.
Three things in our identity were wrong:

- The client-hint brand list was hand-written with "Google Chrome" first —
  a permutation real Chrome never sends. It is now computed the way Chromium
  does it (GREASE brand from the major version, per-major brand order).
- The page-side navigator.userAgentData still said "Chromium" only, so
  headers and JS disagreed. The same metadata is now installed per tab via
  Emulation.setUserAgentOverride, so both sides match.
- Accept-Language went out as "en-US,en;q=0.8;q=0.9": we appended a q-value
  and Chromium appended another. Chromium now gets a plain language list.

That makes the identity self-consistent, but DataDome still blocks on the
version: Chromium 130 (Electron 33) is a year old, and claiming Chrome 152
(THESEUS_CHROME_VERSION, added here for exactly this test) loads the page.
The real fix is a current Electron; this commit removes the other tells.
2026-09-20 14:12:09 +02:00
Local Dev
dbc18da36e feat(theseus): tabs in the title bar, link-opened tabs next to their opener, clearer active tab
Three tab-strip changes from use:

- A tab opened from a link (target=_blank, middle-click, the context menu,
  Duplicate) now goes right after the tab it came from — and after any
  siblings that tab already opened — instead of at the end of the strip.
  The + button, session restore and add-on requests still append.
- The selected tab gets an accent stripe and outline on top of its brighter
  fill; with a dozen same-size tabs the fill alone was easy to lose. A
  grouped tab keeps its group colour on the stripe.
- On Windows the tab row is the title bar: the native frame is hidden, the
  minimise/maximise/close buttons are drawn as an overlay over the chrome
  (colours follow the theme), the row is a drag region with every control
  in it opted out, and 140px (or the overlay's real width when the API is
  exposed) is kept clear on the right. The page gains the old title bar's
  height. Other platforms keep the native frame.
2026-09-20 02:19:19 +02:00
Local Dev
cd098bc9f3 screenshot 0.6.5: line tool + resizable text box
Two annotation-editor asks:

- Line tool: same drag flow as the arrow, no arrowhead. New toolbar
  button between arrow and rect, shortcut L.
- Text box is resizable: swapped the single-line input for a textarea
  with resize:both and a drag corner. Enter still commits, Shift+Enter
  inserts a newline, blur commits. Multi-line rendering steps the
  fillText baseline by 1.15x the font size per line so the baked
  pixels match the live layout.

The textarea swallows its own pointer events so drag-resizing the
corner doesn't leak to the canvas underneath.
2026-09-18 01:02:56 +02:00
Local Dev
ed323713d7 fix(theseus): Ariadne's Thread card — find the installed resolver, answer in ~2 s
Two reasons the Plug-ins card looked dead ("only a Refresh button"):

1. The state check ran Get-ScheduledTask, whose module import took 8–10 s
   cold, and only then fetched the release manifest. Every button is hidden
   during "checking…", so for 10–15 s the card showed nothing but Refresh.
   Task state now comes from the Task Scheduler COM object (numeric, locale-
   independent — schtasks.exe prints localized words on non-English
   Windows) and the manifest fetch runs in parallel: ~2 s.

2. The Inno installer's AppId is written as {{…}}, which Inno registers as
   {…}}_is1 (doubled closing brace). Theseus looked for the single-brace
   key, never found it, and so never knew the installed version — no Update
   button, no Uninstall button. The entry is now found by DisplayName.
2026-09-16 20:15:19 +02:00
Local Dev
27a1243e4d feat(theseus): consume owner-signed DNS records alongside on-chain records
Owners can now publish a signed _records.json (A/AAAA/MX/TXT/CNAME/NS)
beside their Sia content; the gateway verifies it against the current NFT
holder and serves it as GET /api/dns/<name>. Every BCDN resolution now
starts a background fetch of that answer (3 s cap, 30 s cache, seq rollback
guard) and attaches it to the entry as entry.dns. Navigation never waits
for it — on-chain h/s3/ip/p/u stay authoritative — except when a name has
no content record at all and a signed A is the only way to reach it. Only
registered names are looked up, so ICANN hosts never reach the gateway.

Exposed as window.bcnr.dnsRecords(name) for add-ons (TXT verification, MX
for mail bridges), on resolveName() as .dns, and as a "Signed DNS" row in
the site-info popover.
2026-09-16 00:53:13 +02:00
Local Dev
621758834a chore(theseus): 0.3.48 — HTTP auth prompt, local files from the address bar, on top of 0.3.47 2026-09-15 23:09:45 +02:00
Local Dev
f9a7063635 merge: 0.3.47 plug-in category + panel-driven add-on self-update, aegis 0.6.31 into master line 2026-09-15 23:09:30 +02:00
Local Dev
27265c4e21 fix(theseus): put the last two main.js hunks where they belong
df181d9 and b2c6f62 were staged hunk-by-hunk from a working tree that also
carried unrelated uncommitted edits, and the context-free hunks landed a
few lines off: the local-file check ran after the search rewrite (so paths
still went to the search engine in the committed file), the loadBns header
sat inside loadLocalFile's comment, and the refreshTabUrl comment was split
by the auth block. Content is unchanged; only placement is corrected.
2026-09-15 22:33:07 +02:00
Local Dev
ab78535192 fix(theseus): prompt for HTTP authentication instead of showing the bare 401
Sites behind Basic/Digest auth (silentmode.st/guardian/admin) rendered the
server's 401 page because nothing listened for Electron's login event,
which cancels every challenge by default. A modal sign-in prompt now asks
for the credentials and answers the challenge; Cancel leaves the 401 page.
Concurrent challenges for the same host and realm share one prompt while it
is open, and a rejected answer re-prompts instead of replaying the same
credentials until Chromium gives up with ERR_TOO_MANY_RETRIES.

Also: THESEUS_NO_UPDATE_CHECK skips the release check, for throwaway dev
instances — the one-click install chip they show targets the real install.
2026-09-15 22:30:29 +02:00
Local Dev
e596454446 fix(theseus): open local files from the address bar instead of searching for them
A typed or pasted path such as D:\Dev\x\page.html has no dotted host, so
the URL-vs-search heuristic handed it to the search engine. Paths (drive,
UNC, file://, and absolute/~ on POSIX) now load as file:// URLs before the
heuristic runs. Local-file tabs keep their file:// URL in the address bar
(normally suppressed because our own home/error pages are file://), show a
"Local file" badge, and hide the registry button since no name resolution
is involved. A missing file lands on the error page with a matching badge.
2026-09-15 01:36:10 +02:00
Local Dev
f46e9112b7 chore(theseus): 0.3.47 — plug-in category + panel-driven addon self-update, aegis 0.6.31
Theseus core:
- addons-host: manifest.category ("plugin") propagates through snapshot(); new
  addon API surface checkAndStageSelfUpdate() + restartApp() so a plug-in
  can offer in-panel "update now → restart to apply" without pushing the
  user to Settings.
- main.js: wires the two new hooks into the AddonHost constructor.
- settings.html: Extensions listing filters out category==="plugin"; those
  add-ons live in Plug-ins instead, single source of truth.

Aegis 0.6.31:
- BTC picker trimmed to Signet only; testnet3 hidden (adapter kept so any
  existing wallet still loads).
- Wallet strip groups by chain, not chain:network; ticker gets a ▾ chevron
  and a dropdown listing every subnetwork with its own totals. Mainnet
  reads as the plain ticker; testnets carry a small Chipnet/Signet/Sepolia
  pill inline.
- Per-unit price sits directly under the ticker; amount + fiat mirror on
  the right — one glance covers name/price/holding/value.
- + Add and ⋯ More promoted from the strip into the header's action row,
  next to the new ✎ chip (was the redundant top ⋯). Duplicate "Manage
  current wallet" entry removed from the More menu.
- Footer update chip is a two-step flow via the new API: stage → restart.
  Falls back to opening Settings on any Theseus that lacks the hooks.
- Manifest declares "category": "plugin".
2026-09-14 02:30:51 +02:00