2026-09-08 01:42:41 +02:00
<!doctype html>
< html lang = "en" >
< head > < meta charset = "utf-8" > < title > Theseus — Settings< / title >
< style >
2026-09-08 01:51:03 +02:00
/* --acid-text: color used for text/inline emphasis. In dark mode it
equals --acid (bright acid green, ~10:1 on the dark ground). Light
mode overrides to #253A49 (BCH dark navy) which is ~12:1 on white,
because the BCH-teal --acid (#0AC18E) only manages ~2.9:1 as text.
--acid still drives fills, borders, and background tints where a
brand-green splash is wanted. */
2026-09-08 01:42:41 +02:00
:root{ --bg:#0b0e14; --panel:#141a24; --line:rgba(255,255,255,.09);
2026-09-08 01:51:03 +02:00
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; --acid-text:#d6ff3d; }
2026-09-08 01:42:41 +02:00
*{box-sizing:border-box}
body{margin:0;height:100vh;background:var(--bg);color:var(--ink);font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
.app{display:flex;height:100vh}
/* left sidebar menu */
.side{flex:none;width:220px;background:#0e131c;border-right:1px solid var(--line);padding:20px 12px;display:flex;flex-direction:column;gap:2px}
2026-09-08 01:51:03 +02:00
.brand{font-weight:700;color:var(--acid-text);font-size:15px;padding:4px 12px 16px}
2026-09-08 01:42:41 +02:00
.side a{display:block;padding:9px 12px;border-radius:8px;color:var(--mut);text-decoration:none;font-size:14px;cursor:pointer}
.side a:hover{background:#ffffff0a;color:var(--ink)}
2026-09-08 01:51:03 +02:00
.side a.active{background:rgb(from var(--acid) r g b / .12);color:var(--acid-text)}
2026-09-08 01:42:41 +02:00
/* content */
.content{flex:1;overflow-y:auto;padding:2.4rem 2.4rem 4rem}
section{max-width:640px}
h1{font-size:1.4rem;margin:0 0 .2rem}
.lede{color:var(--mut);margin:0 0 1.8rem;font-size:13.5px}
h2.sub{font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);margin:1.8rem 0 .2rem;border-top:1px solid var(--line);padding-top:1.4rem}
.subd{color:var(--mut);margin:0 0 1rem;font-size:13px}
.row{display:flex;align-items:center;gap:16px;background:var(--panel);border:1px solid var(--line);
border-radius:12px;padding:14px 18px;margin-bottom:10px}
.row .txt{flex:1}
.row .t{font-weight:600}
.row .d{color:var(--mut);font-size:13px;margin-top:2px}
/* control column — mode select + optional value field on the same row so the
dropdown menus don't get cut off underneath, wraps only when narrow */
.ctl{display:flex;flex-direction:row;flex-wrap:wrap;gap:6px;align-items:center;justify-content:flex-end;flex:none;max-width:60%}
.ctl .coords{display:flex;gap:6px}
select,.ctl input{background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;outline:none;min-width:140px}
select:focus,.ctl input:focus{border-color:#4b7bec}
.ctl input{width:170px} .ctl .coords input{width:92px;min-width:auto}
/* Chromium's native < select > popup uses the page's color-scheme; when it's
"light dark" (both accepted) Chromium picks by the OS, so a dark-theme app
on a light OS shows a light popup. main.js's applyTheme() maps to
nativeTheme.themeSource, which drives prefers-color-scheme — so pinning
color-scheme via that media query keeps the popup in sync automatically. */
:root { color-scheme: dark; }
2026-09-08 01:51:03 +02:00
@media (prefers-color-scheme: light) { :root { color-scheme: light; --acid: #0AC18E; --acid-text: #253A49; } }
2026-09-08 01:42:41 +02:00
/* Explicit option styling — Chromium respects it in the popup on Windows. */
select option { background: #1b2330; color: var(--ink); }
@media (prefers-color-scheme: light) { select option { background: #f1f3f7; color: #1a1f28; } }
code{font-family:ui-monospace,monospace;font-size:12px;background:#0e131b;border:1px solid var(--line);border-radius:5px;padding:1px 5px;color:#bfeae4}
.addeng{display:flex;gap:6px} .addeng input{flex:1;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:13px;outline:none}
.addeng input:focus{border-color:#4b7bec}
2026-09-08 01:51:03 +02:00
.btn{border:1px solid color-mix(in srgb, var(--acid) 33%, transparent);background:rgb(from var(--acid) r g b / .12);color:var(--acid-text);border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
2026-09-08 01:42:41 +02:00
.btn:hover{background:rgb(from var(--acid) r g b / .22)}
/* segmented control — small toggle used elsewhere (kept for reuse) */
.segseg{display:inline-flex;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:2px;gap:2px}
.segseg .seg{background:transparent;color:var(--mut);border:none;border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer;font:inherit;line-height:1.2}
.segseg .seg:hover{color:var(--ink)}
.segseg .seg.on{background:#1c2432;color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
/* theme cards — three visual previews (System / Light / Dark) */
.themeCards{display:flex;gap:14px;margin:6px 0 4px;flex-wrap:wrap}
.themeCards .tc{background:transparent;border:2px solid var(--line);border-radius:10px;padding:10px;
display:flex;flex-direction:column;align-items:center;gap:8px;cursor:pointer;color:var(--ink);
font:inherit;font-size:13px;min-width:150px;transition:border-color .12s}
.themeCards .tc:hover{border-color:#4b7bec55}
.themeCards .tc.on{border-color:#4b7bec;box-shadow:0 0 0 1px #4b7bec inset}
.themeCards .mock{width:130px;height:82px;border-radius:6px;overflow:hidden;display:block;position:relative;
border:1px solid rgba(255,255,255,.08)}
.themeCards .mock .mm-chrome{position:absolute;left:0;right:0;top:0;height:22px;display:block}
.themeCards .mock .mm-chrome::before{content:"";position:absolute;left:8px;top:6px;width:8px;height:8px;border-radius:50%;background:#f6768a}
.themeCards .mock .mm-chrome::after {content:"";position:absolute;left:22px;top:6px;width:8px;height:8px;border-radius:50%;background:#f6c15c;box-shadow:14px 0 0 #6ec27d}
.themeCards .mock .mm-body{position:absolute;left:0;right:0;top:22px;bottom:0;display:block}
.themeCards .mock-light .mm-chrome{background:#e9ecf2}
.themeCards .mock-light .mm-body {background:#ffffff;background-image:linear-gradient(#0000000c 1px,transparent 1px);background-size:100% 12px;background-position:0 10px}
.themeCards .mock-dark .mm-chrome{background:#141b28}
.themeCards .mock-dark .mm-body {background:#0b0e14;background-image:linear-gradient(#ffffff10 1px,transparent 1px);background-size:100% 12px;background-position:0 10px}
.themeCards .mock-system .mm-chrome{background:linear-gradient(90deg,#141b28 0 50%,#e9ecf2 50% 100%)}
.themeCards .mock-system .mm-body{background:linear-gradient(90deg,#0b0e14 0 50%,#ffffff 50% 100%)}
.themeCards .tc-label{font-weight:500;color:var(--ink)}
.polrow{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer}
.polrow:hover{background:#141c28}
.polrow input{accent-color:var(--acid)}
.pmuted{color:var(--mut)}
.ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
.ceng .cs{font-size:14px;flex:none}
/* engine checklist */
.eng{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
.eng .eic{width:18px;height:18px;flex:none;display:grid;place-items:center}
.eng .eic .ei{width:16px;height:16px;border-radius:3px} .eng .eic .es{font-size:14px}
.eng .enm{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.eng .cx{cursor:pointer;color:var(--dim);border:none;background:transparent;font-size:13px} .eng .cx:hover{color:#f6768a}
.eng{cursor:default}
.eng .grip{flex:none;color:var(--dim);cursor:grab;font-size:14px;line-height:1;padding:0 2px;user-select:none}
.eng .grip:active{cursor:grabbing}
.eng.dragging{opacity:.45}
.eng.over{border-color:var(--acid);box-shadow:0 -2px 0 var(--acid) inset}
.eng.off{opacity:.55}
.eng.off .enm{color:var(--mut)}
.ehdr{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin:14px 0 6px;padding-top:2px}
.ehdr:first-child{margin-top:0}
/* right-click context menu for an engine row */
.ctxmenu{position:fixed;z-index:9999;background:#1c222c;border:1px solid var(--line);border-radius:8px;
box-shadow:0 12px 34px #000c;padding:4px;min-width:180px;font-size:13px;color:var(--ink)}
.ctxmenu .mi{padding:7px 12px;border-radius:5px;cursor:pointer;white-space:nowrap}
.ctxmenu .mi:hover{background:#ffffff10}
.ctxmenu .mi.danger{color:#f6768a}
.ctxmenu .mi.danger:hover{background:rgba(246,118,138,.12)}
@media (prefers-color-scheme: light){
.ctxmenu{background:#ffffff;border-color:rgba(0,0,0,.15)}
.ctxmenu .mi:hover{background:rgba(0,0,0,.05)}
}
/* engine catalog panel — appears under the enabled list when "+ Add" is clicked */
.engcat{margin-top:6px;padding:12px 12px 10px;background:#0e131c;border:1px solid var(--line);border-radius:10px}
.engcat .cat{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;font-size:13px}
.engcat .cat:hover{background:#141b26}
.engcat .cat .eic{width:18px;height:18px;flex:none;display:grid;place-items:center}
.engcat .cat .eic .ei{width:16px;height:16px;border-radius:3px} .engcat .cat .eic .es{font-size:14px}
.engcat .cat .enm{flex:1}
.engcat .cat .kind{font-size:10.5px;letter-spacing:.05em;color:var(--dim);padding:1px 6px;border:1px solid var(--line);border-radius:999px}
.engcat .cat .add{background:transparent;border:1px solid var(--line);color:var(--ink);border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer}
2026-09-08 01:51:03 +02:00
.engcat .cat .add:hover{background:rgb(from var(--acid) r g b / .14);border-color:color-mix(in srgb, var(--acid) 33%, transparent);color:var(--acid-text)}
2026-09-08 01:42:41 +02:00
.engcat .cempty2{color:var(--dim);font-size:12.5px;padding:6px 8px}
.sw.sm{width:38px;height:22px} .sw.sm .knob{width:15px;height:15px} .sw.sm input:checked + .track .knob{transform:translateX(16px)}
.ceng .cn{font-weight:600} .ceng .cu{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.ceng .cx{cursor:pointer;color:#8b98a9;border:none;background:transparent;font-size:13px} .ceng .cx:hover{color:#f6768a}
.cempty{color:var(--dim);font-size:12.5px}
/* toggle */
.sw{position:relative;width:46px;height:26px;flex:none;cursor:pointer}
.sw input{opacity:0;width:0;height:0}
.track{position:absolute;inset:0;background:#2b3444;border:1px solid var(--line);border-radius:999px;transition:.15s}
.knob{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#8b98a9;transition:.15s}
.sw input:checked + .track{background:rgb(from var(--acid) r g b / .25);border-color:color-mix(in srgb, var(--acid) 33%, transparent)}
.sw input:checked + .track .knob{transform:translateX(20px);background:var(--acid)}
.note{color:var(--dim);font-size:12.5px;margin-top:1.4rem;border-top:1px solid var(--line);padding-top:1rem}
/* light theme (placed last so these win over the dark base rules) */
@media (prefers-color-scheme: light){
fix(theseus/settings/light): map every dark-navy card to a BCH-palette surface
Screenshot showed Registries > Collision policy rows rendering as
solid dark bars in light mode — .polrow hardcoded background:#10151f
with no light override, so the whole card blob-ed dark on the white
page and the text disappeared. Same story for .segseg, .engcat,
select/input backgrounds, .ctxmenu, .themeCards etc.
The light-media block gains a proper mapping keyed to the user's
BCH palette (#0AC18E / #253A49 / #F8FDFF):
- --bg → #F8FDFF (BCH white), --ink → #253A49 (BCH dark)
- .polrow → #f4f8fb card on white; hover #eaf0f5; SELECTED row
gets an acid-green tint via :has(input:checked) so BCDN-first
reads as the chosen option without an ugly dark bar
- .segseg, .engcat, .themeCards, .ctxmenu, select/input all get
the same treatment — subtle off-white surfaces on top of the
panel, dark-navy text
- Sidebar (.side) already had a light override; refined the border
and hover states to use rgba(37,58,73,X) so they match the ink
Verified via CDP: --bg=#F8FDFF, --ink=#253A49, .polrow color=
rgb(37,58,73), engcat bg=rgb(244,248,251), theme card bg=white.
2026-09-08 18:49:53 +02:00
:root{ --bg:#F8FDFF; --panel:#ffffff; --line:rgba(37,58,73,.15); --ink:#253A49; --mut:#4a5262; --dim:#7b8494; }
.side{ background:#f1f5f8; border-right-color:rgba(37,58,73,.10); }
.side a{ color:#4a5262; }
.side a:hover{ background:rgba(37,58,73,.06); color:#253A49; }
/* Every dark-hardcoded card / control needs a light equivalent. All the
#10151f / #0e131c / #1b2330 tones map to a subtle off-white so they
still read as cards against the #F8FDFF page, with rgba(37,58,73,X)
hover tints keyed to BCH dark for consistent depth. */
.polrow{ background:#f4f8fb; color:#253A49; }
.polrow:hover{ background:#eaf0f5; }
.polrow input:checked ~ span,
.polrow:has(input:checked){ background:rgb(from var(--acid) r g b / .10); border-color:color-mix(in srgb, var(--acid) 35%, transparent); }
.segseg{ background:#eef1f6; }
.segseg .seg.on{ background:#ffffff; color:#253A49; box-shadow:inset 0 0 0 1px rgba(37,58,73,.12); }
.engcat{ background:#f4f8fb; }
.engcat .cat:hover{ background:#eaf0f5; }
select, .ctl input, .addeng input{ background:#f1f5f8; color:#253A49; }
select option{ background:#ffffff; color:#253A49; }
.track{ background:#d3dae3; }
2026-09-08 01:42:41 +02:00
.knob{ background:#8a93a2; }
fix(theseus/settings/light): map every dark-navy card to a BCH-palette surface
Screenshot showed Registries > Collision policy rows rendering as
solid dark bars in light mode — .polrow hardcoded background:#10151f
with no light override, so the whole card blob-ed dark on the white
page and the text disappeared. Same story for .segseg, .engcat,
select/input backgrounds, .ctxmenu, .themeCards etc.
The light-media block gains a proper mapping keyed to the user's
BCH palette (#0AC18E / #253A49 / #F8FDFF):
- --bg → #F8FDFF (BCH white), --ink → #253A49 (BCH dark)
- .polrow → #f4f8fb card on white; hover #eaf0f5; SELECTED row
gets an acid-green tint via :has(input:checked) so BCDN-first
reads as the chosen option without an ugly dark bar
- .segseg, .engcat, .themeCards, .ctxmenu, select/input all get
the same treatment — subtle off-white surfaces on top of the
panel, dark-navy text
- Sidebar (.side) already had a light override; refined the border
and hover states to use rgba(37,58,73,X) so they match the ink
Verified via CDP: --bg=#F8FDFF, --ink=#253A49, .polrow color=
rgb(37,58,73), engcat bg=rgb(244,248,251), theme card bg=white.
2026-09-08 18:49:53 +02:00
code, .ceng, .eng{ background:#eef2f6; color:#253A49; }
.themeCards .tc{ background:#ffffff; border-color:rgba(37,58,73,.15); }
.themeCards .tc:hover{ border-color:rgba(37,58,73,.30); }
.ctxmenu{ background:#ffffff; border-color:rgba(37,58,73,.15); color:#253A49; }
.ctxmenu .mi:hover{ background:rgba(37,58,73,.06); }
2026-09-08 01:42:41 +02:00
}
< / style > < / head >
< body >
< div class = "app" >
< nav class = "side" >
< div class = "brand" > ⛓ Theseus< / div >
< a data-sec = "general" class = "active" > General< / a >
< a data-sec = "search" > Search< / a >
< a data-sec = "passwords" > Passwords< / a >
< a data-sec = "performance" > Performance< / a >
< a data-sec = "privacy" > Privacy< / a >
2026-09-08 23:00:41 +02:00
< a data-sec = "plugins" > Plug-ins< / a >
2026-09-08 01:42:41 +02:00
< a data-sec = "addons" > Extensions< / a >
< / nav >
< div class = "content" >
<!-- GENERAL -->
< section id = "general" >
< h1 > General< / h1 >
< p class = "lede" > Changes apply immediately and are saved for next time.< / p >
< h2 class = "sub" style = "border-top:0;padding-top:0;margin-top:0" > Startup< / h2 >
< div class = "row" >
< div class = "txt" > < div class = "t" > Open previous windows and tabs< / div > < div class = "d" > Restore the tabs from your last session when Theseus starts.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "restoreSession" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:8px" >
< div class = "txt" >
< div class = "t" > Updates< / div >
< div class = "d" > Theseus already checks the release manifest at boot and every 6h. Click below to check right now.< / div >
< / div >
2026-09-08 12:36:21 +02:00
< div id = "updStatus" class = "pmuted" style = "font-size:12.5px" > Loading…< / div >
2026-09-08 01:42:41 +02:00
< div style = "display:flex;gap:8px;flex-wrap:wrap" >
< button id = "updCheck" class = "btn" > Check for updates< / button >
< / div >
< / div >
< h2 class = "sub" > Appearance< / h2 >
< p class = "subd" > Choose a theme for the browser. < b > System< / b > follows your operating system's light/dark setting.< / p >
< div class = "themeCards" id = "theme" role = "radiogroup" aria-label = "Theme" >
< button type = "button" class = "tc" data-val = "system" role = "radio" aria-checked = "false" >
< span class = "mock mock-system" > < span class = "mm-chrome" > < / span > < span class = "mm-body" > < / span > < / span >
< span class = "tc-label" > System< / span >
< / button >
< button type = "button" class = "tc" data-val = "light" role = "radio" aria-checked = "false" >
< span class = "mock mock-light" > < span class = "mm-chrome" > < / span > < span class = "mm-body" > < / span > < / span >
< span class = "tc-label" > Light< / span >
< / button >
< button type = "button" class = "tc" data-val = "dark" role = "radio" aria-checked = "false" >
< span class = "mock mock-dark" > < span class = "mm-chrome" > < / span > < span class = "mm-body" > < / span > < / span >
< span class = "tc-label" > Dark< / span >
< / button >
< / div >
2026-09-09 00:51:05 +02:00
< h2 class = "sub" style = "margin-top:1.8rem" > Developer tools< / h2 >
< p class = "subd" > Where the Chromium DevTools panel appears when you press < b > F12< / b > or < b > Ctrl+Shift+I< / b > . You can always drag it out to a separate window from inside the panel.< / p >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:10px" >
< div id = "devToolsDockList" style = "display:flex;flex-direction:column;gap:6px" >
< label class = "polrow" > < input type = "radio" name = "devToolsDock" value = "bottom" > < span > < b > Bottom panel< / b > < span class = "pmuted" > — docked under the tab, like Chrome's default. Best for wide screens.< / span > < / span > < / label >
< label class = "polrow" > < input type = "radio" name = "devToolsDock" value = "sidebar" > < span > < b > Right sidebar< / b > < span class = "pmuted" > — DevTools takes the right side. If an add-on sidebar is open, it hides while DevTools is up.< / span > < / span > < / label >
< label class = "polrow" > < input type = "radio" name = "devToolsDock" value = "two-sidebars" > < span > < b > Two sidebars< / b > < span class = "pmuted" > — DevTools opens on the right, and the add-on sidebar stays where it is. The two share the right area.< / span > < / span > < / label >
< / div >
< / div >
2026-09-08 01:42:41 +02:00
< h2 class = "sub" style = "margin-top:1.8rem" > Registries< / h2 >
< p class = "subd" > How Theseus picks between the < b > BCNR / BCDN< / b > and < b > ICANN / IANA< / b > , when a name exists in both.< / p >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:10px" >
< div class = "txt" > < div class = "t" > Collision policy< / div >
< div class = "d" > A name only exists in both registries when its TLD isn't BCNR-unique (e.g. < code > .de< / code > ). BCNR-unique TLDs (that only exist on BCNR) never conflict.< / div > < / div >
< div id = "policyList" style = "display:flex;flex-direction:column;gap:6px" >
< label class = "polrow" > < input type = "radio" name = "collisionPolicy" value = "bcnr-first" > < span > < b > BCDN first< / b > < span class = "pmuted" > — BCDN wins conflicts; falls back to ICANN for anything BCDN doesn't have.< / span > < / span > < / label >
< label class = "polrow" > < input type = "radio" name = "collisionPolicy" value = "icann-first" > < span > < b > ICANN first< / b > < span class = "pmuted" > — ICANN wins conflicts; BCDN fills gaps for non-ICANN TLDs.< / span > < / span > < / label >
< label class = "polrow" > < input type = "radio" name = "collisionPolicy" value = "soft" > < span > < b > Ask each time< / b > < span class = "pmuted" > — an "Open with…" prompt on conflict, remembered per name or per TLD.< / span > < / span > < / label >
< / div >
< / div >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:8px" >
< div class = "txt" > < div class = "t" > Remembered choices< / div >
< div class = "d" > "Always use …" picks you made from the switcher or the prompt. Reset them to be asked again.< / div > < / div >
< div id = "colSummary" class = "pmuted" style = "font-size:12.5px" > < / div >
< div > < button id = "resetCollisions" class = "btn" > Reset remembered choices< / button > < / div >
< / div >
2026-09-08 23:00:41 +02:00
< / section >
<!-- PLUG - INS -->
< section id = "plugins" hidden >
< h1 > Plug-ins< / h1 >
< p class = "lede" > Silent Mode components that live alongside Theseus — the system-wide resolver and the built-in wallet. Each ships with its own install / update / on-off controls.< / p >
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
< div class = "row" style = "justify-content:flex-end;gap:8px" > < a class = "btn" style = "text-decoration:none;display:inline-flex;align-items:center" href = "https://theseus.x/plug-ins/" target = "_blank" rel = "noopener" title = "Current signed versions, hashes and how plug-ins update" > About plug-ins on theseus.x ↗< / a > < / div >
2026-09-08 01:42:41 +02:00
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:8px" >
< div class = "txt" >
< div class = "t" > System-wide resolver — Ariadne's Thread< / div >
< div class = "d" >
A local daemon that resolves BCDN names for < b > every browser on this machine< / b >
(Chrome, Edge, Firefox, etc.), not just Theseus. Turning it off means non-Theseus
browsers stop resolving < code > .bch< / code > / < code > .x< / code > / other BCNR TLDs;
Theseus keeps working either way, since it has its own built-in resolver.
< / div >
< / div >
< div id = "ariadneStatus" class = "pmuted" style = "font-size:12.5px" > checking…< / div >
< div style = "display:flex;gap:8px;flex-wrap:wrap" >
< button id = "ariadneStart" class = "btn" hidden > Turn on< / button >
< button id = "ariadneStop" class = "btn" hidden > Turn off< / button >
< button id = "ariadneInstall" class = "btn" hidden > Install< / button >
< button id = "ariadneUpdate" class = "btn" hidden > Update< / button >
< button id = "ariadneUninstall" class = "btn" hidden style = "color:#f6768a;border-color:rgba(246,118,138,.35)" > Uninstall< / button >
< button id = "ariadneRefresh" class = "btn" > Refresh< / button >
< / div >
< div id = "ariadneMissing" class = "pmuted" style = "font-size:12px" hidden >
Ariadne's Thread isn't installed on this machine. Click < b > Install< / b > above
to run the bundled installer, or grab it manually from
< a href = "https://silentmode.st/tools/" > silentmode.st/tools< / a > .
< / div >
< / div >
2026-09-08 18:09:55 +02:00
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:8px" >
< div class = "txt" >
< div class = "t" > Built-in wallet — Aegis< / div >
< div class = "d" >
Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) that ships in the box.
Bundled with Theseus, but updates are delivered < b > over-the-air< / b > : new signed
versions land without a Theseus release. Runs entirely inside this browser —
never system-wide.
< / div >
< / div >
< div id = "aegisStatus" class = "pmuted" style = "font-size:12.5px" > Loading…< / div >
< div style = "display:flex;gap:8px;flex-wrap:wrap" >
< button id = "aegisCheck" class = "btn" > Check for updates< / button >
2026-09-21 02:43:10 +02:00
< button id = "aegisApply" class = "btn" hidden > Apply update now< / button >
2026-09-08 18:09:55 +02:00
< / div >
< / div >
2026-09-08 01:42:41 +02:00
< / section >
<!-- SEARCH -->
< section id = "search" hidden >
< h1 > Search< / h1 >
< p class = "lede" > Pick what your address bar and the toolbar dropdown search with.< / p >
< div class = "row" >
< div class = "txt" > < div class = "t" > Default search engine< / div > < div class = "d" > Used when you type into the address bar.< / div > < / div >
< div class = "ctl" > < select id = "searchEngine" > < / select > < / div >
< / div >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:10px" >
< div class = "txt" style = "display:flex;align-items:center;justify-content:space-between;gap:12px" >
< div >
< div class = "t" > Additional search engines< / div >
< div class = "d" > These appear in the toolbar dropdown. Drag to reorder. Toggle off to move an engine back to the catalog.< / div >
< / div >
< button id = "engAddBtn" class = "btn" type = "button" > + Add search engine< / button >
< / div >
< div id = "engineList" > < / div >
<!-- Catalog: hidden until "Add" is clicked. Three tiers, top to bottom:
1. curated built-ins the user hasn't enabled (tier="catalog")
2. wider bank filtered by a search box (tier="extra")
3. custom-URL form -->
< div id = "engineCatalog" class = "engcat" hidden >
< div class = "ehdr" > Add from catalog< / div >
< div id = "catalogList" > < / div >
< div class = "ehdr" style = "display:flex;align-items:center;justify-content:space-between;gap:8px" >
< span > Discover more engines< / span >
< input id = "engineFilter" type = "search" placeholder = "Filter by name…" autocomplete = "off"
style="background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:4px 8px;font-size:12px;min-width:auto;width:170px">
< / div >
< div id = "extraList" > < / div >
< div class = "ehdr" > Or add a custom URL< / div >
< div class = "addeng" >
< input id = "engSym" placeholder = "🔍" style = "max-width:52px;text-align:center;flex:none" >
< input id = "engName" placeholder = "Name (e.g. My SearXNG)" >
< input id = "engUrl" placeholder = "https://example.com/search?q=%s" >
< button id = "engAdd" class = "btn" > Add< / button >
< / div >
< / div >
< / div >
< / section >
<!-- PASSWORDS -->
< section id = "passwords" hidden >
< h1 > Passwords< / h1 >
< p class = "lede" > Local password vault. Set once, unlocked with a master password. Encrypted at rest; nothing leaves your machine.< / p >
<!-- State A: no vault yet — set up -->
< div id = "pwSetup" hidden >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:10px" >
< div class = "txt" > < div class = "t" > Master password< / div >
< div class = "d" > Used to unlock the vault every session. This is separate from your Ariadne wallet passphrase — memorize it, we can't recover it.< / div > < / div >
< div class = "addeng" > < input id = "pwSetupPw1" type = "password" placeholder = "Master password" > < input id = "pwSetupPw2" type = "password" placeholder = "Confirm" > < / div >
< / div >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:10px" >
< div class = "txt" > < div class = "t" > Seed for deterministic passwords< / div >
< div class = "d" > The "Generate" button in an entry derives a password from this seed. Same seed on another device → same passwords for the same site + username.< / div > < / div >
< div style = "display:flex;flex-direction:column;gap:6px" >
< label class = "polrow" > < input type = "radio" name = "pwSeedSource" value = "mnemonic" checked > < span > < b > Use my Ariadne wallet mnemonic< / b > < span class = "pmuted" > — unified identity, one seed to back up< / span > < / span > < / label >
< label class = "polrow" > < input type = "radio" name = "pwSeedSource" value = "generate" > < span > < b > Generate a new independent seed< / b > < span class = "pmuted" > — isolated from any BCH funds< / span > < / span > < / label >
< / div >
< textarea id = "pwSetupMnemonic" placeholder = "12 or 24 BIP39 words separated by spaces" rows = "3" style = "background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:13px;font-family:ui-monospace,monospace;outline:none;resize:vertical" > < / textarea >
< div class = "pmuted" style = "font-size:12px" > The mnemonic is used only to derive the password-purpose subtree (m/1381'/0'). It is not stored — only the derived subtree key is persisted, encrypted with your master password.< / div >
< / div >
< div class = "row" style = "justify-content:flex-end" >
< button id = "pwSetupBtn" class = "btn" type = "button" > Create vault< / button >
< / div >
< / div >
<!-- State B: vault exists but locked -->
< div id = "pwLocked" hidden >
< div class = "row" >
< div class = "txt" > < div class = "t" > Unlock vault< / div > < div class = "d" > Enter your master password to view or add entries.< / div > < / div >
< div class = "ctl" style = "align-items:stretch" > < input id = "pwUnlockPw" type = "password" placeholder = "Master password" > < button id = "pwUnlockBtn" class = "btn" type = "button" > Unlock< / button > < / div >
< / div >
< div id = "pwUnlockErr" class = "pmuted" style = "color:#f6768a;font-size:12.5px;margin-top:4px" hidden > < / div >
< / div >
<!-- State C: vault unlocked -->
< div id = "pwUnlocked" hidden >
< div class = "row" style = "justify-content:space-between" >
< div class = "txt" > < div class = "t" > Your passwords< / div > < div class = "d" > Reveal, copy, or edit any entry. The vault re-locks when Theseus quits.< / div > < / div >
< button id = "pwLockBtn" class = "btn" type = "button" > Lock now< / button >
< / div >
< div id = "pwList" > < / div >
< h2 class = "sub" > Add an entry< / h2 >
< div class = "row" style = "flex-direction:column;align-items:stretch;gap:8px" >
< div class = "addeng" > < input id = "pwAddDomain" placeholder = "Site (e.g. github.com)" > < input id = "pwAddUser" placeholder = "Username or email" > < / div >
< div style = "display:flex;flex-direction:column;gap:6px" >
< label class = "polrow" > < input type = "radio" name = "pwAddKind" value = "generated" checked > < span > < b > Generate deterministically< / b > < span class = "pmuted" > — derived from your seed; same across devices< / span > < / span > < / label >
< label class = "polrow" > < input type = "radio" name = "pwAddKind" value = "literal" > < span > < b > Paste an existing password< / b > < span class = "pmuted" > — for legacy accounts you already set elsewhere< / span > < / span > < / label >
< / div >
< input id = "pwAddLiteral" type = "password" placeholder = "Paste password" hidden >
< div class = "addeng" > < button id = "pwAddPreview" class = "btn" type = "button" style = "flex:none" > Preview< / button > < input id = "pwAddPreviewOut" readonly placeholder = "preview appears here" style = "font-family:ui-monospace,monospace" > < / div >
< div style = "display:flex;justify-content:flex-end" > < button id = "pwAddBtn" class = "btn" type = "button" > Save entry< / button > < / div >
< / div >
< div class = "note" > There is no autofill yet (phase 2). Copy the password from an entry and paste it into the site.< / div >
< / div >
< / section >
<!-- NAMING -->
<!-- Registries moved into General (above). -->
<!-- PERFORMANCE -->
< section id = "performance" hidden >
< h1 > Performance< / h1 >
< p class = "lede" > Keep Theseus light on resources.< / p >
< div class = "row" >
< div class = "txt" > < div class = "t" > Throttle inactive tabs< / div > < div class = "d" > Background and inactive tabs use far less CPU. Recommended.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "backgroundThrottle" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< / section >
<!-- PRIVACY -->
< section id = "privacy" hidden >
< h1 > Privacy< / h1 >
< p class = "lede" > Reduce tracking and lock down device access.< / p >
< div class = "row" >
< div class = "txt" > < div class = "t" > WebRTC IP policy< / div >
< div class = "d" > Controls which IP addresses WebRTC may reveal — the same thing the "WebRTC Network Limiter" extension does, built in. < b > Public interface only< / b > hides your local IP; < b > Disable non-proxied UDP< / b > is strongest. Tor forces the strongest automatically.< / div > < / div >
< div class = "ctl" > < select id = "webrtcMode" >
< option value = "default" > Allow all (default)< / option >
< option value = "public_only" > Public interface only< / option >
< option value = "public_private" > Public + private interfaces< / option >
< option value = "disable_udp" > Disable non-proxied UDP< / option >
< / select > < / div >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Block camera< / div > < div class = "d" > Deny camera by default — also hides its name from fingerprinting.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "blockCamera" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Block microphone< / div > < div class = "d" > Deny microphone by default — also hides its name from fingerprinting.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "blockMicrophone" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Hide media devices< / div > < div class = "d" > Blank the labels and IDs of all cameras, microphones < b > and speakers< / b > from < code > enumerateDevices()< / code > — closes a WebRTC fingerprinting leak, like Firefox.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "hideMediaDevices" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< h2 class = "sub" > Anti-fingerprinting< / h2 >
< p class = "subd" > For each: < b > Show< / b > (real), < b > Hide< / b > (neutral value), < b > Spoof< / b > (a decoy), or < b > Manual< / b > (set your own).< / p >
< div class = "row" >
< div class = "txt" > < div class = "t" > Timezone< / div > < div class = "d" > What sites read via JavaScript (Intl / Date).< / div > < / div >
< div class = "ctl" >
< select id = "timezoneMode" > < option value = "show" > Show real< / option > < option value = "hide" > Hide (UTC)< / option > < option value = "spoof" > Spoof (auto)< / option > < option value = "manual" > Manual…< / option > < / select >
<!-- Native <select> instead of an <input list=""> datalist — the
datalist popup was flaky in Electron and never rendered on some
displays; a real select is unambiguous. -->
< select id = "timezoneValue" hidden >
< option value = "UTC" > UTC< / option >
< option value = "Europe/London" > Europe/London< / option >
< option value = "Europe/Berlin" > Europe/Berlin< / option >
< option value = "Europe/Paris" > Europe/Paris< / option >
< option value = "Europe/Madrid" > Europe/Madrid< / option >
< option value = "Europe/Rome" > Europe/Rome< / option >
< option value = "Europe/Moscow" > Europe/Moscow< / option >
< option value = "America/New_York" > America/New_York< / option >
< option value = "America/Chicago" > America/Chicago< / option >
< option value = "America/Denver" > America/Denver< / option >
< option value = "America/Los_Angeles" > America/Los_Angeles< / option >
< option value = "America/Sao_Paulo" > America/Sao_Paulo< / option >
< option value = "America/Mexico_City" > America/Mexico_City< / option >
< option value = "America/Toronto" > America/Toronto< / option >
< option value = "Asia/Tokyo" > Asia/Tokyo< / option >
< option value = "Asia/Shanghai" > Asia/Shanghai< / option >
< option value = "Asia/Seoul" > Asia/Seoul< / option >
< option value = "Asia/Kolkata" > Asia/Kolkata< / option >
< option value = "Asia/Dubai" > Asia/Dubai< / option >
< option value = "Asia/Singapore" > Asia/Singapore< / option >
< option value = "Asia/Bangkok" > Asia/Bangkok< / option >
< option value = "Australia/Sydney" > Australia/Sydney< / option >
< option value = "Australia/Perth" > Australia/Perth< / option >
< option value = "Africa/Nairobi" > Africa/Nairobi< / option >
< option value = "Africa/Cairo" > Africa/Cairo< / option >
< option value = "Africa/Johannesburg" > Africa/Johannesburg< / option >
< option value = "__other__" > Other…< / option >
< / select >
< input id = "timezoneValueOther" type = "text" placeholder = "IANA zone, e.g. America/Anchorage" hidden >
< / div >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Language< / div > < div class = "d" > navigator.language and the Accept-Language header. < b > Spoof< / b > reports a major world language; < b > Manual< / b > lets you type any locale.< / div > < / div >
< div class = "ctl" >
< select id = "languageMode" > < option value = "show" > Show real< / option > < option value = "hide" > Hide (en-US)< / option > < option value = "spoof" > Spoof (choose)< / option > < option value = "manual" > Manual…< / option > < / select >
< select id = "languageSpoof" hidden >
< option value = "en-US" > English< / option >
< option value = "zh-CN" > Chinese (中文)< / option >
< option value = "es-ES" > Spanish (Español)< / option >
< option value = "hi-IN" > Hindi (हिन्दी)< / option >
< option value = "ar" > Arabic (العربية)< / option >
< option value = "pt-BR" > Portuguese (Português)< / option >
< option value = "ru-RU" > Russian (Русский)< / option >
< option value = "fr-FR" > French (Français)< / option >
< option value = "de-DE" > German (Deutsch)< / option >
< option value = "ja-JP" > Japanese (日本語)< / option >
< / select >
<!-- Native <select> (same reason as timezone above). -->
< select id = "languageValue" hidden >
< option value = "en-US" > English (US) — en-US< / option >
< option value = "en-GB" > English (UK) — en-GB< / option >
< option value = "zh-CN" > Chinese (Simplified) — zh-CN< / option >
< option value = "zh-TW" > Chinese (Traditional) — zh-TW< / option >
< option value = "es-ES" > Spanish (Spain) — es-ES< / option >
< option value = "es-MX" > Spanish (Mexico) — es-MX< / option >
< option value = "pt-BR" > Portuguese (Brazil) — pt-BR< / option >
< option value = "pt-PT" > Portuguese (Portugal) — pt-PT< / option >
< option value = "fr-FR" > French — fr-FR< / option >
< option value = "de-DE" > German — de-DE< / option >
< option value = "it-IT" > Italian — it-IT< / option >
< option value = "nl-NL" > Dutch — nl-NL< / option >
< option value = "ru-RU" > Russian — ru-RU< / option >
< option value = "ja-JP" > Japanese — ja-JP< / option >
< option value = "ko-KR" > Korean — ko-KR< / option >
< option value = "hi-IN" > Hindi — hi-IN< / option >
< option value = "ar" > Arabic — ar< / option >
< option value = "tr-TR" > Turkish — tr-TR< / option >
< option value = "pl-PL" > Polish — pl-PL< / option >
< option value = "uk-UA" > Ukrainian — uk-UA< / option >
< option value = "sv-SE" > Swedish — sv-SE< / option >
< option value = "fi-FI" > Finnish — fi-FI< / option >
< option value = "el-GR" > Greek — el-GR< / option >
< option value = "he-IL" > Hebrew — he-IL< / option >
< option value = "vi-VN" > Vietnamese — vi-VN< / option >
< option value = "th-TH" > Thai — th-TH< / option >
< option value = "id-ID" > Indonesian — id-ID< / option >
< option value = "__other__" > Other…< / option >
< / select >
< input id = "languageValueOther" type = "text" placeholder = "BCP-47 tag, e.g. cs-CZ" hidden >
< / div >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Location< / div > < div class = "d" > < b > Hide< / b > denies geolocation; < b > Spoof< / b > reports a chosen region; < b > Manual< / b > reports exact coordinates.< / div > < / div >
< div class = "ctl" >
< select id = "locationMode" > < option value = "show" > Show real< / option > < option value = "hide" > Hide (block)< / option > < option value = "spoof" > Spoof (region)< / option > < option value = "manual" > Manual…< / option > < / select >
< select id = "locationRegion" hidden >
< option value = "europe" > Europe< / option >
< option value = "asia" > Asia< / option >
< option value = "north_america" > North America< / option >
< option value = "south_america" > South America< / option >
< option value = "africa" > Africa< / option >
< option value = "middle_east" > Middle East< / option >
< option value = "australia" > Australia< / option >
< / select >
<!-- Manual location = pick a city from a native select; its lat/lon
are looked up client-side (see CITIES below) and written to
locationLat / locationLon settings, which drive the
navigator.geolocation override in main.js. The raw lat/lon
inputs used to sit here but nobody types coordinates by hand;
the city picker gives the same override with a single click. -->
< select id = "locationCity" hidden >
< option value = "" > Pick a city…< / option >
< option value = "london" > London< / option >
< option value = "berlin" > Berlin< / option >
< option value = "paris" > Paris< / option >
< option value = "madrid" > Madrid< / option >
< option value = "rome" > Rome< / option >
< option value = "moscow" > Moscow< / option >
< option value = "istanbul" > Istanbul< / option >
< option value = "dubai" > Dubai< / option >
< option value = "mumbai" > Mumbai< / option >
< option value = "singapore" > Singapore< / option >
< option value = "bangkok" > Bangkok< / option >
< option value = "shanghai" > Shanghai< / option >
< option value = "tokyo" > Tokyo< / option >
< option value = "seoul" > Seoul< / option >
< option value = "sydney" > Sydney< / option >
< option value = "new_york" > New York< / option >
< option value = "los_angeles" > Los Angeles< / option >
< option value = "chicago" > Chicago< / option >
< option value = "toronto" > Toronto< / option >
< option value = "mexico_city" > Mexico City< / option >
< option value = "sao_paulo" > São Paulo< / option >
< option value = "buenos_aires" > Buenos Aires< / option >
< option value = "cairo" > Cairo< / option >
< option value = "nairobi" > Nairobi< / option >
< option value = "johannesburg" > Johannesburg< / option >
< option value = "__other__" > Other…< / option >
< / select >
< div id = "locationOther" class = "coords" hidden >
< input id = "locationLatOther" type = "number" step = "0.0001" placeholder = "lat" style = "width:100px" >
< input id = "locationLonOther" type = "number" step = "0.0001" placeholder = "lon" style = "width:100px" >
< / div >
< / div >
< / div >
< div class = "note" > These reduce tracking and hide your IP, but a custom browser can still be fingerprinted. For maximum anonymity, use the Tor Browser.< / div >
< h2 class = "sub" > Storage< / h2 >
< p class = "subd" > By default Theseus keeps < b > nothing< / b > across sessions — everything toggled on here is wiped when you quit. Untoggle a bucket to keep it (e.g. cookies to stay signed in on trusted sites).< / p >
< div class = "row" >
< div class = "txt" > < div class = "t" > Clear cookies on quit< / div > < div class = "d" > Drops session + persistent cookies. You'll sign in again next launch.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "clearCookiesOnQuit" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Clear HTTP cache on quit< / div > < div class = "d" > Drops cached images / scripts / stylesheets. Sites re-download; small disk win.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "clearCacheOnQuit" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Clear site storage on quit< / div > < div class = "d" > Drops localStorage, IndexedDB, service workers, and the cache API. Web-app state resets.< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "clearStorageOnQuit" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< div class = "row" >
< div class = "txt" > < div class = "t" > Clear history on quit< / div > < div class = "d" > Drops navigation history + the saved-tabs session file (overrides "Reopen previous tabs").< / div > < / div >
< label class = "sw" > < input type = "checkbox" id = "clearHistoryOnQuit" > < span class = "track" > < span class = "knob" > < / span > < / span > < / label >
< / div >
< div class = "row" style = "justify-content:flex-end" >
< button id = "clearNow" class = "btn" type = "button" > Clear all now< / button >
< / div >
< div class = "note" > There is no persistent password manager — passwords are never stored to disk regardless of these toggles. Use a dedicated password manager (Bitwarden, KeePass, etc.).< / div >
< / section >
<!-- ADD - ONS -->
< section id = "addons" hidden >
< h1 > Extensions< / h1 >
< p class = "lede" > Small modules that add capabilities to Theseus. Extensions live as folders under
2026-09-21 01:55:25 +02:00
< code style = "background:transparent;border:none;padding:0" id = "addonsPathHint" > %APPDATA%\Theseus\extensions\< / code > . Drop a folder in, restart, it's live.
2026-09-08 01:42:41 +02:00
Bundled reference extensions (like the Notepad) are copied there on first run — you can edit or remove them
without losing anything the browser needs.< / p >
< div class = "row" style = "justify-content:flex-end;gap:8px" >
2026-09-20 15:33:33 +02:00
< a class = "btn" style = "text-decoration:none;display:inline-flex;align-items:center" href = "https://theseus.x/extensions/" target = "_blank" rel = "noopener" title = "What ships, current signed versions, and how updates are verified" > Extensions ↗< / a >
2026-09-08 02:27:36 +02:00
< button id = "addonsCheckUpdates" class = "btn" type = "button" > Check for updates< / button >
2026-09-08 01:42:41 +02:00
< button id = "addonsReload" class = "btn" type = "button" > Reload< / button >
< button id = "addonsOpenDir" class = "btn" type = "button" > Open extensions folder< / button >
< / div >
2026-09-08 02:27:36 +02:00
< div id = "addonsUpdStatus" class = "pmuted" style = "font-size:12.5px;margin-top:6px;text-align:right" > —< / div >
2026-09-08 01:42:41 +02:00
< h2 class = "sub" style = "border-top:0;padding-top:0;margin-top:1.5rem" > Installed< / h2 >
< div id = "addonsList" > < div class = "d" style = "color:var(--dim)" > Loading…< / div > < / div >
2026-09-21 03:28:04 +02:00
< div class = "note" style = "margin-top:1.5rem" > To install a new extension, browse the catalogue at
< a href = "https://theseus.x/extensions/" target = "_blank" rel = "noopener" > theseus.x/extensions< / a >
and use the Install button there. Theseus checks each publisher's signature against the name's current
owner on chain before installing — the relay can't substitute code under a trusted name. Extensions run
with full app access — treat installing one like installing an unsigned executable, and only load
extensions whose source you trust.< / div >
2026-09-08 01:42:41 +02:00
< / section >
< / div >
< / div >
< script >
const C = window.cfg;
// sidebar navigation
2026-09-08 02:23:27 +02:00
// "naming" is intentionally absent: the Registries section was folded
// into General in 0.3.21. Leaving the id in this list threw a
// TypeError inside showSection (getElementById("naming") → null),
// which broke every click after General/Search/Passwords.
2026-09-08 23:00:41 +02:00
const sections = ["general", "search", "passwords", "performance", "privacy", "plugins", "addons"];
2026-09-08 01:42:41 +02:00
function showSection(sec) {
if (!sections.includes(sec)) return;
document.querySelectorAll(".side a").forEach((x) => x.classList.toggle("active", x.dataset.sec === sec));
for (const s of sections) document.getElementById(s).hidden = (s !== sec);
}
document.querySelectorAll(".side a").forEach((a) => a.onclick = () => showSection(a.dataset.sec));
// Main-process asks us to jump to a section (e.g. picker's "Search settings…"
// click routes to the Search section instead of the General default).
if (C & & C.onFocusSection) C.onFocusSection((sec) => showSection(sec));
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
2026-09-09 10:33:21 +02:00
// Land on the section named by `settings.html#< sec > ` when opened via main
// (e.g. Aegis's "Set up vault" button → open-settings with "passwords").
try {
const initSec = String(location.hash || "").replace(/^#/, "").toLowerCase();
if (initSec) showSection(initSec);
} catch {}
2026-09-08 01:42:41 +02:00
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices",
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit"];
C.get().then((s) => {
for (const k of TOGGLES) {
const el = document.getElementById(k); if (!el) continue;
el.checked = !!s[k];
el.addEventListener("change", () => C.set(k, el.checked));
}
// search engines: default picker + custom-engine list + add form
const sel = document.getElementById("searchEngine");
const esc = (s) => String(s || "").replace(/< /g, "< ");
2026-09-08 13:12:55 +02:00
// Try the primary favicon URL (Google's /s2/favicons — set in main.js);
// fall back to DuckDuckGo's icons.duckduckgo.com service if that 404s,
// then to the emoji sym as final fallback so the row is never blank.
const engIcon = (e) => {
let alt = "";
try {
const u = new URL(e.favicon || "https://x");
const domain = u.searchParams.get("domain") || u.hostname;
if (domain & & domain !== "x") alt = `https://icons.duckduckgo.com/ip3/${domain}.ico`;
} catch {}
const sym = (e.sym || "🔍").replace(/'/g, "' ");
const fallbackChain = alt
? `this.onerror=null;this.src='${alt}';this.setAttribute('data-tried','1');this.onerror=function(){this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))};`
: `this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))`;
return e.favicon
? `< img class = "ei" src = "${esc(e.favicon)}" onerror = "${fallbackChain}" > `
: `< span class = "es" > ${sym}< / span > `;
};
2026-09-08 01:42:41 +02:00
const ENGINE_KINDS = [
{ key: "search", label: "Search engines" },
{ key: "llm", label: "AI answer engines" },
];
function renderEngines(d) {
// default engine: pick from the ENABLED engines (those shown in the dropdown)
const enabled = d.engines.filter((e) => e.enabled);
// group the dropdown too, so search and LLM don't intermix
sel.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
const opts = enabled.filter((e) => (e.kind || "search") === key)
.map((e) => `< option value = "${e.id}" > ${(e.sym ? e.sym + " " : "")}${esc(e.name)}< / option > `).join("");
return opts ? `< optgroup label = "${label}" > ${opts}< / optgroup > ` : "";
}).join("");
sel.value = d.current;
// Main list = engines the user has INSTALLED. The toggle only flips
// enabled/disabled — the row STAYS. Right-click a row → "Remove from
// list" is what actually removes an engine (back to the catalog for
// built-ins, permanently for customs).
const list = document.getElementById("engineList");
const installed = d.engines.filter((e) => e.installed);
const rowFor = (e) => `< div class = "eng${e.enabled ? " " : " off " } " data-id = "${e.id}" data-kind = "${e.kind || " search " } " data-builtin = "${e.builtin ? 1 : 0}" draggable = "true" > ` +
`< span class = "grip" title = "Drag to reorder" > ⠿< / span > ` +
`< span class = "eic" > ${engIcon(e)}< / span > < span class = "enm" > ${esc(e.name)}< / span > ` +
`< label class = "sw sm" title = "${e.enabled ? " Turn off " : " Turn on " } " > < input type = "checkbox" data-id = "${e.id}" $ { e . enabled ? " checked " : " " } > < span class = "track" > < span class = "knob" > < / span > < / span > < / label > ` +
`< / div > `;
list.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
const rows = installed.filter((e) => (e.kind || "search") === key).map(rowFor).join("");
if (!rows) return "";
return `< div class = "ehdr" > ${label}< / div > ${rows}`;
}).join("");
// Two catalog panes, split by tier — filtered by !installed now, not
// !enabled (a toggled-off engine stays in the enabled list, not here):
// catalog — curated first-class built-ins the user hasn't installed
// extra — wider bank, filtered live by the "Discover more" search box
const cat = document.getElementById("catalogList");
const extra = document.getElementById("extraList");
const filterInput = document.getElementById("engineFilter");
const catRow = (e) => `< div class = "cat" data-id = "${e.id}" > ` +
`< span class = "eic" > ${engIcon(e)}< / span > ` +
`< span class = "enm" > ${esc(e.name)}< / span > ` +
`< span class = "kind" > ${(e.kind || "search") === "llm" ? "AI" : "Search"}< / span > ` +
`< button class = "add" data-add = "${e.id}" > + Add< / button > < / div > `;
const uninstalled = d.engines.filter((e) => e.builtin & & !e.installed);
const catalogOff = uninstalled.filter((e) => (e.tier || "catalog") === "catalog");
const extraOff = uninstalled.filter((e) => e.tier === "extra");
if (cat) {
cat.innerHTML = catalogOff.length
? catalogOff.map(catRow).join("")
: `< div class = "cempty2" > All curated engines are already in your list. Discover more below or add a custom URL.< / div > `;
cat.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines));
}
if (extra) {
const paintExtras = (q) => {
const filt = String(q || "").trim().toLowerCase();
const shown = filt ? extraOff.filter((e) => e.name.toLowerCase().includes(filt)) : extraOff;
extra.innerHTML = shown.length
? shown.map(catRow).join("")
: `< div class = "cempty2" > ${filt ? "No engines match that filter." : "All discoverable engines are already in your list."}< / div > `;
extra.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines));
};
paintExtras(filterInput ? filterInput.value : "");
if (filterInput & & !filterInput.dataset.wired) {
filterInput.dataset.wired = "1";
filterInput.addEventListener("input", () => paintExtras(filterInput.value));
}
}
// Toggle: pure on/off in the enabled set — the row stays visible either way.
list.querySelectorAll('input[type="checkbox"]').forEach((cb) => cb.onchange = () => C.setEngineEnabled(cb.dataset.id, cb.checked).then(renderEngines));
// Right-click any row → context menu with "Remove from list" (moves a
// built-in back to the catalog; deletes a custom entirely).
list.querySelectorAll(".eng").forEach((row) => {
row.addEventListener("contextmenu", (e) => {
e.preventDefault();
openEngineMenu(row, e.clientX, e.clientY);
});
});
// drag-and-drop reorder — same-kind only (dropping a Search engine into
// the LLM section would just re-group visually on next render, so we
// reject cross-kind drags outright).
let dragId = null;
let dragKind = null;
const sameKind = (row) => row.dataset.kind === dragKind;
list.querySelectorAll(".eng").forEach((row) => {
row.addEventListener("dragstart", (e) => { dragId = row.dataset.id; dragKind = row.dataset.kind; e.dataTransfer.effectAllowed = "move"; row.classList.add("dragging"); });
row.addEventListener("dragend", () => { row.classList.remove("dragging"); list.querySelectorAll(".eng").forEach((r) => r.classList.remove("over")); });
row.addEventListener("dragover", (e) => {
if (!sameKind(row)) { e.dataTransfer.dropEffect = "none"; return; }
e.preventDefault(); e.dataTransfer.dropEffect = "move";
if (row.dataset.id !== dragId) row.classList.add("over");
});
row.addEventListener("dragleave", () => row.classList.remove("over"));
row.addEventListener("drop", (e) => {
e.preventDefault(); row.classList.remove("over");
if (!dragId || dragId === row.dataset.id || !sameKind(row)) return;
const ids = [...list.querySelectorAll(".eng")].map((el) => el.dataset.id);
const from = ids.indexOf(dragId), to = ids.indexOf(row.dataset.id);
ids.splice(from, 1); ids.splice(to, 0, dragId);
C.setEngineOrder(ids).then(renderEngines);
});
});
}
// Floating right-click menu for an engine row. Only one open at a time.
let ctxOpen = null;
function closeEngineMenu() { if (ctxOpen) { ctxOpen.remove(); ctxOpen = null; } }
function openEngineMenu(row, x, y) {
closeEngineMenu();
const id = row.dataset.id;
const builtin = row.dataset.builtin === "1";
const m = document.createElement("div");
m.className = "ctxmenu";
m.innerHTML = `< div class = "mi danger" data-act = "remove" > Remove from list< / div > `;
document.body.appendChild(m);
// Position, keeping the menu inside the viewport.
const rect = m.getBoundingClientRect();
const vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
m.style.left = Math.min(x, vw - rect.width - 6) + "px";
m.style.top = Math.min(y, vh - rect.height - 6) + "px";
m.querySelector('[data-act="remove"]').onclick = () => {
closeEngineMenu();
const call = builtin ? C.removeFromList(id) : C.removeEngine(id);
call.then(renderEngines);
};
ctxOpen = m;
setTimeout(() => {
const off = (ev) => { if (!m.contains(ev.target)) { closeEngineMenu(); document.removeEventListener("mousedown", off); document.removeEventListener("keydown", esc); } };
const esc = (ev) => { if (ev.key === "Escape") { closeEngineMenu(); document.removeEventListener("mousedown", off); document.removeEventListener("keydown", esc); } };
document.addEventListener("mousedown", off);
document.addEventListener("keydown", esc);
}, 0);
}
sel.onchange = () => C.set("searchEngine", sel.value);
// appearance (theme) — three visual cards: system | light | dark. Any
// unrecognised saved value falls back to "system" (follow the OS).
const th = document.getElementById("theme");
let themeValue = ["system", "light", "dark"].includes(s.theme) ? s.theme : "system";
const paintTheme = () => th.querySelectorAll(".tc").forEach((b) => {
const on = b.dataset.val === themeValue;
b.classList.toggle("on", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
paintTheme();
th.querySelectorAll(".tc").forEach((b) => b.onclick = () => {
themeValue = b.dataset.val; paintTheme(); C.set("theme", themeValue);
});
// WebRTC IP policy
const wm = document.getElementById("webrtcMode");
wm.value = s.webrtcMode || "public_only";
wm.onchange = () => C.set("webrtcMode", wm.value);
// Toolbar sizing dropdowns removed — the toolbar drag-handles do the
// same job in-place. The stored urlBarSize / searchBoxSize + the drag-
// set widthPx settings still exist in main; they just have no UI here.
document.getElementById("engAdd").onclick = () => {
const name = document.getElementById("engName").value.trim();
const url = document.getElementById("engUrl").value.trim();
const sym = document.getElementById("engSym").value.trim();
if (!name || !url.includes("%s")) { alert("Enter a name and a URL containing %s (where the query goes)."); return; }
C.addEngine({ name, url, sym }).then((d) => {
document.getElementById("engName").value = ""; document.getElementById("engUrl").value = ""; document.getElementById("engSym").value = "";
renderEngines(d);
});
};
// "+ Add search engine" toggles the catalog panel below the enabled list.
const catBtn = document.getElementById("engAddBtn");
const catBox = document.getElementById("engineCatalog");
if (catBtn & & catBox) {
catBtn.onclick = () => {
const open = catBox.hidden;
catBox.hidden = !open;
catBtn.textContent = open ? "− Hide catalog" : "+ Add search engine";
};
}
C.engines().then(renderEngines);
// anti-fingerprinting mode selectors, with value field(s) shown on "manual"
const bind = (mode, showValIf, apply) => {
const m = document.getElementById(mode);
m.value = s[mode] || "show";
apply(m.value === "manual");
m.addEventListener("change", () => { C.set(mode, m.value); apply(m.value === "manual"); });
};
// Value fields: < select > for tz + lang manual mode, < select > city for
// location manual mode. Each has an "Other…" sentinel at the end that
// reveals a text input so the user can enter a value not in the built-in
// list (any IANA zone, any BCP-47 locale). selectWithOther handles the
// round-trip: if a saved value isn't in the predefined options, "Other"
// is auto-selected on load and the input pre-fills with that value.
function selectWithOther(selectId, otherInputId, settingsKey = selectId) {
const sel = document.getElementById(selectId);
const inp = document.getElementById(otherInputId);
const saved = s[settingsKey] ?? "";
const known = new Set([...sel.options].map((o) => o.value).filter((v) => v & & v !== "__other__"));
const isCustom = saved & & !known.has(saved);
sel.value = isCustom ? "__other__" : saved;
if (isCustom) inp.value = saved;
const applyVis = () => { inp.hidden = sel.value !== "__other__"; };
applyVis();
sel.addEventListener("change", () => {
if (sel.value === "__other__") { applyVis(); setTimeout(() => inp.focus(), 0); return; }
C.set(settingsKey, sel.value); applyVis();
});
inp.addEventListener("change", () => { const v = String(inp.value).trim(); if (v) C.set(settingsKey, v); });
return sel;
}
const tzV = selectWithOther("timezoneValue", "timezoneValueOther");
const lgV = selectWithOther("languageValue", "languageValueOther");
const tzOther = document.getElementById("timezoneValueOther");
const lgOther = document.getElementById("languageValueOther");
// "Manual" mode reveals both the select AND the Other input (if Other was picked).
const applyTzVis = (manual) => { tzV.hidden = !manual; tzOther.hidden = !manual || tzV.value !== "__other__"; };
bind("timezoneMode", null, applyTzVis);
// language: fixed dropdown when spoofing (top 10), broader dropdown when manual
const lngMode = document.getElementById("languageMode");
const lngSpoof = document.getElementById("languageSpoof");
lngSpoof.value = s.languageSpoof || "en-US";
lngSpoof.addEventListener("change", () => C.set("languageSpoof", lngSpoof.value));
const applyLng = () => {
lngSpoof.hidden = lngMode.value !== "spoof";
lgV.hidden = lngMode.value !== "manual";
lgOther.hidden = lngMode.value !== "manual" || lgV.value !== "__other__";
};
lngMode.value = s.languageMode || "show";
applyLng();
lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); });
// location: region dropdown when spoofing, city dropdown when manual.
// Manual writes lat/lon into settings via the CITIES map below; the raw
// coord inputs were dropped from the UI (nobody types them by hand).
const locMode = document.getElementById("locationMode");
const locRegion = document.getElementById("locationRegion");
const locCity = document.getElementById("locationCity");
locRegion.value = s.locationRegion || "europe";
locRegion.addEventListener("change", () => C.set("locationRegion", locRegion.value));
// Location manual reveals the city select; picking "Other…" reveals a
// lat/lon pair, whose values are pushed to locationLat/Lon settings.
const locOther = document.getElementById("locationOther");
const locLatOther = document.getElementById("locationLatOther");
const locLonOther = document.getElementById("locationLonOther");
const applyLoc = () => {
locRegion.hidden = locMode.value !== "spoof";
locCity.hidden = locMode.value !== "manual";
locOther.hidden = locMode.value !== "manual" || locCity.value !== "__other__";
};
locMode.value = s.locationMode || "show";
applyLoc();
locMode.addEventListener("change", () => { C.set("locationMode", locMode.value); applyLoc(); });
locCity.addEventListener("change", applyLoc);
// Prefill Other lat/lon inputs from saved settings.
locLatOther.value = s.locationLat ?? "";
locLonOther.value = s.locationLon ?? "";
const saveOtherCoords = () => {
const la = String(locLatOther.value).trim();
const lo = String(locLonOther.value).trim();
if (la !== "") C.set("locationLat", la);
if (lo !== "") C.set("locationLon", lo);
C.set("locationCity", "__other__");
};
locLatOther.addEventListener("change", saveOtherCoords);
locLonOther.addEventListener("change", saveOtherCoords);
// If the previously-saved city was "__other__", set the select to it so the
// Other inputs stay visible on reload.
if (s.locationCity === "__other__") locCity.value = "__other__";
// Picking a city writes its lat/lon to settings — that's what
// navigator.geolocation returns to pages once "manual" mode is active.
const CITIES = {
london:[51.5074,-0.1278], berlin:[52.52,13.405], paris:[48.8566,2.3522],
madrid:[40.4168,-3.7038], rome:[41.9028,12.4964], moscow:[55.7558,37.6173],
istanbul:[41.0082,28.9784], dubai:[25.2048,55.2708], mumbai:[19.076,72.8777],
singapore:[1.3521,103.8198], bangkok:[13.7563,100.5018], shanghai:[31.2304,121.4737],
tokyo:[35.6762,139.6503], seoul:[37.5665,126.978], sydney:[-33.8688,151.2093],
new_york:[40.7128,-74.006], los_angeles:[34.0522,-118.2437], chicago:[41.8781,-87.6298],
toronto:[43.6532,-79.3832], mexico_city:[19.4326,-99.1332], sao_paulo:[-23.5505,-46.6333],
buenos_aires:[-34.6037,-58.3816], cairo:[30.0444,31.2357], nairobi:[-1.2921,36.8219],
johannesburg:[-26.2041,28.0473],
};
// Restore prior city selection when possible by matching stored lat/lon.
const restoreCity = () => {
const la = Number(s.locationLat), lo = Number(s.locationLon);
for (const [key, [x, y]] of Object.entries(CITIES))
if (Math.abs(x - la) < 0.01 & & Math . abs ( y - lo ) < 0 . 01 ) { locCity . value = key; return ; }
};
restoreCity();
locCity.addEventListener("change", () => {
const c = CITIES[locCity.value]; if (!c) return;
C.set("locationLat", String(c[0]));
C.set("locationLon", String(c[1]));
C.set("locationCity", locCity.value); // save the pick so we can restore it later
});
// Storage: "Clear all now" wipes everything the toggles cover, without
// waiting for quit. Confirm first — this signs the user out of everything.
const clrBtn = document.getElementById("clearNow");
if (clrBtn) clrBtn.onclick = async () => {
if (!confirm("Clear cookies, cache, site storage, and history now?\n\nYou'll be signed out of everything and open tabs won't be restored.")) return;
clrBtn.disabled = true; clrBtn.textContent = "Clearing…";
try {
await C.clearBrowsingData({ cookies: true, cache: true, storage: true, history: true });
clrBtn.textContent = "Cleared ✓";
} catch (e) { clrBtn.textContent = "Clear failed"; console.error(e); }
setTimeout(() => { clrBtn.textContent = "Clear all now"; clrBtn.disabled = false; }, 1600);
};
// ---- Naming section: BCNR/ICANN collision policy + remembered choices ----
function refreshCollisions() {
C.collisionState().then((cs) => {
// pick the current radio
document.querySelectorAll('input[name="collisionPolicy"]').forEach((r) => { r.checked = (r.value === cs.policy); });
const nn = Object.keys(cs.byName || {}).length, tn = Object.keys(cs.byTld || {}).length;
const bc = (cs.bcnrTlds || []).length;
document.getElementById("colSummary").textContent =
`Remembered: ${nn} name${nn === 1 ? "" : "s"}, ${tn} TLD${tn === 1 ? "" : "s"}. `
+ `BCNR-native TLDs on chain: ${bc}.`;
}).catch(() => {});
}
refreshCollisions();
document.querySelectorAll('input[name="collisionPolicy"]').forEach((r) => {
r.addEventListener("change", () => { if (r.checked) C.setCollisionPolicy(r.value).then(refreshCollisions); });
});
document.getElementById("resetCollisions").onclick = () => {
C.resetCollisions().then(refreshCollisions);
};
2026-09-09 00:51:05 +02:00
// ---- Developer tools dock position --------------------------------------
// The active radio reflects the current setting; changing it just calls
// C.set — the F12 handler in main.js reads settings.devToolsDock each
// time DevTools opens, so no restart is needed for the choice to apply.
const devToolsDock = s.devToolsDock || "bottom";
document.querySelectorAll('input[name="devToolsDock"]').forEach((r) => {
r.checked = (r.value === devToolsDock);
r.addEventListener("change", () => { if (r.checked) C.set("devToolsDock", r.value); });
});
2026-09-08 01:42:41 +02:00
// ---- Ariadne's Thread system-wide resolver toggle -----------------------
// Ariadne runs as two elevated Scheduled Tasks ("BNS Resolver Daemon" +
// "BNS Sia Bridge"). Toggling requires admin — main spawns an elevated
// powershell for start/stop, which prompts UAC once per action.
const arStatus = document.getElementById("ariadneStatus");
const arStart = document.getElementById("ariadneStart");
const arStop = document.getElementById("ariadneStop");
const arInstall = document.getElementById("ariadneInstall");
const arUpdate = document.getElementById("ariadneUpdate");
const arUninstall = document.getElementById("ariadneUninstall");
const arRefresh = document.getElementById("ariadneRefresh");
const arMissing = document.getElementById("ariadneMissing");
async function refreshAriadne() {
arStatus.textContent = "checking…";
arStart.hidden = arStop.hidden = arInstall.hidden = arUpdate.hidden = arUninstall.hidden = arMissing.hidden = true;
try {
const r = await C.ariadneState();
const verSuffix = r.installedVersion ? ` (v${r.installedVersion})` : "";
const bundledSuffix = r.bundledVersion ? ` (bundled v${r.bundledVersion})` : "";
if (r.state === "running") {
2026-09-08 01:51:03 +02:00
arStatus.innerHTML = `Status: < b style = "color:var(--acid-text)" > running< / b > ${verSuffix} — every browser on this machine resolves BCDN names.`;
2026-09-08 01:42:41 +02:00
arStop.hidden = false; arUninstall.hidden = !r.hasUninstaller;
if (r.canUpdate) arUpdate.hidden = false;
} else if (r.state === "stopped") {
arStatus.innerHTML = `Status: < b > stopped< / b > ${verSuffix} — only Theseus resolves BCDN names; other browsers won't.`;
arStart.hidden = false; arUninstall.hidden = !r.hasUninstaller;
if (r.canUpdate) arUpdate.hidden = false;
} else {
arStatus.innerHTML = `Status: < b > not installed< / b > on this machine.${bundledSuffix}`;
arInstall.hidden = false;
arMissing.hidden = false;
}
} catch (e) { arStatus.textContent = "Status check failed: " + (e?.message || e); }
}
// Wrap each async action so the button disables during work + always
// refreshes state after (success OR failure), so the UI never lies.
function wire(btn, action, busyLabel) {
btn.onclick = async () => {
const orig = btn.textContent;
btn.disabled = true; btn.textContent = busyLabel;
try { await action(); } catch {}
finally { btn.disabled = false; btn.textContent = orig; refreshAriadne(); }
};
}
wire(arStart, () => C.ariadneToggle(true), "Starting…");
wire(arStop, () => C.ariadneToggle(false), "Stopping…");
wire(arInstall, () => C.ariadneInstall(), "Installing… (accept the UAC prompt)");
wire(arUpdate, () => C.ariadneUpdate(), "Updating… (accept the UAC prompt)");
wire(arUninstall, () => C.ariadneUninstall(), "Uninstalling… (accept the UAC prompt)");
arRefresh.onclick = refreshAriadne;
refreshAriadne();
2026-09-08 18:17:44 +02:00
// ---- Aegis update card --------------------------------------------------
2026-09-08 18:09:55 +02:00
// Aegis is a bundled add-on but its updates are shipped OTA via the signed
// add-on update endpoint, so users can get new wallet versions without
// updating Theseus. The same addons-check-updates IPC the Extensions page
2026-09-08 18:17:44 +02:00
// uses is filtered for the aegis id so this card only speaks for Aegis.
// Legacy id "bchwallet" is also matched for upgrades from pre-rename
// installs (0.3.x); post-migration the entry lives at id="aegis".
2026-09-08 18:09:55 +02:00
const aegisStatus = document.getElementById("aegisStatus");
const aegisCheck = document.getElementById("aegisCheck");
const aegisApply = document.getElementById("aegisApply");
2026-09-08 18:17:44 +02:00
const isAegis = (a) => a & & (a.id === "aegis" || a.id === "bchwallet");
2026-09-08 18:26:58 +02:00
// addons-list returns {installed:[...], sidebarPanels:[...]}
// addons-check-updates returns {report, skipped, staged:[...]}
// addons-list-staged returns a flat [...]. Normalise both call sites.
2026-09-08 18:09:55 +02:00
async function refreshAegis() {
aegisApply.hidden = true;
try {
2026-09-08 18:26:58 +02:00
const [listRes, stagedRes] = await Promise.all([
C.listAddons ? C.listAddons() : Promise.resolve({}),
2026-09-08 18:09:55 +02:00
C.listStagedAddonUpdates ? C.listStagedAddonUpdates() : Promise.resolve([]),
]);
2026-09-08 18:26:58 +02:00
const installed = (listRes & & listRes.installed) || [];
const staged = Array.isArray(stagedRes) ? stagedRes : [];
const cur = installed.find(isAegis);
const upd = staged.find(isAegis);
2026-09-08 18:09:55 +02:00
if (!cur) { aegisStatus.textContent = "Aegis isn't installed. Reinstall Theseus to add it back, or open Settings > Extensions to load it manually."; return; }
const curVer = cur.version || "?";
if (upd & & upd.version) {
2026-09-21 02:43:10 +02:00
aegisStatus.innerHTML = `< b style = "color:var(--acid-text)" > v${upd.version}< / b > ready — click Apply update now to switch without restarting Theseus. You're on v${curVer}.`;
2026-09-08 18:09:55 +02:00
aegisApply.hidden = false;
} else {
aegisStatus.textContent = `You're on v${curVer}. Updates arrive over-the-air; the next check runs at boot and every 6h.`;
}
} catch (e) { aegisStatus.textContent = "Status check failed: " + (e?.message || e); }
}
aegisCheck.onclick = async () => {
const orig = aegisCheck.textContent;
aegisCheck.disabled = true; aegisCheck.textContent = "Checking…";
aegisStatus.textContent = "Checking…";
try {
2026-09-08 18:26:58 +02:00
const res = await (C.checkAddonUpdates ? C.checkAddonUpdates() : Promise.resolve({}));
const staged = (res & & Array.isArray(res.staged)) ? res.staged : [];
const upd = staged.find(isAegis);
2026-09-08 18:09:55 +02:00
if (!upd) { aegisStatus.textContent = "You're on the latest Aegis."; }
// refresh will populate everything else + toggle the Apply button
refreshAegis();
} catch (e) { aegisStatus.textContent = "Check failed: " + (e?.message || e); }
finally { aegisCheck.disabled = false; aegisCheck.textContent = orig; }
};
2026-09-21 02:43:10 +02:00
// Apply staged update without restarting Theseus. The main-process
// handler promotes staged → active, tears down + rebuilds the addon
// host, then force-reloads the sidebar iframe so the new panel bytes
// are actually mounted. Falls back to a full restart if the IPC isn't
// wired (older Theseus builds).
aegisApply.onclick = async () => {
const orig = aegisApply.textContent;
aegisApply.disabled = true; aegisApply.textContent = "Applying…";
try {
if (C & & C.applyStagedAddons) {
const r = await C.applyStagedAddons();
if (r & & r.ok === false) throw new Error(r.err || "apply failed");
aegisStatus.textContent = "Update applied. Reopen the Aegis panel to see the new version.";
refreshAegis();
} else if (C & & C.restartApp) {
C.restartApp();
} else {
aegisStatus.textContent = "Update staged — restart Theseus to switch.";
}
} catch (e) {
aegisStatus.textContent = "Apply failed: " + (e?.message || e) + " — restart Theseus to fall back.";
} finally { aegisApply.disabled = false; aegisApply.textContent = orig; }
};
2026-09-08 18:09:55 +02:00
refreshAegis();
2026-09-08 01:42:41 +02:00
// ---- Manual "Check for updates" -----------------------------------------
// Fires an IPC that un-dismisses any lingering session chip and hits the
// release manifest. Renders either "you're on the latest (vN.M.O)" or
// "vX.Y.Z is available — the update chip in the toolbar will offer it".
const updStatus = document.getElementById("updStatus");
2026-09-08 12:36:21 +02:00
// Show the current version by default so the user doesn't have to
// click "Check for updates" just to find out which Theseus they're on.
if (C & & C.appVersion) C.appVersion().then((v) => {
if (v) updStatus.textContent = `You're on v${v}.`;
}).catch(() => {});
2026-09-08 01:42:41 +02:00
const updCheck = document.getElementById("updCheck");
updCheck.onclick = async () => {
const orig = updCheck.textContent;
updCheck.disabled = true; updCheck.textContent = "Checking…";
updStatus.textContent = "Checking…";
try {
const r = await C.recheckUpdate();
if (r & & r.updateAvailable & & r.updateAvailable.version) {
2026-09-08 01:51:03 +02:00
updStatus.innerHTML = `< b style = "color:var(--acid-text)" > v${r.updateAvailable.version}< / b > is available — the update chip in the toolbar will offer it.`;
2026-09-08 01:42:41 +02:00
} else {
updStatus.textContent = `You're on the latest (v${(r & & r.currentVersion) || "?"}).`;
}
} catch (e) { updStatus.textContent = "Check failed: " + (e?.message || e); }
finally { updCheck.disabled = false; updCheck.textContent = orig; }
};
// ---- Passwords section: three states (setup / locked / unlocked) ---------
// The vault lives in main.js — this UI just calls IPC. No plaintext ever
// sits in this DOM except the value produced by a specific Show/Copy click.
const pwSetupEl = document.getElementById("pwSetup");
const pwLockedEl = document.getElementById("pwLocked");
const pwUnlockedEl = document.getElementById("pwUnlocked");
const pwListEl = document.getElementById("pwList");
function pwShow(which) {
pwSetupEl.hidden = which !== "setup";
pwLockedEl.hidden = which !== "locked";
pwUnlockedEl.hidden = which !== "unlocked";
}
async function pwRefresh() {
const st = await C.pwStatus();
if (!st.setup) return pwShow("setup");
if (!st.unlocked) return pwShow("locked");
pwShow("unlocked");
const res = await C.pwList();
renderPwList(res.ok ? res.entries : []);
}
function renderPwList(entries) {
if (!entries.length) {
pwListEl.innerHTML = `< div class = "cempty" style = "padding:12px 0" > No entries yet — add one below.< / div > `;
return;
}
pwListEl.innerHTML = entries.map((e) => `< div class = "eng" data-id = "${esc(e.id)}" > ` +
`< span class = "eic" > < img class = "ei" src = "https://icons.duckduckgo.com/ip3/${esc(e.domain)}.ico" onerror = "this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'🔑'}))" > < / span > ` +
`< span class = "enm" > < b > ${esc(e.domain)}< / b > < span class = "pmuted" > · ${esc(e.username || "—")}< / span > < span class = "pmuted" style = "font-size:11px" > · ${e.kind === "generated" ? "generated" : "pasted"}< / span > < / span > ` +
`< button class = "cx pwShow" title = "Show + copy" > 👁< / button > ` +
`< button class = "cx pwDel" title = "Remove" > ✕< / button > ` +
`< / div > `).join("");
pwListEl.querySelectorAll(".pwShow").forEach((b) => b.onclick = async (ev) => {
const id = ev.target.closest(".eng").dataset.id;
const res = await C.pwGet(id);
if (!res.ok) return alert("Couldn't read: " + res.err);
try { await navigator.clipboard.writeText(res.password); }
catch { /* browser may block clipboard in dev — fall through to a prompt */ prompt("Password (copy manually):", res.password); return; }
b.textContent = "copied ✓"; setTimeout(() => (b.textContent = "👁"), 1600);
});
pwListEl.querySelectorAll(".pwDel").forEach((b) => b.onclick = async (ev) => {
const id = ev.target.closest(".eng").dataset.id;
if (!confirm("Remove this entry?")) return;
await C.pwRemove(id); pwRefresh();
});
}
// Setup — create vault
document.querySelectorAll('input[name="pwSeedSource"]').forEach((r) => r.addEventListener("change", () => {
document.getElementById("pwSetupMnemonic").hidden = document.querySelector('input[name="pwSeedSource"]:checked').value !== "mnemonic";
}));
document.getElementById("pwSetupBtn").onclick = async () => {
const p1 = document.getElementById("pwSetupPw1").value;
const p2 = document.getElementById("pwSetupPw2").value;
if (!p1 || p1.length < 8 ) return alert ( " Master password must be at least 8 characters . " ) ;
if (p1 !== p2) return alert("Passwords don't match.");
const kind = document.querySelector('input[name="pwSeedSource"]:checked').value;
const seedSource = kind === "mnemonic"
? { kind: "mnemonic", mnemonic: document.getElementById("pwSetupMnemonic").value }
: { kind: "generate" };
if (kind === "mnemonic" & & !seedSource.mnemonic.trim()) return alert("Paste your mnemonic or switch to 'Generate a new independent seed'.");
const res = await C.pwSetup(p1, seedSource);
if (!res.ok) return alert("Setup failed: " + res.err);
// Vault created AND unlocked by main. Clear the setup fields.
document.getElementById("pwSetupPw1").value = "";
document.getElementById("pwSetupPw2").value = "";
document.getElementById("pwSetupMnemonic").value = "";
pwRefresh();
};
// Unlock
document.getElementById("pwUnlockBtn").onclick = async () => {
const err = document.getElementById("pwUnlockErr");
err.hidden = true;
const pw = document.getElementById("pwUnlockPw").value;
const res = await C.pwUnlock(pw);
if (!res.ok) { err.textContent = res.err; err.hidden = false; return; }
document.getElementById("pwUnlockPw").value = "";
pwRefresh();
};
document.getElementById("pwUnlockPw").addEventListener("keydown", (e) => { if (e.key === "Enter") document.getElementById("pwUnlockBtn").click(); });
// Lock
document.getElementById("pwLockBtn").onclick = async () => { await C.pwLock(); pwRefresh(); };
// Add-entry form: toggle literal input; wire preview + save
document.querySelectorAll('input[name="pwAddKind"]').forEach((r) => r.addEventListener("change", () => {
const kind = document.querySelector('input[name="pwAddKind"]:checked').value;
document.getElementById("pwAddLiteral").hidden = kind !== "literal";
document.getElementById("pwAddPreview").hidden = kind !== "generated";
document.getElementById("pwAddPreviewOut").hidden = kind !== "generated";
}));
document.getElementById("pwAddPreview").onclick = async () => {
const domain = document.getElementById("pwAddDomain").value.trim();
const username = document.getElementById("pwAddUser").value.trim();
if (!domain) return alert("Enter a site.");
const res = await C.pwGenerate({ domain, username });
if (!res.ok) return alert("Preview failed: " + res.err);
document.getElementById("pwAddPreviewOut").value = res.password;
};
document.getElementById("pwAddBtn").onclick = async () => {
const domain = document.getElementById("pwAddDomain").value.trim();
const username = document.getElementById("pwAddUser").value.trim();
if (!domain) return alert("Enter a site.");
const kind = document.querySelector('input[name="pwAddKind"]:checked').value;
const spec = { domain, username };
if (kind === "literal") {
const lit = document.getElementById("pwAddLiteral").value;
if (!lit) return alert("Paste the password to save.");
spec.literal = lit;
}
const res = await C.pwAdd(spec);
if (!res.ok) return alert("Add failed: " + res.err);
document.getElementById("pwAddDomain").value = "";
document.getElementById("pwAddUser").value = "";
document.getElementById("pwAddLiteral").value = "";
document.getElementById("pwAddPreviewOut").value = "";
pwRefresh();
};
// Initial state — decide which panel to show now, and every time the user
// switches to Passwords in the sidebar (so a lock elsewhere is reflected).
pwRefresh();
document.querySelector('.side a[data-sec="passwords"]').addEventListener("click", pwRefresh);
});
// ---- Add-ons management ----
const addonsList = document.getElementById("addonsList");
2026-09-09 02:43:28 +02:00
// Per-addon update state, keyed by addon id. Populated by loadAddonUpdates()
// (staged: from listStagedAddonUpdates, background-polled) and by the
// manual "Check for updates" button (report: fresh per-addon status).
// renderAddons reads both when drawing each card so the update line lives
// inside the card — no separate "Pending updates" strip at the top.
const addonUpdates = { staged: {}, report: {} };
function updateLineFor(a) {
const st = addonUpdates.staged[a.id];
const rep = addonUpdates.report[a.id];
if (st) {
return '< div class = "d" style = "color:var(--acid);margin-top:4px" > ↻ Update < b > v' + escapeHtml(st.version) + '< / b > staged — restart Theseus to apply.< / div > ';
}
if (!rep) return "";
let msg = "", cls = "color:var(--dim)";
switch (rep.status) {
case "up-to-date": msg = "Up to date."; break;
case "no-update-url": return ""; // don't clutter cards that never opted in
case "fetch-failed": msg = "Update check failed — " + escapeHtml(rep.detail || "network"); cls = "color:#f6768a"; break;
case "signature-invalid": msg = "Endpoint offered v" + escapeHtml(rep.newVer || "?") + " with a BAD signature — rejected."; cls = "color:#f6768a"; break;
case "sha256-mismatch": msg = "Endpoint offered v" + escapeHtml(rep.newVer || "?") + " but the tarball hash didn't match."; cls = "color:#f6768a"; break;
case "extract-failed": msg = "v" + escapeHtml(rep.newVer || "?") + " downloaded but wouldn't extract — " + escapeHtml(rep.detail || ""); cls = "color:#f6768a"; break;
case "manifest-mismatch": msg = "Extracted manifest didn't match signed values."; cls = "color:#f6768a"; break;
case "already-staged": msg = "↻ v" + escapeHtml(rep.newVer || "?") + " already staged — restart to apply."; cls = "color:var(--acid)"; break;
case "staged": msg = "↻ Staged v" + escapeHtml(rep.newVer || "?") + " — restart to apply."; cls = "color:var(--acid)"; break;
default: msg = escapeHtml(rep.status || "unknown");
}
return '< div class = "d" style = "' + cls + ';margin-top:4px" > ' + msg + '< / div > ';
}
2026-09-08 01:42:41 +02:00
function renderAddons(snap) {
2026-09-21 02:43:10 +02:00
const items = (snap & & snap.installed) || [];
2026-09-08 01:42:41 +02:00
if (!items.length) {
addonsList.innerHTML = '< div class = "d" style = "color:var(--dim)" > No extensions installed. Drop a folder into the extensions directory to install one.< / div > ';
return;
}
2026-09-08 13:12:55 +02:00
// Split into built-in (ship with Theseus, reseeded on version bump) vs
// sideloaded (dropped in by the user). Built-in ones can still be
// disabled — the toggle just means "load me at startup" — but they
// can't be permanently deleted since reseedBundledAddons runs on the
// next launch. Rendering the two groups apart makes that clear.
const bundled = items.filter((a) => a.bundled);
const sideloaded = items.filter((a) => !a.bundled);
const rowFor = (a, isBundled) => {
2026-09-08 01:42:41 +02:00
if (a.error) {
return '< div class = "row" > < div class = "txt" > < div class = "t" > ⚠ Load failed < span style = "color:var(--dim);font-weight:400" > ' + escapeHtml(a.folder) + '< / span > < / div > < div class = "d" style = "color:#f6768a" > ' + escapeHtml(a.error) + '< / div > < / div > < div > < button class = "btn" data-reveal = "' + escapeAttr(a.folder) + '" > Show folder< / button > < / div > < / div > ';
}
const caps = (a.capabilities || []).length ? '< span style = "color:var(--dim);font-size:11.5px;margin-left:8px" > ' + a.capabilities.map(escapeHtml).join(", ") + '< / span > ' : "";
2026-09-08 13:12:55 +02:00
const builtInBadge = isBundled
? '< span style = "display:inline-block;font-size:10px;letter-spacing:.05em;padding:1px 6px;border-radius:3px;background:rgba(214,255,61,.14);color:var(--acid);font-weight:700;margin-left:6px;vertical-align:middle" > BUILT-IN< / span > '
: "";
2026-09-08 18:42:00 +02:00
// Manifest icons may be an emoji ("🛡"), a "•" fallback, or a
// data: URI to a branded SVG/PNG — render as < img > for URIs,
// as escaped text otherwise so the emoji case keeps working.
const iconHtml = /^data:image\//i.test(a.icon || "")
? '< img src = "' + escapeAttr(a.icon) + '" alt = "" style = "width:16px;height:16px;vertical-align:middle;margin-right:4px" > '
: escapeHtml(a.icon || "•");
2026-09-09 02:43:28 +02:00
return '< div class = "row" > < div class = "txt" > < div class = "t" > ' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' < span style = "color:var(--dim);font-weight:400" > v' + escapeHtml(a.version) + '< / span > ' + caps + '< / div > < div class = "d" > ' + escapeHtml(a.description || "") + (a.author ? ' < span style = "color:var(--dim)" > — ' + escapeHtml(a.author) + '< / span > ' : '') + '< / div > ' + updateLineFor(a) + '< / div > < div style = "display:flex;gap:8px;align-items:center" > < button class = "btn" data-reveal = "' + escapeAttr(a.folder) + '" > Show folder< / button > < label class = "sw" > < input type = "checkbox" data-toggle = "' + escapeAttr(a.id) + '" ' + ( a . enabled ? " checked " : " " ) + ' > < span class = "track" > < span class = "knob" > < / span > < / span > < / label > < / div > < / div > ';
2026-09-08 13:12:55 +02:00
};
const sectionHeader = (label, hint) =>
'< div class = "d" style = "color:var(--dim);margin:12px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em" > ' + escapeHtml(label) + '< / div > '
+ (hint ? '< div class = "d" style = "color:var(--dim);margin:-4px 0 8px" > ' + escapeHtml(hint) + '< / div > ' : '');
let html = "";
if (bundled.length) html += sectionHeader("Built into Theseus", "Ship with every Theseus install. Disabling one hides its UI but the folder stays; a fresh install of a newer Theseus reseeds it.")
+ bundled.map((a) => rowFor(a, true)).join("");
if (sideloaded.length) html += sectionHeader("Sideloaded", sideloaded.length & & bundled.length ? "Third-party or hand-installed. Remove by deleting the folder." : "")
+ sideloaded.map((a) => rowFor(a, false)).join("");
addonsList.innerHTML = html;
2026-09-08 01:42:41 +02:00
addonsList.querySelectorAll('input[data-toggle]').forEach((cb) => {
cb.addEventListener("change", async () => {
await C.setAddonEnabled(cb.dataset.toggle, cb.checked);
loadAddons();
});
});
addonsList.querySelectorAll('button[data-reveal]').forEach((btn) => {
btn.addEventListener("click", () => C.revealAddon(btn.dataset.reveal));
});
}
2026-09-21 03:28:04 +02:00
// Discovery of new community extensions happens at theseus.x/extensions —
// Settings only shows what's installed on this machine. The install button
// on that page invokes the same "addons-install-community" IPC and Theseus
// refreshes the installed list on its own.
2026-09-08 01:42:41 +02:00
function escapeHtml(s) { return String(s || "").replace(/[& < >"']/g, (c) => ({ "&":"& ","< ":"< ",">":"> ",'"':"" ","'":"' " })[c]); }
function escapeAttr(s) { return escapeHtml(s); }
async function loadAddons() {
try { renderAddons(await C.listAddons()); }
catch (e) { addonsList.textContent = "Failed to load extensions: " + (e?.message || e); }
}
2026-09-09 02:43:28 +02:00
// Refresh the staged-updates map from the background poll. Whatever came
// back through listStagedAddonUpdates is what promoteStagedUpdates will
// pick up on the next launch — the per-card badge reflects exactly that.
async function loadAddonUpdates() {
let staged = [];
try { staged = await C.listStagedAddonUpdates(); } catch { staged = []; }
addonUpdates.staged = {};
for (const s of (staged || [])) addonUpdates.staged[s.id] = { version: s.version, name: s.name };
}
2026-09-08 01:42:41 +02:00
document.getElementById("addonsReload").addEventListener("click", async () => {
2026-09-09 02:43:28 +02:00
await C.reloadAddons(); await loadAddonUpdates(); loadAddons();
2026-09-08 01:42:41 +02:00
});
document.getElementById("addonsOpenDir").addEventListener("click", () => C.openAddonsDir());
2026-09-09 02:43:28 +02:00
document.querySelector('.side a[data-sec="addons"]').addEventListener("click", async () => {
2026-09-21 03:28:04 +02:00
await loadAddonUpdates(); loadAddons();
2026-09-09 02:43:28 +02:00
});
2026-09-08 23:00:41 +02:00
// Plug-ins tab: refresh Ariadne's live daemon state on every visit so it
// doesn't display stale "checking…" text if the background poll finished
// while another section was open. Aegis card loads on page-init and its
// "Check for updates" button is user-initiated, so we don't auto-poll it
// (that would fire a network request every tab-switch).
document.querySelector('.side a[data-sec="plugins"]').addEventListener("click", () => {
const ar = document.getElementById("ariadneRefresh");
if (ar) ar.click();
});
2026-09-08 02:27:36 +02:00
2026-09-09 02:43:28 +02:00
// (Old separate "Pending updates" strip lived here. Per-card update
// line is now painted by updateLineFor() inside each addon row —
// loadAddonUpdates() populates addonUpdates.staged before renderAddons.)
2026-09-08 02:27:36 +02:00
document.getElementById("addonsCheckUpdates").addEventListener("click", async () => {
const btn = document.getElementById("addonsCheckUpdates");
const status = document.getElementById("addonsUpdStatus");
btn.disabled = true; const orig = btn.textContent; btn.textContent = "Checking…";
2026-09-08 18:14:07 +02:00
status.innerHTML = "";
2026-09-08 02:27:36 +02:00
try {
2026-09-08 18:14:07 +02:00
const res = await C.checkAddonUpdates();
// Back-compat: some callers still pass a bare array. Normalize.
const report = Array.isArray(res) ? [] : (res?.report || []);
const skipped = Array.isArray(res) ? null : (res?.skipped || null);
const staged = Array.isArray(res) ? res : (res?.staged || []);
if (skipped === "no-pubkeys") {
status.textContent = "Update endpoint disabled — no operator pubkey baked into this build.";
} else {
2026-09-09 02:43:28 +02:00
// Fold the per-addon report into addonUpdates.report so the
// per-card update line reflects the freshest check. A summary at
// the top counts staged/updated vs. up-to-date, but the per-addon
// detail lives on each card.
addonUpdates.report = {};
for (const r of report) addonUpdates.report[r.id] = r;
const stagedNow = report.filter((r) => r.status === "staged" || r.status === "already-staged").length;
const failed = report.filter((r) => ["fetch-failed","signature-invalid","sha256-mismatch","extract-failed","manifest-mismatch"].includes(r.status)).length;
if (stagedNow) {
status.textContent = stagedNow + " update" + (stagedNow > 1 ? "s" : "") + " staged; restart Theseus to apply.";
} else if (failed) {
status.textContent = failed + " failed — see the extension card" + (failed > 1 ? "s" : "") + " below.";
} else if (!report.length) {
status.textContent = "No extensions with an update endpoint.";
} else {
status.textContent = "All extensions up to date.";
}
2026-09-08 02:27:36 +02:00
}
2026-09-09 02:43:28 +02:00
await loadAddonUpdates();
loadAddons();
2026-09-08 02:27:36 +02:00
} catch (e) {
status.textContent = "Check failed: " + (e?.message || e);
} finally {
btn.disabled = false; btn.textContent = orig;
}
});
2026-09-08 01:42:41 +02:00
// Populate on first paint so the tab is ready when the user clicks in.
2026-09-21 03:28:04 +02:00
(async () => { await loadAddonUpdates(); loadAddons(); })();
2026-09-08 01:42:41 +02:00
< / script >
< / body >
< / html >