diff --git a/collision-preload.js b/collision-preload.js
new file mode 100644
index 0000000..e0c97a8
--- /dev/null
+++ b/collision-preload.js
@@ -0,0 +1,7 @@
+// Minimal, sandboxed IPC bridge for the "Open withβ¦" collision prompt window.
+// Only exposes the exact call the page needs: send the user's choice back to
+// the main process (which awaits it via ipcMain.once).
+const { contextBridge, ipcRenderer } = require("electron");
+contextBridge.exposeInMainWorld("collisionApi", {
+ choose(host, payload) { ipcRenderer.send(`collision-choose:${host}`, payload); },
+});
diff --git a/collision.html b/collision.html
new file mode 100644
index 0000000..e129565
--- /dev/null
+++ b/collision.html
@@ -0,0 +1,94 @@
+
+
+
+
+ Open withβ¦
+
+
+
+
+ exists in two registries
+ Choose which one to open. You can change this later in Settings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main.js b/main.js
index b13be3e..c7fe59e 100644
--- a/main.js
+++ b/main.js
@@ -4,7 +4,7 @@
// h, Sia s3, direct ip, redirect u). Tabs, nav controls, a search box, a home
// page, and optional Tor onion routing. No system daemon; the app is the trust
// boundary.
-const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme } = require("electron");
+const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme, shell } = require("electron");
const path = require("path");
const http = require("http");
const https = require("https");
@@ -21,27 +21,29 @@ const RES_DIR = app.isPackaged ? process.resourcesPath : __dirname;
const RESOLVER = app.isPackaged
? path.join(RES_DIR, "resolver-web.mjs")
: path.join(__dirname, "..", "Argus", "src", "lib", "resolver-web.js");
-// Built-in search engines. Users can also add their own (settings.customEngines,
+// Built-in engines. Users can also add their own (settings.customEngines,
// each { id, name, url } where the url contains "%s" for the query).
// Catalog of built-in engines (users pick which to enable + can add their own).
// fav = the domain to load a real favicon from; sym = emoji fallback.
+// kind = "search" (traditional search engines) or "llm" (AI answer engines).
+// The picker + settings render each kind in its own section; kind stays a
+// display-only grouping (URL routing is the same for both).
const SEARCH_ENGINES = {
- duckduckgo: { name: "DuckDuckGo", sym: "π¦", fav: "duckduckgo.com", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
- google: { name: "Google", sym: "π΅", fav: "www.google.com", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
- brave: { name: "Brave", sym: "π¦", fav: "search.brave.com", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
- bing: { name: "Bing", sym: "π", fav: "www.bing.com", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
- startpage: { name: "Startpage", sym: "π‘οΈ", fav: "www.startpage.com", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
- yandex: { name: "Yandex", sym: "π΄", fav: "yandex.com", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
- ecosia: { name: "Ecosia", sym: "π±", fav: "www.ecosia.org", url: (q) => "https://www.ecosia.org/search?q=" + encodeURIComponent(q) },
- mojeek: { name: "Mojeek", sym: "π§", fav: "www.mojeek.com", url: (q) => "https://www.mojeek.com/search?q=" + encodeURIComponent(q) },
- searxng: { name: "SearXNG", sym: "π§©", fav: "searx.be", url: (q) => "https://searx.be/search?q=" + encodeURIComponent(q) },
- wikipedia: { name: "Wikipedia", sym: "π", fav: "en.wikipedia.org", url: (q) => "https://en.wikipedia.org/wiki/Special:Search?search=" + encodeURIComponent(q) },
- perplexity: { name: "Perplexity", sym: "π§ ", fav: "www.perplexity.ai", url: (q) => "https://www.perplexity.ai/search?q=" + encodeURIComponent(q) },
- // AI / LLM answer engines (accept a URL query and answer it directly).
- chatgpt: { name: "ChatGPT", sym: "π€", fav: "chatgpt.com", url: (q) => "https://chatgpt.com/?q=" + encodeURIComponent(q) },
- claude: { name: "Claude", sym: "β³οΈ", fav: "claude.ai", url: (q) => "https://claude.ai/new?q=" + encodeURIComponent(q) },
- phind: { name: "Phind", sym: "π§βπ»", fav: "www.phind.com", url: (q) => "https://www.phind.com/search?q=" + encodeURIComponent(q) },
- you: { name: "You.com", sym: "π£", fav: "you.com", url: (q) => "https://you.com/search?q=" + encodeURIComponent(q) + "&tbm=youchat" },
+ duckduckgo: { kind: "search", name: "DuckDuckGo", sym: "π¦", fav: "duckduckgo.com", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
+ google: { kind: "search", name: "Google", sym: "π΅", fav: "www.google.com", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
+ brave: { kind: "search", name: "Brave", sym: "π¦", fav: "search.brave.com", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
+ bing: { kind: "search", name: "Bing", sym: "π", fav: "www.bing.com", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
+ startpage: { kind: "search", name: "Startpage", sym: "π‘οΈ", fav: "www.startpage.com", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
+ yandex: { kind: "search", name: "Yandex", sym: "π΄", fav: "yandex.com", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
+ ecosia: { kind: "search", name: "Ecosia", sym: "π±", fav: "www.ecosia.org", url: (q) => "https://www.ecosia.org/search?q=" + encodeURIComponent(q) },
+ mojeek: { kind: "search", name: "Mojeek", sym: "π§", fav: "www.mojeek.com", url: (q) => "https://www.mojeek.com/search?q=" + encodeURIComponent(q) },
+ searxng: { kind: "search", name: "SearXNG", sym: "π§©", fav: "searx.be", url: (q) => "https://searx.be/search?q=" + encodeURIComponent(q) },
+ wikipedia: { kind: "search", name: "Wikipedia", sym: "π", fav: "en.wikipedia.org", url: (q) => "https://en.wikipedia.org/wiki/Special:Search?search=" + encodeURIComponent(q) },
+ // AI / LLM answer engines that ANSWER the URL query without requiring a login.
+ // ChatGPT / Claude / You.com's youchat all bounce to sign-in before running
+ // ?q=, so they'd fail silently as a "search engine" β omitted deliberately.
+ perplexity: { kind: "llm", name: "Perplexity", sym: "π§ ", fav: "www.perplexity.ai", url: (q) => "https://www.perplexity.ai/search?q=" + encodeURIComponent(q) },
+ phind: { kind: "llm", name: "Phind", sym: "π§βπ»", fav: "www.phind.com", url: (q) => "https://www.phind.com/search?q=" + encodeURIComponent(q) },
};
// 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.
@@ -55,9 +57,9 @@ function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).inc
// enabledEngines; custom engines are always enabled.
function allEngines() {
const list = Object.entries(SEARCH_ENGINES).map(([id, e]) =>
- ({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), builtin: true, enabled: isEnabled(id) }));
+ ({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), kind: e.kind || "search", builtin: true, enabled: isEnabled(id) }));
for (const c of settings.customEngines || [])
- list.push({ id: c.id, name: c.name, sym: c.sym || "π", favicon: customFavicon(c.url), builtin: false, enabled: true });
+ list.push({ id: c.id, name: c.name, sym: c.sym || "π", favicon: customFavicon(c.url), kind: c.kind || "search", builtin: false, enabled: true });
// Apply the user's custom order; ids not in engineOrder keep their natural order (stable sort).
const order = settings.engineOrder || [];
return list.slice().sort((a, b) => {
@@ -518,7 +520,7 @@ async function ensureIndex(force = false) {
if (!force && sharedIndex && Date.now() - indexBuiltAt < INDEX_TTL) return sharedIndex;
if (indexBuilding) return indexBuilding; // dedupe concurrent builds
indexBuilding = buildIndex({ WebSocket: currentWS(), directIP: true, electrum: electrumPool })
- .then((idx) => { sharedIndex = idx; indexBuiltAt = Date.now(); return idx; })
+ .then((idx) => { sharedIndex = idx; indexBuiltAt = Date.now(); refreshBcnrTlds(idx); return idx; })
.finally(() => { indexBuilding = null; });
// If we have a stale index, don't block on the rebuild β serve stale, refresh async.
return (sharedIndex && !force) ? sharedIndex : indexBuilding;
@@ -584,6 +586,13 @@ const POP_W = 360; let popH = 210; // popH is updated to fit the popover's conte
// engine favicons, like Firefox β a native