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){
:root{ --bg:#eef0f4; --panel:#ffffff; --line:rgba(0,0,0,.11); --ink:#1a1f28; --mut:#4a5262; --dim:#7b8494; }
.side{ background:#f6f8fb; }
.side a:hover{ background:rgba(0,0,0,.05); }
select, .ctl input, .addeng input{ background:#f1f3f7; }
.track{ background:#c8cfdb; }
.knob{ background:#8a93a2; }
code, .ceng, .eng{ background:#eef1f6; }
}
< / 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 >
< 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 >
< 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 >
< 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 >
< button id = "aegisApply" class = "btn" hidden > Restart to apply update< / button >
< / 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
< code style = "background:transparent;border:none;padding:0" id = "addonsPathHint" > %APPDATA%\Theseus Navigator\addons\< / code > . Drop a folder in, restart, it's live.
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-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 >
< div id = "addonsStagedBox" hidden style = "margin-top:1rem;padding:12px 14px;border:1px solid var(--acid);border-radius:10px;background:rgba(214,255,61,.06)" >
< div style = "font-weight:600;margin-bottom:6px" > Pending updates< / div >
< div id = "addonsStaged" style = "font-size:13px" > < / div >
< div style = "color:var(--dim);font-size:12px;margin-top:8px" > These apply on the next Theseus restart.< / div >
< / 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 >
< div class = "note" > Extensions run with full app access — treat installing one like installing an unsigned executable.
Only load extensions whose source you trust.< / div >
< / 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.
const sections = ["general", "search", "passwords", "performance", "privacy", "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));
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);
};
// ---- 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:09:55 +02:00
// ---- Aegis (bchwallet) update card --------------------------------------
// 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
// uses is filtered for the bchwallet id so this card only speaks for Aegis.
const aegisStatus = document.getElementById("aegisStatus");
const aegisCheck = document.getElementById("aegisCheck");
const aegisApply = document.getElementById("aegisApply");
async function refreshAegis() {
aegisApply.hidden = true;
try {
const [installed, staged] = await Promise.all([
C.listAddons ? C.listAddons() : Promise.resolve([]),
C.listStagedAddonUpdates ? C.listStagedAddonUpdates() : Promise.resolve([]),
]);
const cur = (installed || []).find((a) => a.id === "bchwallet");
const upd = (staged || []).find((a) => a.id === "bchwallet");
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) {
aegisStatus.innerHTML = `< b style = "color:var(--acid-text)" > v${upd.version}< / b > staged — restart Theseus to switch to it. You're on v${curVer}.`;
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 {
const staged = await (C.checkAddonUpdates ? C.checkAddonUpdates() : Promise.resolve([]));
const upd = (staged || []).find((a) => a.id === "bchwallet");
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; }
};
aegisApply.onclick = () => { if (C & & C.restartApp) C.restartApp(); };
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");
function renderAddons(snap) {
const items = (snap & & snap.installed) || [];
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 > '
: "";
return '< div class = "row" > < div class = "txt" > < div class = "t" > ' + a.icon + ' ' + 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 > < / 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 > ';
};
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));
});
}
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); }
}
document.getElementById("addonsReload").addEventListener("click", async () => {
await C.reloadAddons(); loadAddons();
});
document.getElementById("addonsOpenDir").addEventListener("click", () => C.openAddonsDir());
2026-09-08 02:27:36 +02:00
document.querySelector('.side a[data-sec="addons"]').addEventListener("click", () => { loadAddons(); loadStagedAddonUpdates(); });
// Manual "Check for updates" for extensions — runs the same signed-update
// polling the boot timer runs, and surfaces what's staged for the next
// launch. Staged folders live in < userData > /addons-updates-staged/ and
// promoteStagedUpdates() applies them on next init.
async function loadStagedAddonUpdates() {
let staged = [];
try { staged = await C.listStagedAddonUpdates(); } catch { staged = []; }
const box = document.getElementById("addonsStagedBox");
const list = document.getElementById("addonsStaged");
if (!staged || !staged.length) { box.hidden = true; return; }
list.innerHTML = staged.map((s) => '< div > ' + escapeHtml(s.name) + ' → < b > ' + escapeHtml(s.version) + '< / b > < / div > ').join("");
box.hidden = false;
}
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…";
status.textContent = "";
try {
const staged = await C.checkAddonUpdates();
if (!staged || !staged.length) {
status.textContent = "All extensions are up to date.";
} else {
status.textContent = staged.length + " update" + (staged.length > 1 ? "s" : "") + " staged; restart Theseus to apply.";
}
await loadStagedAddonUpdates();
} catch (e) {
status.textContent = "Check failed: " + (e?.message || e);
} finally {
btn.disabled = false; btn.textContent = orig;
}
});
loadStagedAddonUpdates();
2026-09-08 01:42:41 +02:00
// Populate on first paint so the tab is ready when the user clicks in.
loadAddons();
< / script >
< / body >
< / html >