fix(theseus/addons): captureTab skips addon-owned tabs

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.
This commit is contained in:
Local Dev 2026-09-08 19:14:29 +02:00
parent a28c0c5fe4
commit dc58d0426a

27
main.js
View file

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