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:
parent
208a192fc9
commit
22e2b3f75e
7 changed files with 226 additions and 42 deletions
|
|
@ -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
170
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: "<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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue