diff --git a/DESIGN-claude-connector.md b/DESIGN-claude-connector.md index e9626460..bec8757a 100644 --- a/DESIGN-claude-connector.md +++ b/DESIGN-claude-connector.md @@ -96,7 +96,7 @@ Claude Desktop ─stdio─► stdio-shim.cjs ──HTTP──────── | call | does | | --- | --- | -| `list()` | every tab: id, title, url, kind (`web`/`settings`/`addon`/`home`), active, pending | +| `list()` | every tab: id, title, url, kind (`web`/`settings`/`addon`/`home`), active, pending; then the left sidebar's web app (Telegram, WhatsApp, X, the AI chat) as tab `0` with `sidebar: `, while it holds a web page. It is one view, not a tab, and runs the web tabs' preload, so it hands out nothing a tab doesn't; it comes last so it is never taken for the active tab. `activate` reopens the panel, `close` closes it, `navigate` loads in it | | `webContents(id)` | the tab's webContents, after loading a dormant tab and thawing a frozen one; throws for non-web tabs | | `open(url, {background})` | new tab through main's own `createTab` (so BCNR names resolve as typed) | | `navigate(id, url)` | main's `navigateTab` | diff --git a/bundled-addons/claude-connector/lib/tools.cjs b/bundled-addons/claude-connector/lib/tools.cjs index a1b338aa..d9e3d33c 100644 --- a/bundled-addons/claude-connector/lib/tools.cjs +++ b/bundled-addons/claude-connector/lib/tools.cjs @@ -151,7 +151,7 @@ class Controller { const rows = this.tabControl.list().map((t) => { const o = originOf(t.url); const approved = o && this.policy.isAllowed(o); - const base = { id: t.id, active: t.active, window: t.window, kind: t.kind, controlled: this.drivers.has(t.id) }; + const base = { id: t.id, active: t.active, window: t.window, kind: t.kind, controlled: this.drivers.has(t.id), ...(t.sidebar ? { sidebar: t.sidebar } : {}) }; if (t.kind !== "web") return { ...base, title: t.kind === "settings" ? "Settings" : t.title }; return approved ? { ...base, title: t.title, url: t.url } : { ...base, host: hostOf(t.url), approved: false }; }); diff --git a/main.js b/main.js index 369cbac1..c3a09f03 100644 --- a/main.js +++ b/main.js @@ -3212,13 +3212,35 @@ function tabControlKind(t) { const u = (t.pending && t.pending.url) || (() => { try { return t.view.webContents.getURL(); } catch { return ""; } })() || t.url || ""; return /^(?:https?|bns):\/\//i.test(u) ? "web" : "home"; } +// The left sidebar's web app (Telegram, WhatsApp, X, the AI chat) is one +// view, not a tab. While it holds a web page the connector sees it as tab 0, +// listed last so it is never taken for "the active tab". It runs the same +// preload as a web tab, so it hands out nothing a tab doesn't. +const SIDEBAR_CTRL_ID = 0; +let lastQuickLinkId = null; // the web app the panel last showed, to reopen it +function sidebarCtrlEntry() { + if (!quickPanel) return null; + const wc = quickPanel.webContents; + let url = "", title = ""; + try { url = wc.getURL(); title = wc.getTitle(); } catch { return null; } + if (!/^(?:https?|bns):\/\//i.test(url)) return null; + const qid = activeQuickLinkId || lastQuickLinkId; + const L = qid === "llm" ? { title: "AI" } : ((settings.quickLinks || []).find((x) => x.id === qid) || quickLinkFor(url)); + return { id: SIDEBAR_CTRL_ID, title, url, kind: "web", sidebar: (L && L.title) || "Sidebar", active: !!activeQuickLinkId && !leftPanelId, pending: false, window: 0 }; +} +function sidebarCtrl(id) { + if (Number(id) !== SIDEBAR_CTRL_ID) return null; + const e = sidebarCtrlEntry(); + if (!e) throw new Error("the sidebar has no web app open"); + return e; +} function tabControlTab(id) { const t = tabById(Number(id)); if (!t) throw new Error(`no tab ${id}`); return t; } const tabControlHook = { - list: () => tabs.map((t) => ({ + list: () => [...tabs.map((t) => ({ id: t.id, title: t.title || "", url: (t.pending && t.pending.url) || t.url || "", @@ -3226,8 +3248,13 @@ const tabControlHook = { active: isShownTabId(t.id), pending: !!t.pending, window: Math.max(0, windows.indexOf(t.w)), - })), + })), sidebarCtrlEntry()].filter(Boolean), webContents: (id) => { + if (sidebarCtrl(id)) { + const wc = quickPanel.webContents; + if (!wc || wc.isDestroyed()) throw new Error("the sidebar is closed"); + return wc; + } const t = tabControlTab(id); if (tabControlKind(t) !== "web") throw new Error("only web pages can be controlled (not Settings, extension or home tabs)"); if (t.pending) withWin(t.w, () => materializePending(t)); @@ -3243,6 +3270,12 @@ const tabControlHook = { return id; }, navigate: (id, url, addonId) => { + if (sidebarCtrl(id)) { + if (!/^(?:https?|bns):\/\//i.test(url)) throw new Error("navigate needs an http(s) URL"); + console.log(`[addons] [${addonId}] navigate the sidebar app`); + quickPanel.webContents.loadURL(url).catch(() => {}); + return; + } const t = tabControlTab(id); if (tabControlKind(t) !== "web") throw new Error("only web pages can be navigated"); if (!/^(?:https?|bns):\/\//i.test(url)) throw new Error("navigate needs an http(s) URL"); @@ -3250,12 +3283,17 @@ const tabControlHook = { return withWin(t.w, () => navigateTab(t.id, url)); }, close: (id, addonId) => { + if (sidebarCtrl(id)) { console.log(`[addons] [${addonId}] close the sidebar app`); closeQuickPanel(); return; } const t = tabControlTab(id); if (tabControlKind(t) !== "web") throw new Error("only web tabs can be closed"); console.log(`[addons] [${addonId}] close tab ${t.id}`); closeTab(t.id); }, - activate: (id) => { const t = tabControlTab(id); setActive(t.id); }, + activate: (id) => { + const e = sidebarCtrl(id); + if (e) { const q = activeQuickLinkId || lastQuickLinkId || quickLinkFor(e.url)?.id; if (q && !e.active) openQuickPanel(q); return; } + const t = tabControlTab(id); setActive(t.id); + }, setStatus: (st) => { const next = { connected: !!st.connected, @@ -10267,6 +10305,7 @@ function openQuickPanel(id, at) { if (leftPanelId) closeLeftPanel(); if (activeQuickLinkId !== id) leftPanelMax = false; activeQuickLinkId = id; + lastQuickLinkId = id; try { const cur = quickPanel.webContents.getURL(); // Only re-navigate when the panel isn't already showing this origin —