diff --git a/home.html b/home.html
index 97f8d2b7..cc89cd7c 100644
--- a/home.html
+++ b/home.html
@@ -327,6 +327,7 @@
{ title: "Sirius.X", url: "https://sirius.x/", brand: "sirius", sub: "Register and manage BCDN names." },
{ title: "Hephaestus.X", url: "https://hephaestus.x/", brand: "hephaestus", sub: "The forge — Silent Mode's code host." },
{ title: "Prometheus.X", url: "https://prometheus.x/", brand: "prometheus", sub: "Decentralized app marketplace." },
+ { title: "Hermes.X", url: "https://hermes.x/", brand: "hermes", sub: "E2E-encrypted messaging over Nostr." },
{ title: "Helios.X", url: "https://helios.x/", brand: "helios", sub: "Search engine for the decentralized web." },
{ title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne", sub: "BNS resolver — read .x and .bch names." },
{ title: "Aegis.X", url: "https://aegis.x/", brand: "aegis", sub: "Multi-chain wallet, derived from the vault." },
diff --git a/main.js b/main.js
index 0f80d985..f9cf224f 100644
--- a/main.js
+++ b/main.js
@@ -174,7 +174,7 @@ const SEARCH_ENGINES = {
};
// Engines enabled by default (shown in the toolbar dropdown). The rest are in the
// catalog and can be turned on from Settings. Custom + detected engines are always on.
-const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"];
+const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing", "perplexity"];
// A frozen catalog engine (`frozen: ""`) stays visible in Settings so the user sees why it
// is gone, but it is never enabled, never offered in the picker and never the default. Unfreeze by
// deleting the field; the user's installed list is left alone, so the row comes back as it was.
@@ -284,6 +284,27 @@ function allEngines() {
});
}
function enabledEnginesList() { return allEngines().filter((e) => e.enabled); }
+// Enabled LLM engines, in the user's display order. Powers the sidebar
+// strip's LLM button (0.3.100): the first entry is the fallback default,
+// and the "Pick…" menu lists everything here.
+function enabledLlms() { return enabledEnginesList().filter((e) => e.kind === "llm"); }
+// A home URL for an LLM engine so we can open its web UI in the panel.
+// Each engine's `fav` field is the hostname; the home page is the root.
+function llmHomeUrl(engineId) {
+ const def = SEARCH_ENGINES[engineId];
+ if (def && def.fav) return "https://" + def.fav + "/";
+ // Custom engines fall back to the origin of their URL template.
+ const custom = (settings.customEngines || []).find((e) => e.id === engineId);
+ if (custom && custom.url) try { return new URL(String(custom.url).replace(/\{q\}.*$/, "")).origin + "/"; } catch {}
+ return null;
+}
+function currentLlmId() {
+ const list = enabledLlms();
+ if (!list.length) return null;
+ const want = settings.defaultLlmId;
+ if (want && list.some((e) => e.id === want)) return want;
+ return list[0].id;
+}
// Region → 2-letter country code, for engines that accept a `gl`-style hint
// (Google's the notable one — without it Google may bounce a raw ?q= URL to
// a consent redirect or the region-detect start page instead of results).
@@ -505,6 +526,10 @@ const SETTINGS_DEFAULTS = {
installedEngines: DEFAULT_ENABLED.slice(), // built-in engines added to the user's list (visible in Settings)
enabledEngines: DEFAULT_ENABLED.slice(), // subset that's currently toggled on (shown in the toolbar dropdown)
engineOrder: [], // user-defined display order of engine ids (empty = natural)
+ // Sidebar strip's LLM button (0.3.100): which enabled LLM engine opens
+ // in the left panel when the user clicks the button. null = first in
+ // enabledLlms(); the right-click menu writes the user's pick here.
+ defaultLlmId: null,
customEngines: [], // user-added: [{ id, name, url-with-%s }]
theme: "dark", // dark | light | system — drives prefers-color-scheme in all views
// BCNR/ICANN collision policy (see SilentMode/Argus/DESIGN-collision-modes.md):
@@ -586,7 +611,6 @@ const SETTINGS_DEFAULTS = {
quickLinksShow: true,
quickLinks: [
{ id: "proton", url: "https://mail.proton.me/", title: "Proton Mail" },
- { id: "hermes", url: "https://hermes.x/", title: "Hermes" },
{ 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" },
@@ -675,7 +699,7 @@ function loadSettings() {
// itself). quickLinksOffered remembers it, so one the user removes stays
// removed. Kept out of SETTINGS_DEFAULTS so an old profile reads as "never offered".
try {
- const LATER_DEFAULTS = ["proton", "hermes"]; // proton 0.3.88 · hermes 0.3.99
+ const LATER_DEFAULTS = ["proton"]; // added in 0.3.88
const offered = Array.isArray(settings.quickLinksOffered) ? settings.quickLinksOffered.slice() : [];
const have = Array.isArray(settings.quickLinks) ? settings.quickLinks : null;
let changed = false;
@@ -683,19 +707,22 @@ function loadSettings() {
if (offered.includes(id)) continue;
offered.push(id); changed = true;
const def = SETTINGS_DEFAULTS.quickLinks.find((L) => L.id === id);
- // Proton Mail goes to the top of the web apps; Hermes slots in right
- // after Proton (keeping Silent-Mode-owned messaging near the top); any
- // other later one at the end.
+ // Proton Mail goes to the top of the web apps; any later one at the end.
if (have && def && !have.some((L) => L && L.id === id)) {
- if (id === "proton") have.unshift({ ...def });
- else if (id === "hermes") {
- const after = have.findIndex((L) => L && L.id === "proton");
- if (after >= 0) have.splice(after + 1, 0, { ...def });
- else have.unshift({ ...def });
- }
- else have.push({ ...def });
+ if (id === "proton") have.unshift({ ...def }); else have.push({ ...def });
}
}
+ // 0.3.99 one-time Hermes offer was retracted in 0.3.100 — the bundled
+ // Hermes extension already provides a left-strip entry (side: "left"), so
+ // the web quick-link was redundant. Yank it from any profile that caught
+ // it in 0.3.99 so the user doesn't end up with two Hermes buttons.
+ try {
+ const have2 = Array.isArray(settings.quickLinks) ? settings.quickLinks : null;
+ if (have2) {
+ const i = have2.findIndex((L) => L && L.id === "hermes");
+ if (i >= 0) { have2.splice(i, 1); changed = true; }
+ }
+ } catch (e) { console.warn("hermes retract:", e?.message); }
if (changed) { settings.quickLinksOffered = offered; saveSettings(); }
} catch (e) { console.warn("quickLinks offer:", e?.message); }
// Translator moved from a single `translateEndpoint` to a peer list in
@@ -9464,11 +9491,21 @@ ipcMain.handle("quicklinks-reorder", (e, order) => {
return true;
});
function quickLinksPayload() {
+ const llms = enabledLlms();
+ const curLlm = currentLlmId();
return {
order: stripOrder(),
links: visibleQuickLinks(),
openId: leftPanelId ? "p:" + leftPanelId : activeQuickLinkId,
panels: leftPanels().filter((p) => !leftPanelHidden(p.panelId)).map((p) => ({ id: "p:" + p.panelId, title: p.addonName || p.title, icon: p.icon || "" })),
+ // Sidebar strip's LLM button (0.3.100). The strip hides the button
+ // when `engines` is empty — nothing to open yet. `openId === "llm"`
+ // means the panel is currently showing the LLM so the button lights up.
+ llm: {
+ current: curLlm,
+ open: activeQuickLinkId === "llm",
+ engines: llms.map((e) => ({ id: e.id, name: e.name, sym: e.sym || "", favicon: e.favicon || "" })),
+ },
};
}
function emitQuickLinksState() {
@@ -9489,31 +9526,53 @@ const isHeaderSender = (e) => !!panelHeader && e.sender === panelHeader.webConte
ipcMain.handle("panel-header-state", (e) => isHeaderSender(e) ? panelHeaderState() : null);
ipcMain.handle("panel-header-max", (e) => isHeaderSender(e) && leftPanelKey() ? setLeftPanelMax(!leftPanelMax) : false);
ipcMain.handle("panel-header-close", (e) => { if (isHeaderSender(e)) hideLeftPanels(); return true; });
-// The gear above "+" on the strip: how panels sit next to the page, and the
-// Sidebar settings. A native menu, so it can open past the strip's edge.
+// The gear above "+" on the strip. Previously carried panel-mode radios + a
+// reset-widths item; those live on Settings › Sidebar now, so the gear is
+// a single-item shortcut. Kept as a native Menu so the click target stays
+// consistent with the rest of the strip's menus (right-click, LLM picker).
ipcMain.handle("quicklinks-menu", (e, rect) => {
if (!quicklinks || e.sender !== quicklinks.webContents || !primaryW?.win || primaryW.win.isDestroyed()) return false;
- const setMode = (mode) => {
- if (settings.sidebarPanelMode === mode) return;
- settings.sidebarPanelMode = mode;
- saveSettings();
- raisePanelGrip();
- layout();
- broadcastSettings();
- };
- const push = settings.sidebarPanelMode === "push";
const menu = Menu.buildFromTemplate([
- { label: "Panels open over the page", type: "radio", checked: !push, click: () => setMode("overlay") },
- { label: "Panels push the page aside", type: "radio", checked: push, click: () => setMode("push") },
- { type: "separator" },
- { label: "Reset panel widths", enabled: !!(settings.leftPanelWidths && Object.keys(settings.leftPanelWidths).length), click: () => { settings.leftPanelWidths = {}; saveSettings(); layout(); } },
- { label: "Sidebar settings…", click: () => openSettingsTab("sidebar") },
+ { label: "Open sidebar settings", click: () => openSettingsTab("sidebar") },
]);
const r = rect || {};
const y = Math.round(Number(r.y) || 0) + (primaryW.win === win ? CHROME_H : primaryW.chromeH || CHROME_H);
menu.popup({ window: primaryW.win, x: Math.round((Number(r.x) || 0) + (Number(r.width) || 0)), y });
return true;
});
+
+// LLM picker menu — right-click on the sidebar-strip's LLM button pops a
+// native menu of enabled LLM engines. Click one to set it as the default
+// AND open it in the panel; the strip button then reflects that engine.
+ipcMain.handle("quicklinks-llm-menu", (e, rect) => {
+ if (!quicklinks || e.sender !== quicklinks.webContents || !primaryW?.win || primaryW.win.isDestroyed()) return false;
+ const list = enabledLlms();
+ const cur = currentLlmId();
+ const items = list.length ? list.map((eng) => ({
+ label: eng.name,
+ type: "radio",
+ checked: eng.id === cur,
+ click: () => { settings.defaultLlmId = eng.id; saveSettings(); emitQuickLinksState(); openQuickPanel("llm"); },
+ })) : [{ label: "No AI engine enabled", enabled: false }];
+ const extra = [
+ { type: "separator" },
+ { label: "Pick AI engines (Search settings)…", click: () => openSettingsTab("search") },
+ ];
+ const menu = Menu.buildFromTemplate([...items, ...extra]);
+ const r = rect || {};
+ const y = Math.round(Number(r.y) || 0) + (primaryW.win === win ? CHROME_H : primaryW.chromeH || CHROME_H);
+ menu.popup({ window: primaryW.win, x: Math.round((Number(r.x) || 0) + (Number(r.width) || 0)), y });
+ return true;
+});
+
+// Settings › Sidebar's "Reset panel widths" button (0.3.100 — moved out of
+// the strip's gear popover). Blanks every saved width and re-layouts; the
+// next time an app opens it uses its default width again.
+ipcMain.handle("sidebar-reset-widths", (e) => {
+ if (!isSettingsSender(e)) throw new Error("sidebar-reset-widths: settings only");
+ settings.leftPanelWidths = {}; saveSettings(); layout();
+ return true;
+});
function leftPanelHidden(panelId) {
return Array.isArray(settings.leftPanelsHidden) && settings.leftPanelsHidden.includes("p:" + panelId);
}
@@ -9644,7 +9703,20 @@ function visibleQuickLinks() {
// at: an address on the app's site to show (a share screen, a chat), even
// when the panel already has that site open.
function openQuickPanel(id, at) {
- const L = (settings.quickLinks || []).find((x) => x.id === id);
+ // Special "llm" id: open whichever LLM engine the user picked (defaults
+ // to the first enabled LLM). Synthesized as a quick-link-shaped entry so
+ // the rest of the panel lifecycle (resize, maximize, close) just works.
+ let L;
+ if (id === "llm") {
+ const engId = currentLlmId();
+ if (!engId) return; // no LLM enabled — the button shouldn't be visible then
+ const engDef = allEngines().find((e) => e.id === engId);
+ const url = llmHomeUrl(engId);
+ if (!url) return;
+ L = { id: "llm", title: engDef?.name || "AI", url };
+ } else {
+ L = (settings.quickLinks || []).find((x) => x.id === id);
+ }
if (!quickPanel || !L || !L.url) return;
if (leftPanelId) closeLeftPanel();
if (activeQuickLinkId !== id) leftPanelMax = false;
@@ -11047,11 +11119,28 @@ function openHermesWindow() {
ipcMain.handle("hermes-open", () => { openHermesWindow(); return { ok: true }; });
+// 0.3.100: default-open target is the left-sidebar panel in the active browser
+// window. We only fall back to the standalone BrowserWindow when the Hermes
+// extension isn't registered as a left panel — e.g. the user disabled it, or
+// the add-on host hasn't finished starting up yet. Keeping the standalone
+// window alive means the "⧉ pop out" button in panel.html still works for the
+// users who prefer a free-floating Messages window.
+function openHermesInSidebarOrWindow() {
+ try {
+ const haveLeft = leftPanels().some((p) => p.panelId === "hermes:main");
+ const w = primaryW?.win && !primaryW.win.isDestroyed() ? primaryW : null;
+ if (haveLeft && w) {
+ withWin(w, () => openLeftPanel("hermes:main"));
+ return;
+ }
+ } catch {}
+ openHermesWindow();
+}
+
// In-process signal fired by the Hermes bundled add-on (bundled-addons/hermes).
// Add-ons run under the sandbox and can't call openHermesWindow directly, so
-// they emit this and we resolve it here. Separate from the IPC channel so the
-// HERMES_ONLY gate doesn't need to grow a sender exception for sandboxed add-on
-// code — ipcMain.emit never carries a webContents sender.
+// they emit this and we resolve it here. The panel.html "⧉ pop out" button is
+// the only remaining caller — it explicitly wants the standalone window.
ipcMain.on("hermes-open-request", () => { try { openHermesWindow(); } catch {} });
// Dev-only auto-init: `THESEUS_HERMES_DEV_MNEMONIC=...` + `--messages` launches
@@ -11215,8 +11304,12 @@ app.on("web-contents-created", (_event, wc) => {
// Ctrl+Shift+E -> Messages (Hermes / Ermis, Greek Ἑρμῆς). Picked E over the
// old M because Ctrl+Shift+M is Chrome's "Switch profile" (users muscle-
// memory it) and because E matches the Greek spelling.
+ // 0.3.100: open the Hermes left-sidebar panel in whichever browser window
+ // the shortcut was pressed in, instead of the standalone BrowserWindow.
+ // Falls back to the standalone window only if the Hermes extension isn't
+ // registered as a left panel (e.g. the user disabled it).
if (input.control && input.shift && (input.key === "E" || input.key === "e")) {
- openHermesWindow();
+ openHermesInSidebarOrWindow();
return e.preventDefault();
}
// App windows, link windows, Messages: the keys act on the page that got
@@ -11342,9 +11435,10 @@ function handleLaunch(argv) {
if (win && !win.isDestroyed()) { try { if (win.isMinimized()) win.restore(); win.focus(); } catch {} }
else createWindow();
if (openMsg) {
- // openHermesWindow is defined further down; defer with setImmediate so it
- // runs after createWindow's queue has settled on first launch.
- setImmediate(() => { try { openHermesWindow(); } catch {} });
+ // openHermesInSidebarOrWindow is defined further down; defer with setImmediate
+ // so it runs after createWindow's queue has settled on first launch. Falls
+ // back to the standalone window if the sidebar isn't ready yet.
+ setImmediate(() => { try { openHermesInSidebarOrWindow(); } catch {} });
}
}
if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) {
@@ -11436,7 +11530,7 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) {
if (process.argv.includes("--messages")) {
setImmediate(async () => {
try { await hermesDevAutoInit(); } catch {}
- try { openHermesWindow(); } catch {}
+ try { openHermesInSidebarOrWindow(); } catch {}
});
}
// BNS index: a separate process (bns-indexer.js). Its first phase — the
diff --git a/package.json b/package.json
index e329cea0..13bfc4bf 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
- "version": "0.3.99",
+ "version": "0.3.100",
"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-preload.js b/quicklinks-preload.js
index 555173b9..d63a5ba1 100644
--- a/quicklinks-preload.js
+++ b/quicklinks-preload.js
@@ -5,6 +5,9 @@ contextBridge.exposeInMainWorld("quicklinks", {
close: () => ipcRenderer.invoke("quicklinks-close"),
// The gear: rect is the button's box in the strip, for placing the menu.
menu: (rect) => ipcRenderer.invoke("quicklinks-menu", rect),
+ // LLM picker — right-click on the LLM button pops a native menu of
+ // enabled LLM engines. rect positions the menu next to the button.
+ llmMenu: (rect) => ipcRenderer.invoke("quicklinks-llm-menu", rect),
// The strip's new order after a drag: keys "q:" / "p:".
reorder: (order) => ipcRenderer.invoke("quicklinks-reorder", order),
add: () => ipcRenderer.invoke("quicklinks-add"),
diff --git a/quicklinks.html b/quicklinks.html
index 24c2d404..2f9107fa 100644
--- a/quicklinks.html
+++ b/quicklinks.html
@@ -42,16 +42,29 @@
.sep { width: 22px; height: 1px; background: var(--line2); margin: 4px 0; }
.icon.emoji { font-size: 18px; }
.add:hover { color: var(--acid); }
- /* Gear: how panels sit next to the page, and the Sidebar settings. */
+ /* Gear + LLM button sit at the bottom of the strip. The LLM button grows
+ a small chevron badge in the bottom-right that opens the "pick AI"
+ menu; the main area of the button opens the current default LLM in
+ the left panel. Right-click on the button is equivalent to the chevron. */
.gear { color: var(--mut); margin-bottom: 2px; }
.gear:hover { color: var(--ink); }
.gear svg { width: 17px; height: 17px; }
+ .llm { color: var(--mut); margin-bottom: 2px; position: relative; }
+ .llm:hover { color: var(--ink); }
+ .llm.on { background: var(--acid-tint); box-shadow: inset 2px 0 0 var(--acid); color: var(--ink); }
+ .llm .glyph { font-size: 16px; line-height: 1; }
+ .llm img { width: 18px; height: 18px; display: block; }
+ .llm .chev { position: absolute; right: 1px; bottom: 1px; width: 10px; height: 10px;
+ display: grid; place-items: center; color: var(--mut); background: var(--bg);
+ border: 1px solid var(--line2); border-radius: 3px; font-size: 7px; line-height: 1; padding: 0; cursor: pointer; }
+ .llm .chev:hover { color: var(--ink); }
+
Sidebar options+Add link
@@ -66,6 +79,26 @@
const r = gearBtn.getBoundingClientRect();
Q && Q.menu && Q.menu({ x: r.left, y: r.top, width: r.width, height: r.height });
};
+ // LLM button: click opens the current default LLM in the left panel.
+ // Right-click (or the small chevron badge) opens the AI picker menu.
+ const llmBtn = document.getElementById("llmBtn");
+ const llmGlyph = document.getElementById("llmGlyph");
+ const llmTip = document.getElementById("llmTip");
+ const llmChev = document.getElementById("llmChev");
+ llmBtn.addEventListener("click", (e) => {
+ if (e.target === llmChev) return; // chevron handles its own click
+ Q && Q.open && Q.open("llm");
+ });
+ llmBtn.addEventListener("contextmenu", (e) => {
+ e.preventDefault();
+ 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();
+ Q && Q.llmMenu && Q.llmMenu({ x: r.left, y: r.top, width: r.width, height: r.height });
+ });
const { BRAND, svgFor: brandForLink, hostOf } = window.StripIcons;
function letterIcon(b, L) { b.classList.add("letter"); b.prepend(document.createTextNode((L.title || "?").slice(0, 1).toUpperCase())); }
// One order for web apps and extension panels (main keeps it); drag an
@@ -149,6 +182,30 @@
}
for (const k of order) { const b = made.get(k); if (b) { icons.appendChild(b); made.delete(k); } }
for (const b of made.values()) icons.appendChild(b);
+
+ // LLM button — visible only when at least one LLM engine is enabled.
+ const llm = (data && data.llm) || null;
+ const engines = (llm && llm.engines) || [];
+ if (engines.length) {
+ llmBtn.hidden = false;
+ llmBtn.classList.toggle("on", !!(llm && llm.open));
+ const curId = (llm && llm.current) || engines[0].id;
+ const cur = engines.find((e) => e.id === curId) || engines[0];
+ llmTip.textContent = cur.name;
+ // Prefer the engine's bundled favicon; fall back to the symbol emoji.
+ llmGlyph.textContent = "";
+ llmGlyph.innerHTML = "";
+ if (cur.favicon) {
+ const img = document.createElement("img");
+ img.src = cur.favicon; img.alt = "";
+ img.onerror = () => { llmGlyph.innerHTML = ""; llmGlyph.textContent = cur.sym || "✨"; };
+ llmGlyph.appendChild(img);
+ } else {
+ llmGlyph.textContent = cur.sym || "✨";
+ }
+ } else {
+ llmBtn.hidden = true;
+ }
}
Q && Q.getState && Q.getState().then(render).catch(() => render(null));
Q && Q.onState && Q.onState(render);
diff --git a/settings-preload.js b/settings-preload.js
index c5850587..85c3c4d9 100644
--- a/settings-preload.js
+++ b/settings-preload.js
@@ -119,4 +119,6 @@ contextBridge.exposeInMainWorld("cfg", {
resolversGet: () => ipcRenderer.invoke("extra-resolvers-get"),
resolversSet: (key, value) => ipcRenderer.invoke("extra-resolvers-set", String(key || ""), value),
resolversReset: () => ipcRenderer.invoke("extra-resolvers-reset"),
+ // Settings › Sidebar › Reset panel widths (moved from the strip's gear in 0.3.100).
+ sidebarResetWidths: () => ipcRenderer.invoke("sidebar-reset-widths"),
});
diff --git a/settings.html b/settings.html
index e128fed5..3e782992 100644
--- a/settings.html
+++ b/settings.html
@@ -1087,13 +1087,25 @@
How panels open
-
Also in the gear on the strip, above +.
+
+
+
Reset panel widths
+
Each web app and extension panel remembers how wide you dragged it. This blanks every saved width — the next time an app opens it uses its default again.
+
Reset widths
+
+
+
+
+
AI engine in the sidebar
+
The sparkle button at the bottom of the strip opens your default AI engine (right-click it to pick from any you've enabled, or set up more under Search).
+
+
Web apps
Sites that open in the panel. Switch one off to take it off the strip; it stays here, still signed in, for switching back on. A site you installed as an app can be added from Apps.
@@ -2215,13 +2227,28 @@
// The active radio reflects the current setting; changing it just calls
// C.set — the F12 handler in main.js reads settings.devToolsDock each
// time DevTools opens, so no restart is needed for the choice to apply.
- // Sidebar › How panels open. The gear on the strip changes it too.
+ // Sidebar › How panels open. Also reflected in the strip gear's menu
+ // (now a one-item shortcut back to this page, since the radios + Reset
+ // moved here in 0.3.100).
{
const sync = (mode) => document.querySelectorAll('input[name="sidebarPanelMode"]').forEach((r) => { r.checked = (r.value === (mode === "push" ? "push" : "overlay")); });
sync(s.sidebarPanelMode);
document.querySelectorAll('input[name="sidebarPanelMode"]').forEach((r) => r.addEventListener("change", () => { if (r.checked) C.set("sidebarPanelMode", r.value); }));
if (C.onSettingsUpdate) C.onSettingsUpdate((next) => sync(next.sidebarPanelMode));
}
+ // Sidebar › Reset panel widths — blanks every saved width.
+ {
+ const btn = document.getElementById("sidebarResetWidths");
+ if (btn && C.sidebarResetWidths) {
+ btn.addEventListener("click", async () => {
+ if (!confirm("Reset every saved panel width? Each app will open at its default width next time.")) return;
+ const orig = btn.textContent;
+ btn.disabled = true; btn.textContent = "Resetting…";
+ try { await C.sidebarResetWidths(); } catch (e) { console.warn("reset widths failed", e); }
+ finally { btn.disabled = false; btn.textContent = orig; }
+ });
+ }
+ }
const devToolsDock = s.devToolsDock || "bottom";
document.querySelectorAll('input[name="devToolsDock"]').forEach((r) => {
r.checked = (r.value === devToolsDock);