diff --git a/bundled-addons/pithos/addon.json b/bundled-addons/pithos/addon.json
index 61ad32fd..04b6be20 100644
--- a/bundled-addons/pithos/addon.json
+++ b/bundled-addons/pithos/addon.json
@@ -6,6 +6,7 @@
"author": "Silent Mode",
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
"main": "index.js",
+ "activation": "on-demand",
"capabilities": [
"sidebar-panel",
"vault-derive",
diff --git a/home.html b/home.html
index 81f658e5..94e2d1b0 100644
--- a/home.html
+++ b/home.html
@@ -107,9 +107,9 @@
specific slot. Order matches the grid's cell index. Subtly visible
so users see where they can drop; brighter on hover of the strip. */
.slot-empty { display: block; width: 48px; height: 48px; margin: 0 auto;
- border-radius: 10px; border: 2px dashed rgb(from var(--line2) r g b / .55);
+ border-radius: 10px; border: 2px dashed transparent;
background: transparent; transition: border-color .12s, background .12s; }
- .slots-list:hover .slot-empty { border-color: var(--line2); }
+ .slots-list:hover .slot-empty { border-color: rgb(from var(--line2) r g b / .55); }
.slot-empty.drop-hot { border-color: var(--acid); background: rgb(from var(--acid) r g b / .06); }
/* RIGHT COLUMN — just the 3-column grid of Silent Mode cards; search
@@ -148,11 +148,15 @@
}
.card.drag-before::before { left: -8px; }
.card.drag-after::after { right: -8px; }
- .card-add { display: flex; align-items: center; justify-content: center; min-height: 128px;
- border: 2px dashed var(--line2); border-radius: 14px; background: transparent;
+ /* Matches .card's horizontal shape so a "+ Add card" cell sits in the
+ grid as a peer of the real cards instead of a short dashed strip. */
+ .card-add { display: flex; align-items: center; justify-content: center;
+ gap: 14px; padding: 14px 16px; min-height: 92px;
+ border: 2px dashed var(--line2); border-radius: 14px;
+ background: rgba(20,26,36,.35); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
color: var(--mut); font-size: 13.5px; cursor: pointer;
transition: color .12s, border-color .12s, background .12s; }
- .card-add:hover { color: var(--acid); border-color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
+ .card-add:hover { color: var(--acid); border-color: var(--acid); background: rgb(from var(--acid) r g b / .06); }
/* Context menus. One for the background (edit actions), one for images
(copy / edit / remove). Positioned at the pointer by JS. */
@@ -237,11 +241,9 @@
-
Your links
-
Silent Mode
diff --git a/main.js b/main.js
index 23b48e28..2ea4082a 100644
--- a/main.js
+++ b/main.js
@@ -549,6 +549,8 @@ const SETTINGS_DEFAULTS = {
// extension's activate() runs at launch, before the window exists.
extensionsOnDemand: true, // extensions whose manifest allows it start on first use
walletAtLaunch: false, // start Aegis at launch even once it allows on-demand (no effect while it is startup-only)
+ pithosAtLaunch: false, // start Pithos at launch (otherwise it wakes on first panel open or a pithos.sia navigation)
+ hermesAutoRestore: true, // open your Nostr relays 1.5s after launch so Hermes messages arrive before you open the app (off = relays connect on first open)
preloadMenus: true, // load address suggestions / link pill / site info after the first page (prewarmOverlays)
addonsStartAtLaunch: [], // on-demand add-ons that asked (api.startAtLaunch) to start at launch anyway
// Storage retention — nothing persists by default. Auto-clear on quit
@@ -3280,6 +3282,7 @@ function initAddons() {
if (m.activation !== "on-demand") return "startup";
if (settings.extensionsOnDemand === false) return "startup";
if (m.id === "aegis" && settings.walletAtLaunch) return "startup";
+ if (m.id === "pithos" && settings.pithosAtLaunch) return "startup";
if (Array.isArray(settings.addonsStartAtLaunch) && settings.addonsStartAtLaunch.includes(m.id)) return "startup";
return "on-demand";
},
@@ -9705,6 +9708,7 @@ ipcMain.handle("settings-set", (_e, key, val) => {
// leaves running add-ons alone; it applies from the next launch.
if (addonHost && key === "extensionsOnDemand" && val === false) addonHost.activateAllDormant("setting").catch(() => {});
if (addonHost && key === "walletAtLaunch" && val && addonHost.isDormant("aegis")) addonHost.ensureActive("aegis", "setting").catch(() => {});
+ if (addonHost && key === "pithosAtLaunch" && val && addonHost.isDormant("pithos")) addonHost.ensureActive("pithos", "setting").catch(() => {});
if (key === "preloadMenus" && val && chromeReadyDone) prewarmOverlays();
if (key === "freezeBackgroundTabs") applyFreezeSetting();
if (["timezoneMode", "timezoneValue", "languageMode", "languageSpoof", "languageValue",
@@ -10648,7 +10652,14 @@ async function hermesAutoRestore() {
}
// Fire a little after app ready so loadHermesLib's dynamic import and
// safeStorage both exist. Non-blocking.
-app.whenReady().then(() => setTimeout(() => { void hermesAutoRestore(); }, 1500));
+app.whenReady().then(() => setTimeout(() => {
+ // Gated so a user who doesn't want background relay traffic (laptop on
+ // battery, metered connection, or they just don't use Hermes) can opt
+ // out in Settings › Performance › Startup. Default on so first-run
+ // users keep getting messages before they open the panel.
+ if (settings.hermesAutoRestore === false) return;
+ void hermesAutoRestore();
+}, 1500));
const hOk = (o = {}) => ({ ok: true, ...o });
const hErr = (e) => ({ ok: false, err: String((e && e.message) || e) });
@@ -10729,17 +10740,44 @@ async function hermesReverseResolve(pkHex) {
// One receive subscription per relay. If the socket dies we resurrect it on a
// backoff — a locked / logged-out state tears them all down cleanly.
+// Zombie-socket mitigation: ping every PING_MS, close + reconnect if no pong
+// within PONG_TIMEOUT_MS. Without this the TCP state can be "connected" while
+// the relay has silently stopped delivering, and new incoming kind:1059s only
+// arrive after the user's next outbound send forces a write-error and triggers
+// close → reconnect. Nasty invisible lag on long-lived Electron sessions.
+const HERMES_PING_MS = 30_000;
+const HERMES_PONG_TIMEOUT_MS = 10_000;
async function hermesConnectRelay(url) {
const H = await loadHermesLib();
- const state = { ws: null, ready: false, subId: "hermes-inbox" };
+ const state = { ws: null, ready: false, subId: "hermes-inbox", pingTimer: null, pongTimer: null };
const open = () => {
if (!hermesState) return; // torn down while reconnecting
const ws = new WebSocket(url);
state.ws = ws;
+ const clearPingers = () => {
+ if (state.pingTimer) { clearInterval(state.pingTimer); state.pingTimer = null; }
+ if (state.pongTimer) { clearTimeout(state.pongTimer); state.pongTimer = null; }
+ };
+ const schedulePing = () => {
+ clearPingers();
+ state.pingTimer = setInterval(() => {
+ try { ws.ping(); } catch { return; }
+ if (state.pongTimer) clearTimeout(state.pongTimer);
+ state.pongTimer = setTimeout(() => {
+ // No pong in time → the socket is zombie. Terminate forces an
+ // immediate close event which drives the reconnect loop below.
+ try { ws.terminate(); } catch {}
+ }, HERMES_PONG_TIMEOUT_MS);
+ }, HERMES_PING_MS);
+ };
ws.on("open", () => {
state.ready = true;
hermesEmitStatus();
ws.send(JSON.stringify(["REQ", state.subId, { kinds: [1059], "#p": [hermesState.pkHex] }]));
+ schedulePing();
+ });
+ ws.on("pong", () => {
+ if (state.pongTimer) { clearTimeout(state.pongTimer); state.pongTimer = null; }
});
ws.on("message", async (buf) => {
let msg; try { msg = JSON.parse(buf.toString()); } catch { return; }
@@ -10775,6 +10813,7 @@ async function hermesConnectRelay(url) {
});
ws.on("close", () => {
state.ready = false;
+ clearPingers();
hermesEmitStatus();
// Attempt reconnect if we're still supposed to be running.
if (hermesState && hermesState.relays.get(url) === state) {
@@ -10790,6 +10829,8 @@ async function hermesConnectRelay(url) {
function hermesTeardown() {
if (!hermesState) return;
for (const state of hermesState.relays.values()) {
+ if (state.pingTimer) { try { clearInterval(state.pingTimer); } catch {} state.pingTimer = null; }
+ if (state.pongTimer) { try { clearTimeout(state.pongTimer); } catch {} state.pongTimer = null; }
try { state.ws?.close(1000); } catch {}
}
hermesState = null;
diff --git a/package.json b/package.json
index 5457ebf6..71149242 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
- "version": "0.3.111",
+ "version": "0.3.112",
"license": "MPL-2.0",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",
diff --git a/quicklinks.html b/quicklinks.html
index 0d8f545c..f834d45a 100644
--- a/quicklinks.html
+++ b/quicklinks.html
@@ -64,7 +64,7 @@
-
+ ✨AI▾Sidebar settings
@@ -91,6 +91,9 @@
const r = llmBtn.getBoundingClientRect();
Q && Q.llmMenu && Q.llmMenu({ x: r.left, y: r.top, width: r.width, height: r.height });
});
+ llmChev.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); const r = llmBtn.getBoundingClientRect(); Q && Q.llmMenu && Q.llmMenu({ x: r.left, y: r.top, width: r.width, height: r.height }); }
+ });
llmChev.addEventListener("click", (e) => {
e.stopPropagation();
const r = llmBtn.getBoundingClientRect();
diff --git a/settings.html b/settings.html
index 0e363f86..2a9a3731 100644
--- a/settings.html
+++ b/settings.html
@@ -887,6 +887,14 @@
Start the wallet at launch
Aegis is ready the moment a site asks for it, at the cost of a slower start.
+
+
Start Pithos at launch
Pithos boots its control server for the Sia S3 gateway right away. Off: it wakes on first panel open or when you visit a pithos.sia page.
+
+
+
+
Connect Hermes relays at launch
Open your Nostr relays shortly after launch so Hermes messages arrive before you open the panel. Off: relays connect the first time you open Messages.
+
+
Preload common menus
After the first page loads, get address suggestions, the link preview and site info ready in the background so they open instantly. Off saves memory; each opens a moment slower the first time.