diff --git a/main.js b/main.js index 0dac5e0..dd23831 100644 --- a/main.js +++ b/main.js @@ -91,7 +91,13 @@ const SEARCH_ENGINES = { const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"]; // DuckDuckGo's icon service reliably returns a favicon for ANY domain from one // privacy-respecting host — far more robust than guessing /favicon.ico per site. -const faviconUrl = (domain) => (domain ? `https://icons.duckduckgo.com/ip3/${domain}.ico` : null); +// Search-engine favicon source. DuckDuckGo's icons.duckduckgo.com/ip3/… +// service was returning inconsistent results (Brave, Bing, Yandex etc. +// came back 404 → the settings row fell through to an emoji). Google's +// /s2/favicons service is materially more reliable, returns a real 32×32 +// PNG for essentially every host, and doesn't require login. Kept as a +// single point so the fallback source can be swapped again in one place. +const faviconUrl = (domain) => (domain ? `https://www.google.com/s2/favicons?domain=${domain}&sz=32` : null); function customFavicon(url) { try { return faviconUrl(new URL(String(url).replace("%s", "x")).hostname); } catch { return null; } } function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).includes(id); } // Two-tier state: an engine is INSTALLED if it's in the user's Additional @@ -2630,7 +2636,26 @@ ipcMain.handle("addon-tab-close", (e) => { return true; }); // Read-side of Settings' Add-ons tab. -ipcMain.handle("addons-list", () => addonHost ? addonHost.snapshot() : { installed: [], sidebarPanels: [] }); +ipcMain.handle("addons-list", () => { + if (!addonHost) return { installed: [], sidebarPanels: [] }; + const snap = addonHost.snapshot(); + // Mark bundled add-ons so the extensions UI can render them differently + // (Aegis + friends look built-in rather than removable extensions). A + // sibling folder in bundled-addons/ with the same manifest id is proof + // the addon ships with Theseus; seedBundledAddons keeps them in sync. + let bundledIds = new Set(); + try { + for (const e of fs.readdirSync(bundledAddonsDir(), { withFileTypes: true })) { + if (!e.isDirectory()) continue; + try { + const m = JSON.parse(fs.readFileSync(path.join(bundledAddonsDir(), e.name, "addon.json"), "utf8")); + if (m && m.id) bundledIds.add(String(m.id)); + } catch {} + } + } catch {} + snap.installed = snap.installed.map((a) => ({ ...a, bundled: a.id ? bundledIds.has(a.id) : false })); + return snap; +}); // Toggle an add-on's enabled state. Discovery re-runs so newly-enabled // add-ons activate immediately and newly-disabled ones drop out — no // restart required. diff --git a/settings.html b/settings.html index 2da89be..4d237db 100644 --- a/settings.html +++ b/settings.html @@ -593,9 +593,24 @@ // search engines: default picker + custom-engine list + add form const sel = document.getElementById("searchEngine"); const esc = (s) => String(s || "").replace(/ e.favicon - ? `` - : `${e.sym || "🔍"}`; + // Try the primary favicon URL (Google's /s2/favicons — set in main.js); + // fall back to DuckDuckGo's icons.duckduckgo.com service if that 404s, + // then to the emoji sym as final fallback so the row is never blank. + const engIcon = (e) => { + let alt = ""; + try { + const u = new URL(e.favicon || "https://x"); + const domain = u.searchParams.get("domain") || u.hostname; + if (domain && domain !== "x") alt = `https://icons.duckduckgo.com/ip3/${domain}.ico`; + } catch {} + const sym = (e.sym || "🔍").replace(/'/g, "'"); + const fallbackChain = alt + ? `this.onerror=null;this.src='${alt}';this.setAttribute('data-tried','1');this.onerror=function(){this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))};` + : `this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))`; + return e.favicon + ? `` + : `${sym}`; + }; const ENGINE_KINDS = [ { key: "search", label: "Search engines" }, { key: "llm", label: "AI answer engines" }, @@ -1121,13 +1136,32 @@ addonsList.innerHTML = '
No extensions installed. Drop a folder into the extensions directory to install one.
'; return; } - addonsList.innerHTML = items.map((a) => { + // Split into built-in (ship with Theseus, reseeded on version bump) vs + // sideloaded (dropped in by the user). Built-in ones can still be + // disabled — the toggle just means "load me at startup" — but they + // can't be permanently deleted since reseedBundledAddons runs on the + // next launch. Rendering the two groups apart makes that clear. + const bundled = items.filter((a) => a.bundled); + const sideloaded = items.filter((a) => !a.bundled); + const rowFor = (a, isBundled) => { if (a.error) { return '
⚠ Load failed ' + escapeHtml(a.folder) + '
' + escapeHtml(a.error) + '
'; } const caps = (a.capabilities || []).length ? '' + a.capabilities.map(escapeHtml).join(", ") + '' : ""; - return '
' + a.icon + ' ' + escapeHtml(a.name) + ' v' + escapeHtml(a.version) + '' + caps + '
' + escapeHtml(a.description || "") + (a.author ? ' — ' + escapeHtml(a.author) + '' : '') + '
'; - }).join(""); + const builtInBadge = isBundled + ? 'BUILT-IN' + : ""; + return '
' + a.icon + ' ' + escapeHtml(a.name) + builtInBadge + ' v' + escapeHtml(a.version) + '' + caps + '
' + escapeHtml(a.description || "") + (a.author ? ' — ' + escapeHtml(a.author) + '' : '') + '
'; + }; + const sectionHeader = (label, hint) => + '
' + escapeHtml(label) + '
' + + (hint ? '
' + escapeHtml(hint) + '
' : ''); + let html = ""; + if (bundled.length) html += sectionHeader("Built into Theseus", "Ship with every Theseus install. Disabling one hides its UI but the folder stays; a fresh install of a newer Theseus reseeds it.") + + bundled.map((a) => rowFor(a, true)).join(""); + if (sideloaded.length) html += sectionHeader("Sideloaded", sideloaded.length && bundled.length ? "Third-party or hand-installed. Remove by deleting the folder." : "") + + sideloaded.map((a) => rowFor(a, false)).join(""); + addonsList.innerHTML = html; addonsList.querySelectorAll('input[data-toggle]').forEach((cb) => { cb.addEventListener("change", async () => { await C.setAddonEnabled(cb.dataset.toggle, cb.checked);