theseus/bundled-addons/screenshot/index.js

147 lines
6.9 KiB
JavaScript
Raw Normal View History

feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
// Screenshot — capture the active tab, preview in the sidebar, then hand
// off to a full-tab editor only when the user asks for it. The old toolbar-
// dropdown flow auto-opened editor.html the instant a capture completed;
// that turned the editor tab into the ACTIVE tab, so any second click of
// the dropdown snapshotted the editor's still-blank canvas and every follow-
// up produced a white PNG. Sidebar-first breaks that loop entirely: the
// preview is served from a data URL inside the sidebar's own document, and
// the editor tab is opened only on an explicit "Open in editor" click.
//
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
// Flow:
// panel invokes "capture" → api.captureTab → write PNG to per-add-on
// scratch dir → return {name, dataUrl, width, height, host, bytes} to
// panel → panel renders <img src="data:…"> in a preview area.
// panel invokes "openInTab" → we stash the raw PNG in api.storage under
// "__pending" and openTab("editor.html") — the editor's preload lets it
// pull that entry out via silentmode.storage.get().
const fs = require("node:fs");
const path = require("node:path");
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
const MAX_RECENT = 6; // ring size for the recent-captures list
const SCRATCH_DIR = "screenshot-scratch"; // sibling of the add-on's storage JSON
module.exports = {
activate(api) {
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
api.registerSidebarPanel({
id: "main",
title: "Screenshot",
icon: "📸",
page: "panel.html",
});
// Per-add-on scratch dir under <userData>/addons-data/. We don't touch
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
// the add-on folder itself — editing it there would confuse users who
// are inspecting the shipped source. api.folder = <userData>/addons/
// screenshot/, so dirname twice lands on <userData>.
const dataParent = path.dirname(path.join(api.folder, ".."));
const scratchDir = path.join(dataParent, "addons-data", SCRATCH_DIR);
try { fs.mkdirSync(scratchDir, { recursive: true }); }
catch (e) { api.log("scratch mkdir failed:", e?.message); }
// Region-select overlay source, loaded once at activation. Region mode
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
// hands this to capture-tab, which injects it into the target tab and
// awaits the {x,y,w,h} the overlay resolves with.
let overlaySource = "";
try { overlaySource = fs.readFileSync(path.join(api.folder, "panel-preload.js"), "utf8"); }
catch (e) { api.log("panel-preload.js not readable:", e?.message); }
function fileUrl(abs) {
return "file:///" + abs.replace(/\\/g, "/").replace(/^\/+/, "").replace(/#/g, "%23").replace(/\?/g, "%3F");
}
function nowStamp() {
const d = new Date();
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}
function pruneRecent(recent) {
const alive = recent.filter((r) => { try { fs.accessSync(r.path); return true; } catch { return false; } });
const trimmed = alive.slice(0, MAX_RECENT);
const dropped = alive.slice(MAX_RECENT);
for (const r of dropped) { try { fs.unlinkSync(r.path); } catch {} }
return trimmed;
}
function writeScratch(dataUrl, name) {
const m = /^data:image\/png;base64,(.+)$/.exec(String(dataUrl));
if (!m) throw new Error("expected image/png data URL from capture");
const buf = Buffer.from(m[1], "base64");
const file = path.join(scratchDir, name);
fs.writeFileSync(file, buf);
return { path: file, bytes: buf.length };
}
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
function readAsDataUrl(absPath) {
const buf = fs.readFileSync(absPath);
return `data:image/png;base64,${buf.toString("base64")}`;
}
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
// panel invokes "capture": take the shot, write to scratch, hand the
// data URL straight back so the sidebar can preview it immediately.
api.onMessage("capture", async (payload) => {
const mode = String(payload && payload.mode || "visible");
if (mode !== "visible" && mode !== "full" && mode !== "region") {
throw new Error(`unknown capture mode: ${mode}`);
}
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
const opts = { mode, format: "png" };
if (mode === "region") opts.overlaySource = overlaySource;
const cap = await api.captureTab(opts);
if (cap.cancelled) { api.log(`region capture cancelled`); return { ok: false, cancelled: true }; }
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
const name = `screenshot-${nowStamp()}${mode === "full" ? "-fullpage" : mode === "region" ? "-region" : ""}.png`;
const written = writeScratch(cap.dataUrl, name);
let recent = api.storage.get("recent", []);
if (!Array.isArray(recent)) recent = [];
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
recent.unshift({ id: name, name, path: written.path, bytes: written.bytes, at: Date.now(), mode });
recent = pruneRecent(recent);
api.storage.set("recent", recent);
api.log(`captured ${mode}${name} (${written.bytes} bytes)`);
return {
ok: true, name, mode,
dataUrl: cap.dataUrl,
width: cap.width, height: cap.height,
host: cap.host, bytes: written.bytes,
};
});
feat(theseus/screenshot): 0.5.0 — sidebar-first editor, direct save/copy, sounds Two problems the old editor kept hitting: - __pending drain race: opening the editor a second time (refresh, back-and- forth navigation) found the storage entry already consumed and bailed to a blank canvas silently. - Cross-origin img loading: editor.html at file:///…/addons/screenshot/ loading a scratch PNG at file:///…/addons-data/ counts as cross-origin under Chromium's file-URL policy; setting crossOrigin="anonymous" made the load fail outright. Rebuilt editor v2: - Load path is idempotent: silentmode.invoke("getBytes", {name}) → addon reads the scratch file and returns a data URL. No __pending drain, no cross-origin trickery — data: URLs are same-origin and never taint the canvas, so getImageData / toBlob keep working. - Two-canvas model (#base + #over, over is pointer-events:none) so live previews don't cost a full re-composite per mousemove. - Tools: cursor, arrow, rect, ellipse, pen, text. 6 swatches, 3 widths, undo / redo (25-deep). Copy + Save at the top bar. Back and Maximize buttons in the same top bar so navigation controls stay reachable when the toolbar wraps at narrow widths. - Keyboard: A/R/O/P/T select tool, Esc = cursor, Ctrl+Z/Shift+Z undo/redo, Ctrl+S save, Ctrl+C copy. - Toast surface for save/copy/error feedback. Sidebar panel gains a direct raw-save path so the user can copy or save the capture without entering the editor: - Two-row actions: [Copy] [Save] on top, [Discard] [Edit] below. - Copy uses navigator.clipboard.write(ClipboardItem); Save uses <a download> with a Blob URL — same path Chromium's will-download tracker already handles, so the file lands in Downloads and the chip updates like any other save. Inline "clear all" confirmation replaces the native confirm() — the old system-modal opened over the tab area (out of the sidebar's visual context) and looked like Windows 95. Now a compact red strip appears under the Recent header with Cancel / Delete buttons. Sounds + a sound-on/off toggle in both surfaces: - Web Audio oscillator-synthesized (no .wav shipped): shutter click on capture, two-tone bloop on copy, descending pair on discard/back, ascending triad on save. - Preference stored in silentmode.storage under "soundOn" (default on), shared between the panel and the editor. Simplifications: - Dropped the addon's "arm" onMessage handler (superseded by getBytes). - Manifest capabilities: sidebar-panel + capture-tab (no open-tab, no toolbar-menu). Bundled but not shipped — parent session handles the OTA sign + push.
2026-09-09 00:56:41 +02:00
// (The old "arm" handler that pre-armed a __pending storage entry is
// gone: editor.html now pulls its bytes via getBytes on load, so there
// is nothing to hand off in advance. Idempotent — reopening the editor
// in the sidebar just re-fetches the bytes.)
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
// Read the ring for a "recent captures" strip in the sidebar. Bytes are
// reported; the actual images are pulled through "getBytes" on demand
// so we don't ship every thumbnail through IPC on every panel open.
api.onMessage("listRecent", () => {
let recent = api.storage.get("recent", []);
if (!Array.isArray(recent)) recent = [];
recent = pruneRecent(recent);
api.storage.set("recent", recent);
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
return recent.map((r) => ({ id: r.id, name: r.name, bytes: r.bytes, at: r.at, mode: r.mode }));
});
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
// Panel asks for a specific past capture's bytes so it can preview it.
api.onMessage("getBytes", (payload) => {
const wantedName = String(payload && payload.name || "");
const recent = api.storage.get("recent", []) || [];
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
const hit = recent.find((r) => r.id === wantedName);
if (!hit) throw new Error(`no recent capture "${wantedName}"`);
return { name: hit.name, dataUrl: readAsDataUrl(hit.path), bytes: hit.bytes, at: hit.at, mode: hit.mode };
});
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
api.onMessage("clearRecent", (payload) => {
const wantedName = payload && payload.name ? String(payload.name) : "";
let recent = api.storage.get("recent", []) || [];
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
if (wantedName) {
const hit = recent.find((r) => r.id === wantedName);
if (hit) { try { fs.unlinkSync(hit.path); } catch {} }
feat(theseus/screenshot): 0.3.0 — sidebar-first flow with explicit "open in tab" 0.3.33 still ships blank screenshots because the whole toolbar-menu → auto- open-editor path can't be made race-free: the moment the editor tab opens it becomes the active tab, and a snapshot of the editor's own tab (before its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG. The lastCapturableTabId fallback I added in a279864 catches the second click, but the first click can still land on the addon-owned tab whenever the user re-triggers before setActive has settled. Rebuild the UX so this class of race can't happen at all: - Drop the toolbar-menu capability. Manifest is back to sidebar-panel + capture-tab + open-tab, so the dock icon opens the panel (never the editor directly). No dropdown, no clip-under-tab-view issue, no auto- jump into an addon-owned tab. - Sidebar has the three capture buttons + a preview <img> + a "Open in editor tab" button. The preview is fed a data:image/png URL returned straight from api.captureTab, rendered inside the sidebar's own document — same origin, no file:// cross-directory gotcha, and the user can see immediately whether the shot actually landed. - Editor.html tab opens only on an explicit "Open in editor tab" click. The addon rewrites __pending at that moment (so the editor always sees the just-selected capture even if a prior editor tab drained the entry), then api.openTab("editor.html", {name}). The editor's storage-based load path is unchanged. - Recent captures ring is kept and now exposed as a horizontal thumbnail strip in the sidebar; clicking a tile re-previews that capture and arms "Open in editor tab" for it. Editor page (editor.html/js/css) unchanged — same crop / arrow / rect / ellipse / pen / text / mosaic-redact / undo / redo / copy / save. Bundled but not shipped — leaving version bump + deploy to the parent session.
2026-09-08 20:26:48 +02:00
recent = recent.filter((r) => r.id !== wantedName);
} else {
for (const r of recent) { try { fs.unlinkSync(r.path); } catch {} }
recent = [];
}
api.storage.set("recent", recent);
return { ok: true };
});
feat(theseus/screenshot): 0.5.0 — sidebar-first editor, direct save/copy, sounds Two problems the old editor kept hitting: - __pending drain race: opening the editor a second time (refresh, back-and- forth navigation) found the storage entry already consumed and bailed to a blank canvas silently. - Cross-origin img loading: editor.html at file:///…/addons/screenshot/ loading a scratch PNG at file:///…/addons-data/ counts as cross-origin under Chromium's file-URL policy; setting crossOrigin="anonymous" made the load fail outright. Rebuilt editor v2: - Load path is idempotent: silentmode.invoke("getBytes", {name}) → addon reads the scratch file and returns a data URL. No __pending drain, no cross-origin trickery — data: URLs are same-origin and never taint the canvas, so getImageData / toBlob keep working. - Two-canvas model (#base + #over, over is pointer-events:none) so live previews don't cost a full re-composite per mousemove. - Tools: cursor, arrow, rect, ellipse, pen, text. 6 swatches, 3 widths, undo / redo (25-deep). Copy + Save at the top bar. Back and Maximize buttons in the same top bar so navigation controls stay reachable when the toolbar wraps at narrow widths. - Keyboard: A/R/O/P/T select tool, Esc = cursor, Ctrl+Z/Shift+Z undo/redo, Ctrl+S save, Ctrl+C copy. - Toast surface for save/copy/error feedback. Sidebar panel gains a direct raw-save path so the user can copy or save the capture without entering the editor: - Two-row actions: [Copy] [Save] on top, [Discard] [Edit] below. - Copy uses navigator.clipboard.write(ClipboardItem); Save uses <a download> with a Blob URL — same path Chromium's will-download tracker already handles, so the file lands in Downloads and the chip updates like any other save. Inline "clear all" confirmation replaces the native confirm() — the old system-modal opened over the tab area (out of the sidebar's visual context) and looked like Windows 95. Now a compact red strip appears under the Recent header with Cancel / Delete buttons. Sounds + a sound-on/off toggle in both surfaces: - Web Audio oscillator-synthesized (no .wav shipped): shutter click on capture, two-tone bloop on copy, descending pair on discard/back, ascending triad on save. - Preference stored in silentmode.storage under "soundOn" (default on), shared between the panel and the editor. Simplifications: - Dropped the addon's "arm" onMessage handler (superseded by getBytes). - Manifest capabilities: sidebar-panel + capture-tab (no open-tab, no toolbar-menu). Bundled but not shipped — parent session handles the OTA sign + push.
2026-09-09 00:56:41 +02:00
api.log("registered screenshot v0.5.0 sidebar panel");
},
};