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: "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: "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: "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: "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: "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." },
|
{ 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
|
// 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.
|
// 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
|
// 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
|
// 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.
|
// 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); }
|
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
|
// 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
|
// (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).
|
// 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)
|
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)
|
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)
|
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 }]
|
customEngines: [], // user-added: [{ id, name, url-with-%s }]
|
||||||
theme: "dark", // dark | light | system — drives prefers-color-scheme in all views
|
theme: "dark", // dark | light | system — drives prefers-color-scheme in all views
|
||||||
// BCNR/ICANN collision policy (see SilentMode/Argus/DESIGN-collision-modes.md):
|
// BCNR/ICANN collision policy (see SilentMode/Argus/DESIGN-collision-modes.md):
|
||||||
|
|
@ -586,7 +611,6 @@ const SETTINGS_DEFAULTS = {
|
||||||
quickLinksShow: true,
|
quickLinksShow: true,
|
||||||
quickLinks: [
|
quickLinks: [
|
||||||
{ id: "proton", url: "https://mail.proton.me/", title: "Proton Mail" },
|
{ 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: "telegram", url: "https://web.telegram.org/k/", title: "Telegram" },
|
||||||
{ id: "whatsapp", url: "https://web.whatsapp.com/", title: "WhatsApp" },
|
{ id: "whatsapp", url: "https://web.whatsapp.com/", title: "WhatsApp" },
|
||||||
{ id: "x", url: "https://x.com/", title: "X" },
|
{ id: "x", url: "https://x.com/", title: "X" },
|
||||||
|
|
@ -675,7 +699,7 @@ function loadSettings() {
|
||||||
// itself). quickLinksOffered remembers it, so one the user removes stays
|
// itself). quickLinksOffered remembers it, so one the user removes stays
|
||||||
// removed. Kept out of SETTINGS_DEFAULTS so an old profile reads as "never offered".
|
// removed. Kept out of SETTINGS_DEFAULTS so an old profile reads as "never offered".
|
||||||
try {
|
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 offered = Array.isArray(settings.quickLinksOffered) ? settings.quickLinksOffered.slice() : [];
|
||||||
const have = Array.isArray(settings.quickLinks) ? settings.quickLinks : null;
|
const have = Array.isArray(settings.quickLinks) ? settings.quickLinks : null;
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
@ -683,19 +707,22 @@ function loadSettings() {
|
||||||
if (offered.includes(id)) continue;
|
if (offered.includes(id)) continue;
|
||||||
offered.push(id); changed = true;
|
offered.push(id); changed = true;
|
||||||
const def = SETTINGS_DEFAULTS.quickLinks.find((L) => L.id === id);
|
const def = SETTINGS_DEFAULTS.quickLinks.find((L) => L.id === id);
|
||||||
// Proton Mail goes to the top of the web apps; Hermes slots in right
|
// Proton Mail goes to the top of the web apps; any later one at the end.
|
||||||
// after Proton (keeping Silent-Mode-owned messaging near the top); any
|
|
||||||
// other later one at the end.
|
|
||||||
if (have && def && !have.some((L) => L && L.id === id)) {
|
if (have && def && !have.some((L) => L && L.id === id)) {
|
||||||
if (id === "proton") have.unshift({ ...def });
|
if (id === "proton") have.unshift({ ...def }); else have.push({ ...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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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(); }
|
if (changed) { settings.quickLinksOffered = offered; saveSettings(); }
|
||||||
} catch (e) { console.warn("quickLinks offer:", e?.message); }
|
} catch (e) { console.warn("quickLinks offer:", e?.message); }
|
||||||
// Translator moved from a single `translateEndpoint` to a peer list in
|
// Translator moved from a single `translateEndpoint` to a peer list in
|
||||||
|
|
@ -9464,11 +9491,21 @@ ipcMain.handle("quicklinks-reorder", (e, order) => {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
function quickLinksPayload() {
|
function quickLinksPayload() {
|
||||||
|
const llms = enabledLlms();
|
||||||
|
const curLlm = currentLlmId();
|
||||||
return {
|
return {
|
||||||
order: stripOrder(),
|
order: stripOrder(),
|
||||||
links: visibleQuickLinks(),
|
links: visibleQuickLinks(),
|
||||||
openId: leftPanelId ? "p:" + leftPanelId : activeQuickLinkId,
|
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 || "" })),
|
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() {
|
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-state", (e) => isHeaderSender(e) ? panelHeaderState() : null);
|
||||||
ipcMain.handle("panel-header-max", (e) => isHeaderSender(e) && leftPanelKey() ? setLeftPanelMax(!leftPanelMax) : false);
|
ipcMain.handle("panel-header-max", (e) => isHeaderSender(e) && leftPanelKey() ? setLeftPanelMax(!leftPanelMax) : false);
|
||||||
ipcMain.handle("panel-header-close", (e) => { if (isHeaderSender(e)) hideLeftPanels(); return true; });
|
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
|
// The gear above "+" on the strip. Previously carried panel-mode radios + a
|
||||||
// Sidebar settings. A native menu, so it can open past the strip's edge.
|
// 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) => {
|
ipcMain.handle("quicklinks-menu", (e, rect) => {
|
||||||
if (!quicklinks || e.sender !== quicklinks.webContents || !primaryW?.win || primaryW.win.isDestroyed()) return false;
|
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([
|
const menu = Menu.buildFromTemplate([
|
||||||
{ label: "Panels open over the page", type: "radio", checked: !push, click: () => setMode("overlay") },
|
{ label: "Open sidebar settings", click: () => openSettingsTab("sidebar") },
|
||||||
{ 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") },
|
|
||||||
]);
|
]);
|
||||||
const r = rect || {};
|
const r = rect || {};
|
||||||
const y = Math.round(Number(r.y) || 0) + (primaryW.win === win ? CHROME_H : primaryW.chromeH || CHROME_H);
|
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 });
|
menu.popup({ window: primaryW.win, x: Math.round((Number(r.x) || 0) + (Number(r.width) || 0)), y });
|
||||||
return true;
|
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) {
|
function leftPanelHidden(panelId) {
|
||||||
return Array.isArray(settings.leftPanelsHidden) && settings.leftPanelsHidden.includes("p:" + 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
|
// at: an address on the app's site to show (a share screen, a chat), even
|
||||||
// when the panel already has that site open.
|
// when the panel already has that site open.
|
||||||
function openQuickPanel(id, at) {
|
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 (!quickPanel || !L || !L.url) return;
|
||||||
if (leftPanelId) closeLeftPanel();
|
if (leftPanelId) closeLeftPanel();
|
||||||
if (activeQuickLinkId !== id) leftPanelMax = false;
|
if (activeQuickLinkId !== id) leftPanelMax = false;
|
||||||
|
|
@ -11047,11 +11119,28 @@ function openHermesWindow() {
|
||||||
|
|
||||||
ipcMain.handle("hermes-open", () => { openHermesWindow(); return { ok: true }; });
|
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).
|
// 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
|
// 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
|
// they emit this and we resolve it here. The panel.html "⧉ pop out" button is
|
||||||
// HERMES_ONLY gate doesn't need to grow a sender exception for sandboxed add-on
|
// the only remaining caller — it explicitly wants the standalone window.
|
||||||
// code — ipcMain.emit never carries a webContents sender.
|
|
||||||
ipcMain.on("hermes-open-request", () => { try { openHermesWindow(); } catch {} });
|
ipcMain.on("hermes-open-request", () => { try { openHermesWindow(); } catch {} });
|
||||||
|
|
||||||
// Dev-only auto-init: `THESEUS_HERMES_DEV_MNEMONIC=...` + `--messages` launches
|
// 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
|
// Ctrl+Shift+E -> Messages (Hermes / Ermis, Greek Ἑρμῆς). Picked E over the
|
||||||
// old M because Ctrl+Shift+M is Chrome's "Switch profile" (users muscle-
|
// old M because Ctrl+Shift+M is Chrome's "Switch profile" (users muscle-
|
||||||
// memory it) and because E matches the Greek spelling.
|
// 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")) {
|
if (input.control && input.shift && (input.key === "E" || input.key === "e")) {
|
||||||
openHermesWindow();
|
openHermesInSidebarOrWindow();
|
||||||
return e.preventDefault();
|
return e.preventDefault();
|
||||||
}
|
}
|
||||||
// App windows, link windows, Messages: the keys act on the page that got
|
// 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 {} }
|
if (win && !win.isDestroyed()) { try { if (win.isMinimized()) win.restore(); win.focus(); } catch {} }
|
||||||
else createWindow();
|
else createWindow();
|
||||||
if (openMsg) {
|
if (openMsg) {
|
||||||
// openHermesWindow is defined further down; defer with setImmediate so it
|
// openHermesInSidebarOrWindow is defined further down; defer with setImmediate
|
||||||
// runs after createWindow's queue has settled on first launch.
|
// so it runs after createWindow's queue has settled on first launch. Falls
|
||||||
setImmediate(() => { try { openHermesWindow(); } catch {} });
|
// back to the standalone window if the sidebar isn't ready yet.
|
||||||
|
setImmediate(() => { try { openHermesInSidebarOrWindow(); } catch {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) {
|
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")) {
|
if (process.argv.includes("--messages")) {
|
||||||
setImmediate(async () => {
|
setImmediate(async () => {
|
||||||
try { await hermesDevAutoInit(); } catch {}
|
try { await hermesDevAutoInit(); } catch {}
|
||||||
try { openHermesWindow(); } catch {}
|
try { openHermesInSidebarOrWindow(); } catch {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// BNS index: a separate process (bns-indexer.js). Its first phase — the
|
// BNS index: a separate process (bns-indexer.js). Its first phase — the
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.3.99",
|
"version": "0.3.100",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ contextBridge.exposeInMainWorld("quicklinks", {
|
||||||
close: () => ipcRenderer.invoke("quicklinks-close"),
|
close: () => ipcRenderer.invoke("quicklinks-close"),
|
||||||
// The gear: rect is the button's box in the strip, for placing the menu.
|
// The gear: rect is the button's box in the strip, for placing the menu.
|
||||||
menu: (rect) => ipcRenderer.invoke("quicklinks-menu", rect),
|
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>".
|
// The strip's new order after a drag: keys "q:<link id>" / "p:<panelId>".
|
||||||
reorder: (order) => ipcRenderer.invoke("quicklinks-reorder", order),
|
reorder: (order) => ipcRenderer.invoke("quicklinks-reorder", order),
|
||||||
add: () => ipcRenderer.invoke("quicklinks-add"),
|
add: () => ipcRenderer.invoke("quicklinks-add"),
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,29 @@
|
||||||
.sep { width: 22px; height: 1px; background: var(--line2); margin: 4px 0; }
|
.sep { width: 22px; height: 1px; background: var(--line2); margin: 4px 0; }
|
||||||
.icon.emoji { font-size: 18px; }
|
.icon.emoji { font-size: 18px; }
|
||||||
.add:hover { color: var(--acid); }
|
.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 { color: var(--mut); margin-bottom: 2px; }
|
||||||
.gear:hover { color: var(--ink); }
|
.gear:hover { color: var(--ink); }
|
||||||
.gear svg { width: 17px; height: 17px; }
|
.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>
|
</style></head>
|
||||||
<body>
|
<body>
|
||||||
<div class="strip">
|
<div class="strip">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="icons" id="icons"></div>
|
<div class="icons" id="icons"></div>
|
||||||
<div class="spacer"></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 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>
|
<button class="icon add" id="addBtn" title="Add link">+<span class="tt">Add link</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,6 +79,26 @@
|
||||||
const r = gearBtn.getBoundingClientRect();
|
const r = gearBtn.getBoundingClientRect();
|
||||||
Q && Q.menu && Q.menu({ x: r.left, y: r.top, width: r.width, height: r.height });
|
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;
|
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())); }
|
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
|
// 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 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);
|
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.getState && Q.getState().then(render).catch(() => render(null));
|
||||||
Q && Q.onState && Q.onState(render);
|
Q && Q.onState && Q.onState(render);
|
||||||
|
|
|
||||||
|
|
@ -119,4 +119,6 @@ contextBridge.exposeInMainWorld("cfg", {
|
||||||
resolversGet: () => ipcRenderer.invoke("extra-resolvers-get"),
|
resolversGet: () => ipcRenderer.invoke("extra-resolvers-get"),
|
||||||
resolversSet: (key, value) => ipcRenderer.invoke("extra-resolvers-set", String(key || ""), value),
|
resolversSet: (key, value) => ipcRenderer.invoke("extra-resolvers-set", String(key || ""), value),
|
||||||
resolversReset: () => ipcRenderer.invoke("extra-resolvers-reset"),
|
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>
|
<label class="sw"><input type="checkbox" id="quickLinksShow"><span class="track"><span class="knob"></span></span></label>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="sub">How panels open</h2>
|
<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 class="row" style="flex-direction:column;align-items:stretch;gap:10px">
|
||||||
<div style="display:flex;flex-direction:column;gap:6px">
|
<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="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>
|
<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>
|
</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>
|
<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>
|
<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">
|
<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
|
// The active radio reflects the current setting; changing it just calls
|
||||||
// C.set — the F12 handler in main.js reads settings.devToolsDock each
|
// 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.
|
// 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")); });
|
const sync = (mode) => document.querySelectorAll('input[name="sidebarPanelMode"]').forEach((r) => { r.checked = (r.value === (mode === "push" ? "push" : "overlay")); });
|
||||||
sync(s.sidebarPanelMode);
|
sync(s.sidebarPanelMode);
|
||||||
document.querySelectorAll('input[name="sidebarPanelMode"]').forEach((r) => r.addEventListener("change", () => { if (r.checked) C.set("sidebarPanelMode", r.value); }));
|
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));
|
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";
|
const devToolsDock = s.devToolsDock || "bottom";
|
||||||
document.querySelectorAll('input[name="devToolsDock"]').forEach((r) => {
|
document.querySelectorAll('input[name="devToolsDock"]').forEach((r) => {
|
||||||
r.checked = (r.value === devToolsDock);
|
r.checked = (r.value === devToolsDock);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue