Theseus 0.3.60: pick your browsing language from the URL bar; per plug-in settings

A globe chip next to the URL-bar star shows the language sites see you in
(Accept-Language + navigator.language) — "AUTO" while following the OS locale,
the two-letter code once you pin one. Click opens a 23-language menu; the same
setting has a friendly row at the top of Settings › General. Both write to the
existing languageMode/languageValue and stay in sync with the Anti-fingerprinting
Language row through a settings-update broadcast (settings.html and chrome.html
both react live).

Settings › Plug-ins is now two compact rows — one per plug-in — with the on/off
toggle on the right and the update controls beside it. Clicking a plug-in's title
opens its own sub-page (plugins/ariadne, plugins/aegis) with the full description
and the Uninstall button, so the main list stays scannable and dangerous actions
stop travelling with the everyday ones. The Ariadne toggle and its sub-page
mirror the same scheduled-task state.
This commit is contained in:
Silent Mode 2026-09-30 02:16:11 +02:00
parent 13e4c6997c
commit df2b1f57ff
6 changed files with 521 additions and 180 deletions

View file

@ -187,6 +187,20 @@
display: grid; place-items: center; line-height: 1; box-shadow: 0 0 0 2px var(--bg); } display: grid; place-items: center; line-height: 1; box-shadow: 0 0 0 2px var(--bg); }
.dlbtn.done .dlbadge { background: #4fd1a5; } .dlbtn.done .dlbadge { background: #4fd1a5; }
.dlbtn.err .dlbadge { background: #f6768a; } .dlbtn.err .dlbadge { background: #f6768a; }
/* Website-language chip inside the address bar (Firefox-style, next to the
bookmark star). Two-letter uppercase code shows the language sites see us
in — Accept-Language + navigator.language. Same setting the Anti-
fingerprinting Language row edits; both stay in sync via settings-update. */
.langchip { flex: none; display: inline-flex; align-items: center; gap: 3px;
height: 24px; padding: 0 5px 0 4px; border: none; border-radius: 6px;
background: transparent; cursor: pointer; color: var(--dim);
font: 600 10.5px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
letter-spacing: .04em; }
.langchip:hover { background: var(--line2); color: var(--ink); }
.langchip.on { color: var(--acid); }
.langchip svg { width: 15px; height: 15px; fill: none; stroke: currentColor;
stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.langchip .lcode { min-width: 16px; text-align: center; }
/* indeterminate loading bar under the toolbar — collapses when idle so it /* indeterminate loading bar under the toolbar — collapses when idle so it
doesn't take a permanent 2px strip below the address bar */ doesn't take a permanent 2px strip below the address bar */
.loadbar { height: 0; overflow: hidden; background: transparent; transition: height .12s linear; } .loadbar { height: 0; overflow: hidden; background: transparent; transition: height .12s linear; }
@ -286,7 +300,7 @@
its width and pushed the registry button and the star out past the its width and pushed the registry button and the star out past the
bar's right edge. */ bar's right edge. */
#url { flex: 1 1 0; min-width: 0; } #url { flex: 1 1 0; min-width: 0; }
.secbadge, .pwchip, .star, .regbtn, .zoomchip { flex: none; } .secbadge, .pwchip, .star, .regbtn, .zoomchip, .langchip { flex: none; }
/* Zoom indicator — only shown when the active tab isn't at 100 %. /* Zoom indicator — only shown when the active tab isn't at 100 %.
Click resets (Ctrl+0); Ctrl +/- and Ctrl+wheel step the level. */ Click resets (Ctrl+0); Ctrl +/- and Ctrl+wheel step the level. */
.zoomchip { border: none; background: transparent; color: var(--mut); cursor: pointer; padding: 2px 5px; .zoomchip { border: none; background: transparent; color: var(--mut); cursor: pointer; padding: 2px 5px;
@ -490,6 +504,13 @@
makes it installable (Chrome's omnibox icon). Filled once the makes it installable (Chrome's omnibox icon). Filled once the
app is installed; click then opens it or removes it. --> app is installed; click then opens it or removes it. -->
<button class="appchip" id="appchip" title="Install app" hidden><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="2.5" width="12" height="9" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 5 V9.5 M6 7.8 L8 9.8 L10 7.8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.5 14 H10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button> <button class="appchip" id="appchip" title="Install app" hidden><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="2.5" width="12" height="9" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 5 V9.5 M6 7.8 L8 9.8 L10 7.8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.5 14 H10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
<!-- Website-language chip: click to change the language sites serve
you in. Icon = globe with meridian + equator (universal "language"
glyph); label = 2-letter code, or "AUTO" when we follow the OS. -->
<button class="langchip" id="langBtn" title="Language websites serve you in">
<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M2 8 H14"/><path d="M8 2 C5 5 5 11 8 14 C11 11 11 5 8 2"/></svg>
<span class="lcode" id="langCode">AUTO</span>
</button>
<button class="star" id="star" title="Save this page">☆</button> <button class="star" id="star" title="Save this page">☆</button>
</div> </div>
<div class="bardrag" id="bardrag" title="Drag to shift the address / search bar ratio"></div> <div class="bardrag" id="bardrag" title="Drag to shift the address / search bar ratio"></div>
@ -694,6 +715,41 @@
T.getDownloads && T.getDownloads().then(renderDownloads); T.getDownloads && T.getDownloads().then(renderDownloads);
T.onDownloads && T.onDownloads(renderDownloads); T.onDownloads && T.onDownloads(renderDownloads);
// ---- website-language pill: shows the language sites see us in ----
// "AUTO" (2-letter tone) when following the OS locale; a two-letter
// uppercase code (e.g. "DE", "PT") when a specific language is pinned.
// Same setting the Anti-fingerprinting Language row edits; both stay in
// sync via T.onSettingsUpdate below.
const langBtn = $("langBtn"), langCode = $("langCode");
let systemLocaleCached = "en-US";
function short(tag) { return String(tag || "").split("-")[0].toUpperCase().slice(0, 2) || "??"; }
function renderLang(s) {
if (!s) return;
const auto = (s.languageMode || "show") === "show";
if (auto) {
langBtn.classList.remove("on");
langCode.textContent = "AUTO";
langBtn.title = `Website language: automatic (OS: ${systemLocaleCached}). Click to change.`;
} else {
const tag = (s.languageMode === "manual" && s.languageValue) ? s.languageValue
: (s.languageMode === "spoof" && s.languageSpoof) ? s.languageSpoof
: (s.languageMode === "hide") ? "en-US" : systemLocaleCached;
langBtn.classList.add("on");
langCode.textContent = short(tag);
langBtn.title = `Websites see you in ${tag}. Click to change.`;
}
}
langBtn.onclick = () => {
const r = langBtn.getBoundingClientRect();
T.websiteLanguageMenu && T.websiteLanguageMenu({ x: Math.round(r.left), y: Math.round(r.bottom + 4) });
};
// Locale is optional (older main.js won't expose it) — render on settings
// either way so the chip works even without it.
(T.systemLocale ? T.systemLocale() : Promise.resolve("en-US"))
.then((loc) => { systemLocaleCached = loc || "en-US"; return T.getSettings(); })
.then(renderLang).catch(() => {});
T.onSettingsUpdate && T.onSettingsUpdate(renderLang);
// ---- extension dock ---- // ---- extension dock ----
// One toolbar button per registered addon sidebar-panel, plus a static // One toolbar button per registered addon sidebar-panel, plus a static
// "coming soon" placeholder for Aegis (the built-in BCH wallet, in // "coming soon" placeholder for Aegis (the built-in BCH wallet, in

66
main.js
View file

@ -4045,6 +4045,72 @@ ipcMain.handle("tab-context-menu-popup", (e, tabId, rect) => {
popup.popup({ window: win, x, y }); popup.popup({ window: win, x, y });
return true; return true;
}); });
// Website-language quick switch — a native menu anchored under the toolbar
// pill. Same setting the Anti-fingerprinting Language row edits: "auto"
// (languageMode="show", follow OS) or a specific BCP-47 tag
// (languageMode="manual", languageValue=<tag>). One truth source, two entry
// points. Applied via applyAcceptLanguage() + applyFingerprintAll() below.
const WEBSITE_LANGUAGE_QUICK = [
{ tag: "en-US", label: "English (US)" },
{ tag: "en-GB", label: "English (UK)" },
{ tag: "es-ES", label: "Español" },
{ tag: "pt-BR", label: "Português (Brasil)" },
{ tag: "fr-FR", label: "Français" },
{ tag: "de-DE", label: "Deutsch" },
{ tag: "it-IT", label: "Italiano" },
{ tag: "nl-NL", label: "Nederlands" },
{ tag: "ru-RU", label: "Русский" },
{ tag: "uk-UA", label: "Українська" },
{ tag: "pl-PL", label: "Polski" },
{ tag: "tr-TR", label: "Türkçe" },
{ tag: "ar", label: "العربية" },
{ tag: "he-IL", label: "עברית" },
{ tag: "el-GR", label: "Ελληνικά" },
{ tag: "zh-CN", label: "中文(简体)" },
{ tag: "zh-TW", label: "中文(繁體)" },
{ tag: "ja-JP", label: "日本語" },
{ tag: "ko-KR", label: "한국어" },
{ tag: "hi-IN", label: "हिन्दी" },
{ tag: "vi-VN", label: "Tiếng Việt" },
{ tag: "th-TH", label: "ไทย" },
{ tag: "id-ID", label: "Bahasa Indonesia" },
];
function setWebsiteLanguage(mode, value) {
if (mode === "show") {
settings.languageMode = "show";
} else if (mode === "manual" && value) {
settings.languageMode = "manual";
settings.languageValue = String(value);
} else return;
saveSettings();
applyFingerprintAll();
applyAcceptLanguage();
try { chrome?.webContents.send("settings-update", settings); } catch {}
}
ipcMain.handle("system-locale", () => app.getLocale() || "en-US");
ipcMain.handle("website-language-menu-popup", (e, rect) => {
if (chrome && e.sender !== chrome.webContents) throw new Error("website-language-menu-popup: untrusted sender");
const osLoc = app.getLocale() || "en-US";
const isAuto = settings.languageMode === "show";
const current = isAuto ? null : (settings.languageValue || "").toLowerCase();
const template = [
{ label: `Automatic (OS: ${osLoc})${isAuto ? " ✓" : ""}`,
click: () => setWebsiteLanguage("show") },
{ type: "separator" },
...WEBSITE_LANGUAGE_QUICK.map((L) => ({
label: `${L.label} — ${L.tag}${current === L.tag.toLowerCase() ? " ✓" : ""}`,
click: () => setWebsiteLanguage("manual", L.tag),
})),
{ type: "separator" },
{ label: "More languages…", click: () => { try { openSettingsTab("general"); } catch {} } },
];
const popup = Menu.buildFromTemplate(template);
const chromeBounds = chrome ? chrome.getBounds() : { x: 0, y: 0 };
const x = Math.max(0, Math.round(chromeBounds.x + (rect?.x || 0)));
const y = Math.max(0, Math.round(chromeBounds.y + (rect?.y || 0)));
popup.popup({ window: win, x, y });
return true;
});
ipcMain.handle("move-tab", (_e, id, targetId, place) => { ipcMain.handle("move-tab", (_e, id, targetId, place) => {
const src = tabs.findIndex((t) => t.id === id); const src = tabs.findIndex((t) => t.id === id);
const dst = tabs.findIndex((t) => t.id === targetId); const dst = tabs.findIndex((t) => t.id === targetId);

View file

@ -1,6 +1,6 @@
{ {
"name": "theseus-navigator", "name": "theseus-navigator",
"version": "0.3.59", "version": "0.3.60",
"license": "MPL-2.0", "license": "MPL-2.0",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode", "author": "Silent Mode",

View file

@ -38,6 +38,8 @@ contextBridge.exposeInMainWorld("theseus", {
getSettings: () => ipcRenderer.invoke("settings-get"), getSettings: () => ipcRenderer.invoke("settings-get"),
setSetting: (key, val) => ipcRenderer.invoke("settings-set", key, val), setSetting: (key, val) => ipcRenderer.invoke("settings-set", key, val),
onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)), onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)),
systemLocale: () => ipcRenderer.invoke("system-locale"),
websiteLanguageMenu: (rect) => ipcRenderer.invoke("website-language-menu-popup", rect),
// Find-in-page. main.js fires 'find-open' on Ctrl+F; chrome renderer // Find-in-page. main.js fires 'find-open' on Ctrl+F; chrome renderer
// owns the bar UI and drives findInPage / stopFindInPage via these // owns the bar UI and drives findInPage / stopFindInPage via these
// wrappers. Match count / active ordinal comes back through onFindResult. // wrappers. Match count / active ordinal comes back through onFindResult.

View file

@ -31,23 +31,24 @@ contextBridge.exposeInMainWorld("cfg", {
collisionState: () => ipcRenderer.invoke("collision-state"), collisionState: () => ipcRenderer.invoke("collision-state"),
setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p), setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p),
resetCollisions: () => ipcRenderer.invoke("collision-reset"), resetCollisions: () => ipcRenderer.invoke("collision-reset"),
ariadneState: () => ipcRenderer.invoke("ariadne-state"),
ariadneToggle: (on) => ipcRenderer.invoke("ariadne-toggle", !!on),
ariadneInstall: () => ipcRenderer.invoke("ariadne-install"),
ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"),
ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"),
// Manual "Check for updates" — un-dismisses any existing chip and re-
// fetches the release manifest. Returns { updateAvailable, currentVersion }.
recheckUpdate: () => ipcRenderer.invoke("recheck-update"),
appVersion: () => ipcRenderer.invoke("app-version"),
restartApp: () => ipcRenderer.invoke("app-restart"),
// Add-ons management (Settings > Add-ons tab). // Add-ons management (Settings > Add-ons tab).
listAddons: () => ipcRenderer.invoke("addons-list"), listAddons: () => ipcRenderer.invoke("addons-list"),
setAddonEnabled: (id, enabled) => ipcRenderer.invoke("addons-set-enabled", id, !!enabled), setAddonEnabled: (id, enabled) => ipcRenderer.invoke("addons-set-enabled", id, !!enabled),
revealAddon: (folder) => ipcRenderer.invoke("addons-reveal", folder), revealAddon: (folder) => ipcRenderer.invoke("addons-reveal", folder),
// Delete a non-bundled extension's folder (Settings › Extensions › ⋯ › Remove). // Delete a non-bundled extension's folder (Settings › Extensions › ⋯ › Remove).
removeAddon: (id) => ipcRenderer.invoke("addons-remove", id), removeAddon: (id) => ipcRenderer.invoke("addons-remove", id),
// Install a staged signed update now (Settings › Extensions › "Update to vX"); no id = every staged extension. openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"),
reloadAddons: () => ipcRenderer.invoke("addons-reload"),
// Add-on update flow. checkAddonUpdates hits the release manifest and
// stages any newer signed version; listStagedAddonUpdates reports what's
// waiting; applyStagedAddons promotes staged → active and reactivates the
// addon host so the new bytes load without a full Theseus restart.
// Community extensions from theseus.x/extensions: the catalog (with what
// is installed already) and a verified install/update of one entry.
communityCatalog: () => ipcRenderer.invoke("addons-community-catalog"),
installCommunity: (id) => ipcRenderer.invoke("addons-install-community", id),
checkAddonUpdates: () => ipcRenderer.invoke("addons-check-updates"),
listStagedAddonUpdates: () => ipcRenderer.invoke("addons-list-staged"),
applyStagedAddons: (id) => ipcRenderer.invoke("addons-apply-staged", typeof id === "string" ? id : undefined), applyStagedAddons: (id) => ipcRenderer.invoke("addons-apply-staged", typeof id === "string" ? id : undefined),
// Settings › Performance › Protections: talk to an add-on's own message // Settings › Performance › Protections: talk to an add-on's own message
// handlers (Shield, Cookie Pop-ups) and open its panel for the details. // handlers (Shield, Cookie Pop-ups) and open its panel for the details.
@ -57,12 +58,23 @@ contextBridge.exposeInMainWorld("cfg", {
reportSection: (slug) => ipcRenderer.invoke("settings-section", String(slug || "")), reportSection: (slug) => ipcRenderer.invoke("settings-section", String(slug || "")),
torState: () => ipcRenderer.invoke("tor-state"), torState: () => ipcRenderer.invoke("tor-state"),
toggleTor: () => ipcRenderer.invoke("toggle-tor"), toggleTor: () => ipcRenderer.invoke("toggle-tor"),
openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"), // OS locale — used by the Website-language row to label "Automatic (OS: …)".
reloadAddons: () => ipcRenderer.invoke("addons-reload"), systemLocale: () => ipcRenderer.invoke("system-locale"),
// Community extensions from theseus.x/extensions: the catalog (with what // Live settings updates — the toolbar chip, this page's Website-language
// is installed already) and a verified install/update of one entry. // row, and the Anti-fingerprinting Language row all edit the same setting;
communityCatalog: () => ipcRenderer.invoke("addons-community-catalog"), // any of them writing pushes a "settings-update" the others react to.
installCommunity: (id) => ipcRenderer.invoke("addons-install-community", id), onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)),
checkAddonUpdates: () => ipcRenderer.invoke("addons-check-updates"), // Ariadne's Thread plug-in (system-wide resolver). The state getter returns
listStagedAddonUpdates: () => ipcRenderer.invoke("addons-list-staged"), // { state:"running"|"stopped"|"not-installed", installedVersion, bundledVersion,
// canUpdate, hasUninstaller }; the mutators prompt UAC for admin.
ariadneState: () => ipcRenderer.invoke("ariadne-state"),
ariadneToggle: (on) => ipcRenderer.invoke("ariadne-toggle", !!on),
ariadneInstall: () => ipcRenderer.invoke("ariadne-install"),
ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"),
ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"),
// Manual "Check for updates" — un-dismisses any existing chip and re-
// fetches the release manifest. Returns { updateAvailable, currentVersion }.
recheckUpdate: () => ipcRenderer.invoke("recheck-update"),
appVersion: () => ipcRenderer.invoke("app-version"),
restartApp: () => ipcRenderer.invoke("app-restart"),
}); });

View file

@ -42,6 +42,16 @@
.srow::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--dim);flex:none} .srow::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--dim);flex:none}
.srow.ok::before{background:#3ddc84}.srow.warn::before{background:#f5a524} .srow.ok::before{background:#3ddc84}.srow.warn::before{background:#f5a524}
.row.link{cursor:pointer}.row.link:hover{border-color:rgb(from var(--acid) r g b / .4)}.row .chev{color:var(--dim);font-size:20px;line-height:1} .row.link{cursor:pointer}.row.link:hover{border-color:rgb(from var(--acid) r g b / .4)}.row .chev{color:var(--dim);font-size:20px;line-height:1}
/* Plug-in main-page row: the .txt area is a click target that navigates to
the plug-in's own settings sub-page (chevron on the title); .plug-ctl on
the right hosts the update button + on/off toggle, and stops the click
from bubbling so they don't navigate. */
.plug-row .plug-open{cursor:pointer;border-radius:8px;padding:2px 6px;margin:-2px -6px;transition:background .12s}
.plug-row .plug-open:hover{background:rgba(255,255,255,.04)}
.plug-row .plug-open:focus-visible{outline:2px solid rgb(from var(--acid) r g b / .5);outline-offset:2px}
.plug-row .plug-open .chev{color:var(--dim);font-size:15px;line-height:1;margin-left:4px;opacity:.6}
.plug-row .plug-open:hover .chev{opacity:1;color:var(--acid-text)}
.plug-row .plug-ctl{display:flex;align-items:center;gap:8px;flex:none;flex-wrap:wrap;justify-content:flex-end;max-width:60%}
.exlist{display:flex;flex-direction:column;gap:6px;margin-bottom:14px} .exlist{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
.exrow{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:9px 14px;font:13px ui-monospace,"Cascadia Code",Consolas,monospace} .exrow{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:9px 14px;font:13px ui-monospace,"Cascadia Code",Consolas,monospace}
.exempty{color:var(--dim);font-size:13px;margin:0 0 14px} .exempty{color:var(--dim);font-size:13px;margin:0 0 14px}
@ -241,7 +251,16 @@
<section id="general"> <section id="general">
<h1>General</h1> <h1>General</h1>
<p class="lede">Changes apply immediately and are saved for next time.</p> <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> <h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Website language</h2>
<p class="subd">The language sites serve you in — Wikipedia, YouTube, news sites, and anything else that has a localised version. Sent as your <code>Accept-Language</code> header and <code>navigator.language</code>. <b>Automatic</b> follows your operating system; pick a language to override it. The same setting lives under <b>Privacy → Anti-fingerprinting → Language</b> for advanced modes (Spoof, Hide).</p>
<div class="row">
<div class="txt"><div class="t">Preferred language</div><div class="d" id="webLangHint">Also switchable from the globe icon in the address bar.</div></div>
<div class="ctl">
<select id="webLangPick"></select>
<input id="webLangOther" type="text" placeholder="BCP-47, e.g. cs-CZ" hidden style="width:150px">
</div>
</div>
<h2 class="sub">Startup</h2>
<div class="row"> <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> <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> <label class="sw"><input type="checkbox" id="restoreSession"><span class="track"><span class="knob"></span></span></label>
@ -309,52 +328,107 @@
</div> </div>
</section> </section>
<!-- PLUG-INS --> <!-- PLUG-INS -->
<!-- Compact main page: one row per plug-in with title / status / update
controls on the left and the on/off toggle on the right. Clicking
the TITLE opens that plug-in's own settings sub-page (details +
Uninstall). Toggle and buttons stopPropagation so they don't
navigate. -->
<section id="plugins" hidden> <section id="plugins" hidden>
<h1>Plug-ins</h1> <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> <p class="lede">Silent Mode components that live alongside Theseus — each with its own on/off, updates and settings page.</p>
<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> <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>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
<div class="txt"> <!-- Ariadne's Thread -->
<div class="t">System-wide resolver — Ariadne's Thread</div> <div class="row plug-row">
<div class="d"> <div class="txt plug-open" data-go="plugins/ariadne" role="button" tabindex="0" title="Open Ariadne's Thread settings">
A local daemon that resolves BCDN names for <b>every browser on this machine</b> <div class="t">Ariadne's Thread <span class="pmuted" style="font-weight:400">— system-wide resolver</span> <span class="chev">›</span></div>
(Chrome, Edge, Firefox, etc.), not just Theseus. Turning it off means non-Theseus <div class="d">Resolves BCDN names for every browser on this machine — not just Theseus.</div>
browsers stop resolving <code>.bch</code> / <code>.x</code> / other BCNR TLDs; <div id="ariadneStatusMain" class="pmuted" style="font-size:12.5px;margin-top:4px">checking…</div>
Theseus keeps working either way, since it has its own built-in resolver.
</div>
</div> </div>
<div id="ariadneStatus" class="pmuted" style="font-size:12.5px">checking…</div> <div class="plug-ctl">
<div style="display:flex;gap:8px;flex-wrap:wrap"> <button id="ariadneInstallMain" class="btn small" hidden>Install</button>
<button id="ariadneStart" class="btn" hidden>Turn on</button> <button id="ariadneUpdateMain" class="btn small" hidden>Update</button>
<button id="ariadneStop" class="btn" hidden>Turn off</button> <button id="ariadneCheckMain" class="btn small" title="Refresh Ariadne status">Refresh</button>
<button id="ariadneInstall" class="btn" hidden>Install</button> <label class="sw" id="ariadneSwWrap" title="Turn Ariadne on/off">
<button id="ariadneUpdate" class="btn" hidden>Update</button> <input type="checkbox" id="ariadneToggleMain">
<button id="ariadneUninstall" class="btn" hidden style="color:#f6768a;border-color:rgba(246,118,138,.35)">Uninstall</button> <span class="track"><span class="knob"></span></span>
<button id="ariadneRefresh" class="btn">Refresh</button> </label>
</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>
</div> </div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
<div class="txt"> <!-- Aegis -->
<div class="t">Built-in wallet — Aegis</div> <div class="row plug-row">
<div class="d"> <div class="txt plug-open" data-go="plugins/aegis" role="button" tabindex="0" title="Open Aegis settings">
Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) that ships in the box. <div class="t">Aegis <span class="pmuted" style="font-weight:400">— built-in wallet</span> <span class="chev">›</span></div>
Bundled with Theseus, but updates are delivered <b>over-the-air</b>: new signed <div class="d">Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB). Ships in the box, updates over-the-air.</div>
versions land without a Theseus release. Runs entirely inside this browser — <div id="aegisStatusMain" class="pmuted" style="font-size:12.5px;margin-top:4px">loading…</div>
never system-wide.
</div>
</div> </div>
<div id="aegisStatus" class="pmuted" style="font-size:12.5px">Loading…</div> <div class="plug-ctl">
<div style="display:flex;gap:8px;flex-wrap:wrap"> <button id="aegisCheckMain" class="btn small">Check for updates</button>
<button id="aegisCheck" class="btn">Check for updates</button> <button id="aegisApplyMain" class="btn small" hidden>Apply update</button>
<button id="aegisApply" class="btn" hidden>Apply update now</button>
</div> </div>
</div> </div>
</section> </section>
<!-- PLUG-INS › ARIADNE (sub-page) -->
<section id="plugins-ariadne" class="subpage" data-parent="plugins" hidden>
<div class="crumb"><a data-go="plugins">Plug-ins</a> › Ariadne's Thread</div>
<h1><button class="back" data-go="plugins" title="Back to Plug-ins" aria-label="Back">‹</button>Ariadne's Thread</h1>
<p class="lede">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.</p>
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Status</h2>
<div class="row">
<div class="txt">
<div class="t">System-wide resolver</div>
<div class="d" id="ariadneStatusSub">checking…</div>
</div>
<label class="sw" title="Turn Ariadne on/off">
<input type="checkbox" id="ariadneToggleSub">
<span class="track"><span class="knob"></span></span>
</label>
</div>
<div id="ariadneMissing" class="pmuted" style="font-size:12.5px;margin:-4px 4px 8px" hidden>
Ariadne's Thread isn't installed on this machine. Click <b>Install</b> below to run
the bundled installer, or grab it manually from
<a href="https://silentmode.st/tools/" target="_blank" rel="noopener">silentmode.st/tools</a>.
</div>
<div class="row" style="justify-content:flex-end;gap:8px">
<button id="ariadneInstallSub" class="btn" hidden>Install</button>
<button id="ariadneUpdateSub" class="btn" hidden>Update</button>
<button id="ariadneRefreshSub" class="btn">Refresh</button>
</div>
<h2 class="sub">Remove</h2>
<p class="subd">Uninstall the resolver from this machine. Theseus itself keeps working — its built-in resolver isn't affected.</p>
<div class="row">
<div class="txt">
<div class="t">Uninstall Ariadne's Thread</div>
<div class="d">Removes the two scheduled tasks and deletes <code>C:\ProgramData\Ariadne\</code>. Prompts for admin.</div>
</div>
<button id="ariadneUninstallSub" class="btn" hidden style="color:#f6768a;border-color:rgba(246,118,138,.35)">Uninstall</button>
</div>
</section>
<!-- PLUG-INS › AEGIS (sub-page) -->
<section id="plugins-aegis" class="subpage" data-parent="plugins" hidden>
<div class="crumb"><a data-go="plugins">Plug-ins</a> › Aegis</div>
<h1><button class="back" data-go="plugins" title="Back to Plug-ins" aria-label="Back">‹</button>Aegis</h1>
<p class="lede">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.</p>
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Status</h2>
<div class="row">
<div class="txt">
<div class="t">Aegis wallet</div>
<div class="d" id="aegisStatusSub">loading…</div>
</div>
</div>
<div class="row" style="justify-content:flex-end;gap:8px">
<button id="aegisCheckSub" class="btn">Check for updates</button>
<button id="aegisApplySub" class="btn" hidden>Apply update now</button>
</div>
<div class="note" style="margin-top:1rem">Aegis lives in the Extensions folder alongside every other add-on; to remove it, use <a data-go="addons" style="color:var(--acid-text);cursor:pointer">Extensions</a>.</div>
</section>
<!-- SEARCH --> <!-- SEARCH -->
<section id="search" hidden> <section id="search" hidden>
<h1>Search</h1> <h1>Search</h1>
@ -895,6 +969,69 @@
el.checked = !!s[k]; el.checked = !!s[k];
el.addEventListener("change", () => C.set(k, el.checked)); el.addEventListener("change", () => C.set(k, el.checked));
} }
// ---- website language (General page) — friendly wrapper over the
// same languageMode/languageValue setting the Anti-fingerprinting Language
// row edits. "Auto" = languageMode="show" (follow OS); anything else
// switches to languageMode="manual" and pins languageValue.
const WEB_LANG_QUICK = [
{ tag: "en-US", label: "English (US)" }, { tag: "en-GB", label: "English (UK)" },
{ tag: "es-ES", label: "Español (España)" }, { tag: "es-MX", label: "Español (México)" },
{ tag: "pt-BR", label: "Português (Brasil)" }, { tag: "pt-PT", label: "Português (Portugal)" },
{ tag: "fr-FR", label: "Français" }, { tag: "de-DE", label: "Deutsch" },
{ tag: "it-IT", label: "Italiano" }, { tag: "nl-NL", label: "Nederlands" },
{ tag: "ru-RU", label: "Русский" }, { tag: "uk-UA", label: "Українська" },
{ tag: "pl-PL", label: "Polski" }, { tag: "cs-CZ", label: "Čeština" },
{ tag: "sv-SE", label: "Svenska" }, { tag: "fi-FI", label: "Suomi" },
{ tag: "tr-TR", label: "Türkçe" }, { tag: "el-GR", label: "Ελληνικά" },
{ tag: "he-IL", label: "עברית" }, { tag: "ar", label: "العربية" },
{ tag: "hi-IN", label: "हिन्दी" }, { tag: "zh-CN", label: "中文(简体)" },
{ tag: "zh-TW", label: "中文(繁體)" }, { tag: "ja-JP", label: "日本語" },
{ tag: "ko-KR", label: "한국어" }, { tag: "vi-VN", label: "Tiếng Việt" },
{ tag: "th-TH", label: "ไทย" }, { tag: "id-ID", label: "Bahasa Indonesia" },
];
(async () => {
const webLangPick = document.getElementById("webLangPick");
const webLangOther = document.getElementById("webLangOther");
const hint = document.getElementById("webLangHint");
if (!webLangPick) return;
let osLoc = "en-US";
try { osLoc = (C.systemLocale && await C.systemLocale()) || "en-US"; } catch {}
// Build options: "Automatic" first, then the quick list, then "Other…".
const opts = [`<option value="__auto__">Automatic (OS: ${osLoc})</option>`,
...WEB_LANG_QUICK.map((L) => `<option value="${L.tag}">${L.label} — ${L.tag}</option>`),
`<option value="__other__">Other…</option>`];
webLangPick.innerHTML = opts.join("");
const paint = (cur) => {
const mode = cur.languageMode || "show";
if (mode !== "manual") {
webLangPick.value = "__auto__"; webLangOther.hidden = true;
hint.innerHTML = mode === "show"
? "Following your operating system. Also switchable from the globe icon in the address bar."
: `Anti-fingerprinting is currently overriding this (mode: <b>${mode}</b>). Change it under <b>Privacy → Anti-fingerprinting</b>.`;
return;
}
const tag = cur.languageValue || "en-US";
const known = new Set(WEB_LANG_QUICK.map((L) => L.tag));
if (known.has(tag)) { webLangPick.value = tag; webLangOther.hidden = true; }
else { webLangPick.value = "__other__"; webLangOther.hidden = false; webLangOther.value = tag; }
hint.textContent = `Websites see you in ${tag}. Also switchable from the globe icon in the address bar.`;
};
paint(s);
webLangPick.addEventListener("change", () => {
const v = webLangPick.value;
if (v === "__auto__") { C.set("languageMode", "show"); webLangOther.hidden = true; }
else if (v === "__other__") { webLangOther.hidden = false; setTimeout(() => webLangOther.focus(), 0); }
else { C.set("languageMode", "manual"); C.set("languageValue", v); webLangOther.hidden = true; }
});
webLangOther.addEventListener("change", () => {
const v = String(webLangOther.value || "").trim();
if (!v) return;
C.set("languageMode", "manual"); C.set("languageValue", v);
});
// Keep in sync when the toolbar chip or the Anti-fingerprinting row
// writes to the same underlying setting.
if (C.onSettingsUpdate) C.onSettingsUpdate((next) => paint(next));
})();
// search engines: default picker + custom-engine list + add form // search engines: default picker + custom-engine list + add form
const sel = document.getElementById("searchEngine"); const sel = document.getElementById("searchEngine");
const esc = (s) => String(s || "").replace(/</g, "&lt;"); const esc = (s) => String(s || "").replace(/</g, "&lt;");
@ -1280,131 +1417,199 @@
r.addEventListener("change", () => { if (r.checked) C.set("devToolsDock", r.value); }); r.addEventListener("change", () => { if (r.checked) C.set("devToolsDock", r.value); });
}); });
// ---- Ariadne's Thread system-wide resolver toggle ----------------------- // ---- Plug-ins: Ariadne's Thread + Aegis --------------------------------
// Ariadne runs as two elevated Scheduled Tasks ("BNS Resolver Daemon" + // Two views per plug-in that share one truth source:
// "BNS Sia Bridge"). Toggling requires admin — main spawns an elevated // Main page (#plugins) — compact row: status + update button
// powershell for start/stop, which prompts UAC once per action. // + on/off toggle on the right; title
const arStatus = document.getElementById("ariadneStatus"); // is a link into the sub-page.
const arStart = document.getElementById("ariadneStart"); // Sub-page (#plugins-ariadne, #plugins-aegis) — full details + the
const arStop = document.getElementById("ariadneStop"); // uninstall / apply buttons.
const arInstall = document.getElementById("ariadneInstall"); // Every state-changing IPC re-runs the shared refresh, so both views
const arUpdate = document.getElementById("ariadneUpdate"); // repaint together — the toggle in the main row and in the sub-page
const arUninstall = document.getElementById("ariadneUninstall"); // reflect the same underlying scheduled-task state.
const arRefresh = document.getElementById("ariadneRefresh"); (function () {
const arMissing = document.getElementById("ariadneMissing"); // ARIADNE ————————————————————————————————————————————————————————
async function refreshAriadne() { // Runs as two elevated scheduled tasks ("BNS Resolver Daemon" + "BNS
arStatus.textContent = "checking…"; // Sia Bridge"). Toggling / installing / uninstalling prompts UAC.
arStart.hidden = arStop.hidden = arInstall.hidden = arUpdate.hidden = arUninstall.hidden = arMissing.hidden = true; const q = (id) => document.getElementById(id);
try { const arStatMain = q("ariadneStatusMain");
const r = await C.ariadneState(); const arStatSub = q("ariadneStatusSub");
const verSuffix = r.installedVersion ? ` (v${r.installedVersion})` : ""; const arSwMain = q("ariadneToggleMain");
const bundledSuffix = r.bundledVersion ? ` (bundled v${r.bundledVersion})` : ""; const arSwSub = q("ariadneToggleSub");
if (r.state === "running") { const arSwWrap = q("ariadneSwWrap");
arStatus.innerHTML = `Status: <b style="color:var(--acid-text)">running</b>${verSuffix} — every browser on this machine resolves BCDN names.`; const arInstallMain = q("ariadneInstallMain");
arStop.hidden = false; arUninstall.hidden = !r.hasUninstaller; const arInstallSub = q("ariadneInstallSub");
if (r.canUpdate) arUpdate.hidden = false; const arUpdateMain = q("ariadneUpdateMain");
} else if (r.state === "stopped") { const arUpdateSub = q("ariadneUpdateSub");
arStatus.innerHTML = `Status: <b>stopped</b>${verSuffix} — only Theseus resolves BCDN names; other browsers won't.`; const arCheckMain = q("ariadneCheckMain");
arStart.hidden = false; arUninstall.hidden = !r.hasUninstaller; const arRefreshSub = q("ariadneRefreshSub");
if (r.canUpdate) arUpdate.hidden = false; const arUninstall = q("ariadneUninstallSub");
} else { const arMissing = q("ariadneMissing");
arStatus.innerHTML = `Status: <b>not installed</b> on this machine.${bundledSuffix}`; let arBusy = false; // during install/update/toggle: freeze the switch so refreshes don't fight
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();
// ---- Aegis update card -------------------------------------------------- async function refreshAriadne() {
// Aegis is a bundled add-on but its updates are shipped OTA via the signed try {
// add-on update endpoint, so users can get new wallet versions without const r = await C.ariadneState();
// updating Theseus. The same addons-check-updates IPC the Extensions page const verSuffix = r.installedVersion ? ` (v${r.installedVersion})` : "";
// uses is filtered for the aegis id so this card only speaks for Aegis. const bundledSuffix = r.bundledVersion ? ` (bundled v${r.bundledVersion})` : "";
// Legacy id "bchwallet" is also matched for upgrades from pre-rename // Main row: short one-liner.
// installs (0.3.x); post-migration the entry lives at id="aegis". if (r.state === "running") {
const aegisStatus = document.getElementById("aegisStatus"); arStatMain.innerHTML = `<b style="color:var(--acid-text)">On</b>${verSuffix} — resolving names system-wide.`;
const aegisCheck = document.getElementById("aegisCheck"); } else if (r.state === "stopped") {
const aegisApply = document.getElementById("aegisApply"); arStatMain.innerHTML = `<b>Off</b>${verSuffix} — only Theseus resolves BCDN names.`;
const isAegis = (a) => a && (a.id === "aegis" || a.id === "bchwallet"); } else {
// addons-list returns {installed:[...], sidebarPanels:[...]} arStatMain.innerHTML = `<b>Not installed</b>.${bundledSuffix}`;
// addons-check-updates returns {report, skipped, staged:[...]} }
// addons-list-staged returns a flat [...]. Normalise both call sites. // Sub-page row: fuller description.
async function refreshAegis() { if (r.state === "running") {
aegisApply.hidden = true; arStatSub.innerHTML = `<b style="color:var(--acid-text)">Running</b>${verSuffix} — every browser on this machine resolves BCDN names.`;
try { } else if (r.state === "stopped") {
const [listRes, stagedRes] = await Promise.all([ arStatSub.innerHTML = `<b>Stopped</b>${verSuffix} — only Theseus resolves BCDN names; other browsers won't.`;
C.listAddons ? C.listAddons() : Promise.resolve({}), } else {
C.listStagedAddonUpdates ? C.listStagedAddonUpdates() : Promise.resolve([]), arStatSub.innerHTML = `<b>Not installed</b> on this machine.${bundledSuffix}`;
]); }
const installed = (listRes && listRes.installed) || []; // Toggle state on both views. When not installed the toggle is
const staged = Array.isArray(stagedRes) ? stagedRes : []; // disabled — user has to hit Install first.
const cur = installed.find(isAegis); if (!arBusy) {
const upd = staged.find(isAegis); const on = r.state === "running";
if (!cur) { aegisStatus.textContent = "Aegis isn't installed. Reinstall Theseus to add it back, or open Settings > Extensions to load it manually."; return; } const installed = r.state !== "not-installed";
const curVer = cur.version || "?"; arSwMain.checked = arSwSub.checked = on;
if (upd && upd.version) { arSwMain.disabled = arSwSub.disabled = !installed;
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}.`; if (arSwWrap) arSwWrap.title = installed ? "Turn Ariadne on/off" : "Install Ariadne first";
aegisApply.hidden = false; }
} else { // Install / Update buttons: install only when missing, update only
aegisStatus.textContent = `You're on v${curVer}. Updates arrive over-the-air; the next check runs at boot and every 6h.`; // when an update is available.
const missing = r.state === "not-installed";
arInstallMain.hidden = arInstallSub.hidden = !missing;
arUpdateMain.hidden = arUpdateSub.hidden = !(r.canUpdate && !missing);
arUninstall.hidden = !r.hasUninstaller;
arMissing.hidden = !missing;
} catch (e) {
const msg = "Status check failed: " + (e?.message || e);
arStatMain.textContent = msg; arStatSub.textContent = msg;
} }
} catch (e) { aegisStatus.textContent = "Status check failed: " + (e?.message || e); } }
} function wire(btn, action, busyLabel) {
aegisCheck.onclick = async () => { if (!btn) return;
const orig = aegisCheck.textContent; btn.onclick = async () => {
aegisCheck.disabled = true; aegisCheck.textContent = "Checking…"; const orig = btn.textContent;
aegisStatus.textContent = "Checking…"; btn.disabled = true; btn.textContent = busyLabel;
try { arBusy = true;
const res = await (C.checkAddonUpdates ? C.checkAddonUpdates() : Promise.resolve({})); try { await action(); } catch {}
const staged = (res && Array.isArray(res.staged)) ? res.staged : []; finally { arBusy = false; btn.disabled = false; btn.textContent = orig; refreshAriadne(); }
const upd = staged.find(isAegis); };
if (!upd) { aegisStatus.textContent = "You're on the latest Aegis."; } }
// refresh will populate everything else + toggle the Apply button // Toggle: uses whichever switch the user flipped, mirrors the other.
refreshAegis(); async function toggle(desired) {
} catch (e) { aegisStatus.textContent = "Check failed: " + (e?.message || e); } arBusy = true;
finally { aegisCheck.disabled = false; aegisCheck.textContent = orig; } arSwMain.disabled = arSwSub.disabled = true;
}; try { await C.ariadneToggle(desired); }
// Apply staged update without restarting Theseus. The main-process catch {}
// handler promotes staged → active, tears down + rebuilds the addon finally { arBusy = false; refreshAriadne(); }
// 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 arSwMain.addEventListener("change", () => toggle(arSwMain.checked));
// wired (older Theseus builds). arSwSub.addEventListener("change", () => toggle(arSwSub.checked));
aegisApply.onclick = async () => { wire(arInstallMain, () => C.ariadneInstall(), "Installing…");
const orig = aegisApply.textContent; wire(arInstallSub, () => C.ariadneInstall(), "Installing… (accept the UAC prompt)");
aegisApply.disabled = true; aegisApply.textContent = "Applying…"; wire(arUpdateMain, () => C.ariadneUpdate(), "Updating…");
try { wire(arUpdateSub, () => C.ariadneUpdate(), "Updating… (accept the UAC prompt)");
if (C && C.applyStagedAddons) { wire(arUninstall, () => C.ariadneUninstall(), "Uninstalling… (accept the UAC prompt)");
const r = await C.applyStagedAddons(); arCheckMain.onclick = refreshAriadne;
if (r && r.ok === false) throw new Error(r.err || "apply failed"); arRefreshSub.onclick = refreshAriadne;
aegisStatus.textContent = "Update applied. Reopen the Aegis panel to see the new version."; refreshAriadne();
refreshAegis();
} else if (C && C.restartApp) { // AEGIS ——————————————————————————————————————————————————————————
C.restartApp(); // Bundled add-on shipped OTA. Same addons-check-updates IPC the
} else { // Extensions page uses, filtered for the aegis id. Legacy id
aegisStatus.textContent = "Update staged — restart Theseus to switch."; // "bchwallet" still matched for upgrades from pre-rename installs.
const aeStatMain = q("aegisStatusMain");
const aeStatSub = q("aegisStatusSub");
const aeCheckMain = q("aegisCheckMain");
const aeCheckSub = q("aegisCheckSub");
const aeApplyMain = q("aegisApplyMain");
const aeApplySub = q("aegisApplySub");
const isAegis = (a) => a && (a.id === "aegis" || a.id === "bchwallet");
async function refreshAegis() {
aeApplyMain.hidden = aeApplySub.hidden = true;
try {
const [listRes, stagedRes] = await Promise.all([
C.listAddons ? C.listAddons() : Promise.resolve({}),
C.listStagedAddonUpdates ? C.listStagedAddonUpdates() : Promise.resolve([]),
]);
const installed = (listRes && listRes.installed) || [];
const staged = Array.isArray(stagedRes) ? stagedRes : [];
const cur = installed.find(isAegis);
const upd = staged.find(isAegis);
if (!cur) {
const msg = "Aegis isn't installed. Reinstall Theseus to add it back, or open Extensions to load it manually.";
aeStatMain.textContent = msg; aeStatSub.textContent = msg;
return;
}
const curVer = cur.version || "?";
if (upd && upd.version) {
aeStatMain.innerHTML = `<b style="color:var(--acid-text)">v${upd.version}</b> ready — you're on v${curVer}.`;
aeStatSub.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}.`;
aeApplyMain.hidden = aeApplySub.hidden = false;
} else {
aeStatMain.textContent = `On v${curVer} — updates over-the-air, checked at boot and every 6h.`;
aeStatSub.textContent = `You're on v${curVer}. Updates arrive over-the-air; the next check runs at boot and every 6h.`;
}
} catch (e) {
const msg = "Status check failed: " + (e?.message || e);
aeStatMain.textContent = msg; aeStatSub.textContent = msg;
} }
} catch (e) { }
aegisStatus.textContent = "Apply failed: " + (e?.message || e) + " — restart Theseus to fall back."; function wireCheck(btn) {
} finally { aegisApply.disabled = false; aegisApply.textContent = orig; } btn.onclick = async () => {
}; const orig = btn.textContent;
refreshAegis(); btn.disabled = true; btn.textContent = "Checking…";
const noneMsg = "You're on the latest Aegis.";
try {
const res = await (C.checkAddonUpdates ? C.checkAddonUpdates() : Promise.resolve({}));
const staged = (res && Array.isArray(res.staged)) ? res.staged : [];
if (!staged.find(isAegis)) {
aeStatMain.textContent = noneMsg; aeStatSub.textContent = noneMsg;
}
refreshAegis();
} catch (e) {
const msg = "Check failed: " + (e?.message || e);
aeStatMain.textContent = msg; aeStatSub.textContent = msg;
}
finally { btn.disabled = false; btn.textContent = orig; }
};
}
wireCheck(aeCheckMain);
wireCheck(aeCheckSub);
// Apply staged update without restarting Theseus. Falls back to a
// full restart when the hot-apply IPC isn't wired (older Theseus).
function wireApply(btn) {
btn.onclick = async () => {
const orig = btn.textContent;
btn.disabled = true; btn.textContent = "Applying…";
try {
if (C && C.applyStagedAddons) {
const r = await C.applyStagedAddons();
if (r && r.ok === false) throw new Error(r.err || "apply failed");
const msg = "Update applied. Reopen the Aegis panel to see the new version.";
aeStatMain.textContent = msg; aeStatSub.textContent = msg;
refreshAegis();
} else if (C && C.restartApp) {
C.restartApp();
} else {
const msg = "Update staged — restart Theseus to switch.";
aeStatMain.textContent = msg; aeStatSub.textContent = msg;
}
} catch (e) {
const msg = "Apply failed: " + (e?.message || e) + " — restart Theseus to fall back.";
aeStatMain.textContent = msg; aeStatSub.textContent = msg;
} finally { btn.disabled = false; btn.textContent = orig; }
};
}
wireApply(aeApplyMain);
wireApply(aeApplySub);
refreshAegis();
})();
// ---- Manual "Check for updates" ----------------------------------------- // ---- Manual "Check for updates" -----------------------------------------
// Fires an IPC that un-dismisses any lingering session chip and hits the // Fires an IPC that un-dismisses any lingering session chip and hits the