From 22e2b3f75e3fe9144cf04bbe65e0645e8a3829e5 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 21 Oct 2026 20:10:11 +0300 Subject: [PATCH] Theseus 0.3.100: LLM sidebar button, Hermes consolidation, Settings polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- home.html | 1 + main.js | 170 ++++++++++++++++++++++++++++++++---------- package.json | 2 +- quicklinks-preload.js | 3 + quicklinks.html | 59 ++++++++++++++- settings-preload.js | 2 + settings.html | 31 +++++++- 7 files changed, 226 insertions(+), 42 deletions(-) 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); }
+
@@ -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.
+
+
+
+
+
+
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);