Theseus 0.3.100: LLM sidebar button, Hermes consolidation, Settings polish

The gear popover at the bottom of the left strip is reduced to a single
"Open sidebar settings" item; the overlay/push radio and "Reset panel
widths" controls move to Settings › Sidebar permanently, so there's one
source of truth for the sidebar's own options.

A new sparkle button is added below the icon stack: it opens the user's
default AI engine (any search engine with kind=llm — Perplexity ships
on by default now; Phind is in the catalog). Click opens the engine in
the left panel like the existing web-app quick-links; right-click (or
the small chevron in the button's corner) pops a native menu of every
enabled LLM, each one-click to switch default + open. The button hides
itself when no LLM engine is enabled.

Hermes consolidation. The hermes.x web quick-link added in 0.3.99 is
retracted — the bundled Hermes extension already registers a left-strip
panel (side: "left"), so the web link was duplicating it. Any profile
that caught 0.3.99 gets the stray entry yanked on first load.
Ctrl+Shift+E now opens the Hermes panel in the active window's left
sidebar instead of the standalone Messages BrowserWindow; the window
is still reachable via the ⧉ pop-out button in the panel header for
users who want a free-floating client. The home-page Silent Mode card
for Hermes.X comes back (it's a link to the web page, not a shortcut
to the panel — different purpose).
This commit is contained in:
Local Dev 2026-10-21 20:10:11 +03:00
parent 208a192fc9
commit 22e2b3f75e
7 changed files with 226 additions and 42 deletions

View file

@ -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." },

170
main.js
View file

@ -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: "<reason>"`) 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

View file

@ -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",

View file

@ -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:<link id>" / "p:<panelId>".
reorder: (order) => ipcRenderer.invoke("quicklinks-reorder", order),
add: () => ipcRenderer.invoke("quicklinks-add"),

View file

@ -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); }
</style></head>
<body>
<div class="strip">
<div class="spacer"></div>
<div class="icons" id="icons"></div>
<div class="spacer"></div>
<button class="icon llm" id="llmBtn" hidden><span class="glyph" id="llmGlyph">✨</span><span class="tt" id="llmTip">AI</span><button class="chev" id="llmChev" title="Pick AI engine">▾</button></button>
<button class="icon gear" id="gearBtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><span class="tt">Sidebar options</span></button>
<button class="icon add" id="addBtn" title="Add link">+<span class="tt">Add link</span></button>
</div>
@ -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);

View file

@ -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"),
});

View file

@ -1087,13 +1087,25 @@
<label class="sw"><input type="checkbox" id="quickLinksShow"><span class="track"><span class="knob"></span></span></label>
</div>
<h2 class="sub">How panels open</h2>
<p class="subd">Also in the gear on the strip, above +.</p>
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
<div style="display:flex;flex-direction:column;gap:6px">
<label class="polrow"><input type="radio" name="sidebarPanelMode" value="overlay"><span><b>Over the page</b> <span class="pmuted">— the panel floats over the left of the page, which keeps its width. A click in the page closes it.</span></span></label>
<label class="polrow"><input type="radio" name="sidebarPanelMode" value="push"><span><b>Push the page aside</b> <span class="pmuted">— the page narrows to make room, and the panel stays open until you close it.</span></span></label>
</div>
</div>
<div class="row">
<div class="txt">
<div class="t">Reset panel widths</div>
<div class="d">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.</div>
<div class="acts"><button class="btn small ghost" id="sidebarResetWidths">Reset widths</button></div>
</div>
</div>
<div class="row">
<div class="txt">
<div class="t">AI engine in the sidebar</div>
<div class="d">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 <a data-go="search" style="color:var(--acid-text);cursor:pointer">Search</a>).</div>
</div>
</div>
<h2 class="sub">Web apps</h2>
<p class="subd">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.</p>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
@ -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);