Claude connector: the left sidebar's web app (Telegram, WhatsApp, X, the AI chat) is tab 0

The quick-link panel is one WebContentsView outside `tabs`, so the
connector could not see a web app opened in the sidebar, and the
signed-in session there (Telegram's channel account, for one) was out of
reach. tabControl.list() now ends with the panel as tab 0 while it holds a
web page, tagged with the app's name; webContents, navigate, close and
activate handle it (activate reopens the panel, close closes it). It runs
the same preload as a web tab and still needs the site approved, so it
hands out nothing a tab doesn't. Listed last so it is never picked as the
active tab when a tool call omits tab_id.
This commit is contained in:
Local Dev 2026-10-28 21:45:14 +02:00
parent 5c270ef62f
commit 1457de96b8
3 changed files with 44 additions and 5 deletions

View file

@ -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: <app name>`, 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` |

View file

@ -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 };
});

45
main.js
View file

@ -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 —