From 034d30895f8e5f41c000ff5bec6dd29a22208ff3 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Tue, 8 Sep 2026 19:14:29 +0200 Subject: [PATCH] fix(theseus/addons): captureTab skips addon-owned tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: the Screenshot dropdown produces a blank white PNG "every time". Verified end-to-end via CDP: the FIRST capture on a real page (silentmode.st) worked (221 KB, real pixels). Every capture after that came back as a uniform-white 1920×899 PNG (~24 KB, all bytes 255/255/255/255 confirmed by canvas getImageData in the editor tab). Root cause: api.captureTab reads activeTab(), which after the first successful run is the editor.html tab the add-on just opened. CDP Page.captureScreenshot on the editor's own tab happily snapshots its still-blank canvas, and the addon writes that white PNG into scratch → opens a new editor showing it → next re-capture snapshots THAT editor → all-white feedback loop. Fix: track the last active tab that isn't an add-on-owned page (or the Settings tab) in setActive(), and have captureTab fall back to it when the current active tab has an addonId or settings flag. Last resort: the first non-addon non-settings tab in the list. If none exists, throw a clear "open a page you'd like to shoot first" error instead of returning white pixels. No new capability; no manifest change; addons keep calling api.captureTab({mode}) exactly as before. Ships in the next Theseus release. --- main.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index 2ff2e932..71bba569 100644 --- a/main.js +++ b/main.js @@ -1534,8 +1534,24 @@ function initAddons() { // region — run the caller-supplied overlay source in the tab, wait // for a rect (or null = cancel), then capturePage(rect). captureTab: async (opts, addonId) => { - const t = activeTab(); + // Prefer the currently-active tab, BUT if that's an add-on-owned page + // (e.g. the screenshot editor is already up when the user re-picks a + // mode from the dropdown), fall back to the most-recently-active real + // tab. Otherwise a second capture snapshots the editor's still-blank + // canvas and every follow-up produces a white PNG. + let t = activeTab(); + if (t && (t.addonId || t.settings)) { + const fallback = tabById(lastCapturableTabId); + if (fallback && !fallback.addonId && !fallback.settings) t = fallback; + else { + // Last resort: any non-addon non-settings tab in the list. + t = tabs.find((x) => !x.addonId && !x.settings) || t; + } + } if (!t) throw new Error("no active tab"); + if (t.addonId || t.settings) { + throw new Error("no capturable tab — open a page you'd like to shoot first"); + } const wc = t.view.webContents; const host = t?.prov?.host || (() => { try { return new URL(wc.getURL()).host; } catch { return ""; } })(); const mode = String(opts?.mode || "visible"); @@ -1686,6 +1702,11 @@ const tabs = []; // { id, view, title, url, prov } let activeId = null, tabSeq = 0; const tabById = (id) => tabs.find((t) => t.id === id); const activeTab = () => tabById(activeId); +// The most-recently-active non-addon tab. captureTab falls back to this when +// the currently-active tab is an add-on-owned page (e.g. the screenshot +// editor itself) — otherwise a re-triggered capture snapshots the editor's +// still-blank canvas instead of the page the user actually wants to shoot. +let lastCapturableTabId = null; // Provenance goes to BOTH the top chrome (registry badge + site-info panel) and // the bottom status line, so the resolver detail lives on the bottom bar. @@ -2018,6 +2039,10 @@ function setActive(id) { if (target) target.view.setVisible(true); for (const t of tabs) if (t.id !== id) t.view.setVisible(false); const t = activeTab(); + // Track the last active tab that isn't an add-on-owned page so captureTab + // has a sensible fallback when the user re-triggers the dropdown from + // inside (say) the screenshot editor. + if (t && !t.addonId && !t.settings) lastCapturableTabId = t.id; if (t?.prov) pushNav(t.prov); chrome.webContents.send("bcnr-offer", t?.bcnrOffer ? { host: t.bcnrOffer.host, tld: t.bcnrOffer.tld, registry: REGISTRY } : null); emitTabs();