Theseus 0.3.101: Grok + Gemini + ChatGPT, sidebar polish

Three more LLM engines land in the catalog and in DEFAULT_ENABLED, so the
sparkle button's picker lists five AI engines out of the box: Perplexity,
Phind, ChatGPT, Grok, Gemini. The first two answer a URL query without any
login, the latter three need a free-tier account; all three accept the
chat URL as the "open the home" target, which is what the sidebar button
needs. A one-time offer migration adds them to any existing profile's
enabledEngines (users who disable one keep it off across restarts via a
new `enginesOffered` field).

Sidebar strip polish. The gear button now opens Settings › Sidebar on a
single click instead of popping a one-item native menu first. The "+"
add button under the gear is removed — adding a web app goes through
Settings › Sidebar anyway, which the gear opens. The quicklinks-add IPC
is kept but repointed to Settings › Sidebar (it used to open General,
which was the wrong page for the add-a-web-app flow).
This commit is contained in:
Local Dev 2026-10-21 21:55:10 +03:00
parent 8c4572e329
commit 13d4ab76b3
3 changed files with 82 additions and 17 deletions

84
main.js
View file

@ -153,11 +153,19 @@ const SEARCH_ENGINES = {
// default becomes stale as instances rate-limit / die (searx.be is anti-bot-locked).
// Users who want SearXNG add their preferred instance via the custom URL form.
wikipedia: { kind: "search", tier: "catalog", 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.
// AI / LLM answer engines. The first group answers a URL query without any
// login, so they also work as a "search engine" in the toolbar. The second
// group needs an account (free tier) and bounces ?q= to the sign-in gate —
// they're useful as sidebar LLM targets (opens the chat home) but not as
// the toolbar default search; set one of the first group there instead.
perplexity: { kind: "llm", tier: "catalog", name: "Perplexity", sym: "🧠", fav: "www.perplexity.ai", url: (q) => "https://www.perplexity.ai/search?q=" + encodeURIComponent(q) },
phind: { kind: "llm", tier: "catalog", name: "Phind", sym: "🧑‍💻", fav: "www.phind.com", url: (q) => "https://www.phind.com/search?q=" + encodeURIComponent(q) },
// Account-gated LLMs — free tier available on all three. ChatGPT and Grok
// accept ?q=<query> on their home URL and pre-fill the composer; Gemini's
// webapp ignores query strings so it just lands on the chat screen.
chatgpt: { kind: "llm", tier: "catalog", name: "ChatGPT", sym: "💬", fav: "chatgpt.com", url: (q) => "https://chatgpt.com/?q=" + encodeURIComponent(q) },
grok: { kind: "llm", tier: "catalog", name: "Grok", sym: "🪐", fav: "grok.com", url: (q) => "https://grok.com/?q=" + encodeURIComponent(q) },
gemini: { kind: "llm", tier: "catalog", name: "Gemini", sym: "✦", fav: "gemini.google.com", url: (q) => "https://gemini.google.com/app?q=" + encodeURIComponent(q) },
// ---- Extras: wider bank, discoverable via the Search filter box in Settings.
// These are known-working engines that don't require sign-in on ?q= but aren't
// first-class enough to sit in the primary catalog. Keep the list vetted — if
@ -174,7 +182,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", "perplexity"];
const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing", "perplexity", "chatgpt", "grok", "gemini"];
// 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.
@ -383,8 +391,14 @@ function fetchDnsRecords(name) {
const seq = Number(j?.seq) || 0;
// Rollback guard: a manifest with a lower seq than one already seen
// for this name is stale (or replayed) — keep what we had.
if (j && j.dns && typeof j.dns === "object" && seq >= (c?.seq ?? -1)) {
value = { dns: j.dns, seq, updatedAt: j.updated_at || null, owner: j.verified_owner || null };
if (j && typeof j === "object" && seq >= (c?.seq ?? -1)) {
// The gateway returns both the DNS block (A/AAAA/MX/TXT…) and the
// apex content records (h/u/s3/ip/tls/p) from the same signed and
// verified manifest. Both are optional. See
// Decentralized.DNS/INTEGRATION-signed-records-clients.md.
const dns = (j.dns && typeof j.dns === "object") ? j.dns : {};
const apex = (j.apex && typeof j.apex === "object") ? j.apex : null;
value = { dns, apex, seq, updatedAt: j.updated_at || null, owner: j.verified_owner || null };
}
} else if (r.status === 404 || r.status === 409) {
value = null; // no manifest declared / nowhere to keep one
@ -396,6 +410,25 @@ function fetchDnsRecords(name) {
dnsRecordsCache.set(name, { value: prev, at: c?.at ?? 0, seq: c?.seq ?? -1, pending });
return pending;
}
// The owner-signed apex content records (h/u/s3/ip/tls/p) for a name, from
// the same /api/dns response. These OVERRIDE on-chain records per field at
// serve time — chain stays as a fallback for any field the manifest doesn't
// declare. Returns null when the name has no manifest or no apex block, so
// callers can short-circuit the merge. See serveBns for the merge call site;
// keep the rule aligned with Argus/src/gateway/public-gateway.mjs serve().
async function apexFor(host) {
// Normalise to the registered name — a subdomain query ("www.hermes.x")
// shares its apex ("hermes.x") with every other label under it.
let name = host;
try {
const rec = entries.get(host);
if (rec?.entry?.name) name = rec.entry.name;
} catch { /* resolver not ready yet */ }
const cached = dnsRecordsCache.get(name);
if (cached && Date.now() - cached.at < DNS_RECORDS_TTL) return cached.value?.apex ?? null;
const v = await fetchDnsRecords(name).catch(() => null);
return v?.apex ?? null;
}
// Kick off the fetch for a resolved entry and attach the answer when it lands.
// `entry.dns` is undefined while unknown, null when the owner published no
// manifest, or { dns, seq, updatedAt, owner }.
@ -683,6 +716,23 @@ function loadSettings() {
settings.searchEngine = enabled.find((x) => !isFrozen(x)) || DEFAULT_ENABLED[0];
}
} catch (e) { console.warn("engine normalize:", e?.message); }
// One-time LLM-engine offer (0.3.101): three new account-gated LLMs were
// added to DEFAULT_ENABLED, but an existing profile's enabledEngines is
// already set so the fresh default doesn't take effect. Add each one the
// first time this upgrade runs; `enginesOffered` remembers it so a user
// who disables one keeps it disabled across restarts.
try {
const OFFER = ["chatgpt", "grok", "gemini"];
const offered = Array.isArray(settings.enginesOffered) ? settings.enginesOffered.slice() : [];
let changed = false;
for (const id of OFFER) {
if (offered.includes(id)) continue;
offered.push(id); changed = true;
if (!settings.enabledEngines.includes(id)) settings.enabledEngines.push(id);
if (!settings.installedEngines.includes(id)) settings.installedEngines.push(id);
}
if (changed) settings.enginesOffered = offered;
} catch (e) { console.warn("engines offer:", e?.message); }
// Quick-links default set changed in 0.3.70: drop Messenger + Spotify and
// reorder to Telegram, WhatsApp, X, YouTube. Users who kept the previous
// untouched default (same 6 ids, same order) move to the new set; any
@ -2524,7 +2574,20 @@ async function serveBns(request) {
const block = blockDecision(rec.entry, host);
if (block) return new Response(blockedInlineHtml(block.flag, host), { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } });
}
const r = rec.entry.records;
// Merge the owner-signed manifest's `apex` block over the chain records.
// The gateway's /api/dns/<name> verifies the manifest signature against the
// on-chain NFT holder and returns the apex block; we trust that for now,
// same way the DNS block below trusts it. Omitted apex fields fall back
// to chain, so a legacy name with no manifest is a no-op merge.
//
// Keep this merge rule in step with
// Argus/src/gateway/public-gateway.mjs serve()
// AriadneResolver/.../daemon/resolver.js
// AriadneResolver/mobile/.../BchFetcher.java
// All four surfaces must agree on precedence or a name serves different
// content depending on which resolver the user runs.
const manifestApex = await apexFor(host).catch(() => null);
const r = manifestApex ? { ...rec.entry.records, ...manifestApex } : rec.entry.records;
// Subdomain inheritance: `checkers.game.x` collapses to `game.x` in the
// registry (see resolver-web `normalizeName`). `ip` semantics apply to the
// whole namespace via Host routing; `s3` semantics are exact-key per name.
@ -9753,7 +9816,12 @@ ipcMain.handle("quicklinks-open", (_e, id) => {
return true;
});
ipcMain.handle("quicklinks-close", () => { closeQuickPanel(); closeLeftPanel(); return true; });
ipcMain.handle("quicklinks-add", () => { openSettingsTab("general"); return true; });
// Opens Settings › Sidebar — the page that holds the web-apps list, the
// panel-mode radio and the reset-widths button. Called by the strip's gear
// button (0.3.101: direct open, no popover) and by the retired "+" button
// (also gone in 0.3.101, kept here as the stable IPC name for any later
// surface that wants to jump to Sidebar settings).
ipcMain.handle("quicklinks-add", () => { openSettingsTab("sidebar"); return true; });
function broadcastSettings() {
sendAllChrome("settings-update", settings);
for (const t of tabs) {

View file

@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
"version": "0.3.100",
"version": "0.3.101",
"license": "MPL-2.0",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",

View file

@ -65,20 +65,17 @@
<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>
<button class="icon gear" id="gearBtn" title="Sidebar settings"><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 settings</span></button>
</div>
<script src="strip-icons.js"></script>
<script>
const Q = window.quicklinks;
const icons = document.getElementById("icons");
const addBtn = document.getElementById("addBtn");
addBtn.onclick = () => Q && Q.add();
// Gear: click opens Settings › Sidebar directly (no popover). The "+" add
// button that used to live below the gear is gone — adding a web app goes
// through Settings › Sidebar now, which the gear opens anyway.
const gearBtn = document.getElementById("gearBtn");
gearBtn.onclick = () => {
const r = gearBtn.getBoundingClientRect();
Q && Q.menu && Q.menu({ x: r.left, y: r.top, width: r.width, height: r.height });
};
gearBtn.onclick = () => Q && Q.add && Q.add();
// 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");