Theseus 0.3.70: fully-lazy session restore, quick-links strip reordered

Session restore no longer loads any page on launch. In 0.3.63 the strip was
built from saved titles + favicons and only the previously-active tab's URL
was navigated at startup, so a 20-tab session cost one renderer load instead
of twenty — but that one load is still a real page, often the heaviest one
in the whole session, and it fought every other startup task for the main
thread while the window sat not-responding. Now every restored tab, the
previously-active one included, comes up dormant: zero page renderers at
launch, no page starts loading until the user asks for a specific tab
(clicks the chip, hits reload, types in the URL bar). The previously-active
tab stays highlighted in the strip so one click brings it back; the content
area sits with the tab's own background colour until that click. RAM-at-
launch is now just the chrome, the overlays and the strip — a 500 MB saved
page never materialises as a renderer the user did not even ask to see.

A pending tab that is navigated explicitly (URL bar, link, search) drops
its saved URL at the top of navigateTab, so a later reload or chip click
can't snap it back.

Quick-links strip default set is now Telegram, WhatsApp, X, YouTube — in
that order. Messenger and Spotify are out of the default; users who want
them can still add them via Settings › General › Quick links. Existing
installs whose list still matches the previous untouched default (same six
ids in the same order) migrate on next launch; any customisation (reorder,
add, remove) is left alone.
This commit is contained in:
Silent Mode 2026-10-03 11:03:53 +02:00
parent f2ff48b977
commit c2ec0f0d02
2 changed files with 42 additions and 20 deletions

60
main.js
View file

@ -522,12 +522,10 @@ const SETTINGS_DEFAULTS = {
// service shortcuts; click opens the URL in a new tab. Toggle via settings.
quickLinksShow: true,
quickLinks: [
{ id: "messenger", url: "https://www.messenger.com/", title: "Messenger" },
{ id: "whatsapp", url: "https://web.whatsapp.com/", title: "WhatsApp" },
{ id: "telegram", url: "https://web.telegram.org/k/", title: "Telegram" },
{ id: "whatsapp", url: "https://web.whatsapp.com/", title: "WhatsApp" },
{ id: "x", url: "https://x.com/", title: "X" },
{ id: "youtube", url: "https://www.youtube.com/", title: "YouTube" },
{ id: "spotify", url: "https://open.spotify.com/", title: "Spotify" },
],
// Toolbar bar sizes. The URL bar is flex:1 by default so it eats all
// remaining space; `compact`/`medium` cap it so the extension dock has
@ -580,6 +578,17 @@ function loadSettings() {
settings.searchEngine = enabled.find((x) => !isFrozen(x)) || DEFAULT_ENABLED[0];
}
} catch (e) { console.warn("engine normalize:", e?.message); }
// Quick-links default set changed in 0.3.70: drop Messenger + Spotify and
// reorder to Telegram, WhatsApp, X, YouTube. Users who kept the previous
// untouched default (same 6 ids, same order) move to the new set; any
// customisation (reorder, add, remove) is left alone.
try {
const PRIOR = ["messenger", "whatsapp", "telegram", "x", "youtube", "spotify"];
const have = Array.isArray(settings.quickLinks) ? settings.quickLinks.map((L) => String(L?.id || "")) : [];
if (have.length === PRIOR.length && have.every((id, i) => id === PRIOR[i])) {
settings.quickLinks = SETTINGS_DEFAULTS.quickLinks.map((L) => ({ ...L }));
}
} catch (e) { console.warn("quickLinks migrate:", e?.message); }
}
function saveSettings() {
try { fs.writeFileSync(settingsFile(), JSON.stringify(settings, null, 2)); } catch (e) { console.error("settings save failed:", e.message); }
@ -1272,26 +1281,35 @@ function loadSession() {
return { tabs: urls.map((url) => ({ url, title: "", favicon: null })), active };
} catch { return empty; }
}
// Session restore, LAZY: only the active tab actually loads a page at startup.
// Every other restored tab is a dormant WebContentsView in the strip — title
// and favicon from the saved session — and only navigates when the user clicks
// it (setActive consumes `pending`). This drops N renderer loads at startup
// to 1, so launch time is roughly flat whether the user has 2 or 25 tabs.
// Session restore, FULLY LAZY: nothing navigates at launch. Every restored
// tab — including the one that was active at close — comes up as a dormant
// WebContentsView in the strip, painted from the saved title + favicon, and
// only triggers a page load when the user asks for it specifically (clicks
// the chip, hits reload, types in the URL bar). This drops the whole startup
// renderer load to zero page renderers (chrome.html + the overlays + the
// strip only), so launch is near-flat whether the session has 2 or 50 tabs
// and the RAM-at-launch footprint is just the chrome plus the strip —
// a saved heavy page doesn't come back as a 500 MB renderer the user
// didn't even ask to see. The previously active tab stays highlighted in
// the strip so the user can one-click it back; its content area sits with
// the tab's solid background until that happens.
function restoreTabs() {
const saved = settings.restoreSession ? loadSession() : { tabs: [], active: 0 };
if (!saved.tabs.length) { createTab(); return; }
const activeMeta = saved.tabs[saved.active] || saved.tabs[0];
createTab(activeMeta.url);
// Build the rest as lazy. Insert at their saved slot so the strip order
// matches the save.
for (let i = 0; i < saved.tabs.length; i++) {
if (i === saved.active) continue;
try {
createTab(null, { background: true, pending: saved.tabs[i] });
// createTab appends; move it into position.
const t = tabs.pop();
tabs.splice(Math.min(i, tabs.length), 0, t);
} catch (e) { console.warn("session restore: tab failed:", e?.message); }
try { createTab(null, { background: true, pending: saved.tabs[i] }); }
catch (e) { console.warn("session restore: tab failed:", e?.message); }
}
// Pick the saved active slot as the chip the strip highlights at launch.
// A plain setActive() would materialise it (defeating the point of fully-
// lazy restore), so apply activeId + visibility by hand — the user
// clicking that same chip is what triggers the first navigation.
const idx = Math.min(Math.max(0, saved.active | 0), tabs.length - 1);
const target = tabs[idx];
if (target) {
activeId = target.id;
try { target.view.setVisible(true); } catch {}
for (const t of tabs) if (t.id !== activeId) { try { t.view.setVisible(false); } catch {} }
}
emitTabs();
}
@ -4089,6 +4107,10 @@ function createWindow() {
async function navigateTab(id, input) {
const t = tabById(id); if (!t) return;
// A pending tab the user navigates explicitly (URL bar, link follow,
// search) has outgrown its saved URL — otherwise a later reload or chip
// click would snap it back to that stale URL via materializePending.
if (t.pending) t.pending = null;
let q = String(input).trim();
if (!q) return;
// theseus://extensions/install/<id> typed or pasted into the address bar.

View file

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