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 = '