@wenathlan/extension 1.1.39 → 1.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -731,7 +731,46 @@ var sessionmemory = class {
731
731
  async getmimefilters() {
732
732
  return await this.adapter.get("mimefilters") ?? [];
733
733
  }
734
+ /** Stores one capture record with its bytes and step linkage, replacing the previous record of that id; the user configured capture retention window expires the oldest bytes while the metadata always survives for the audit trail. */
735
+ async addcapture(record2) {
736
+ const records = await this.getcaptures();
737
+ const retention = (await this.getsettings())?.captureretention;
738
+ const combined = [record2, ...records.filter((item) => item.id !== record2.id)];
739
+ const stored = retention === void 0 ? combined : combined.map((item, index) => index < retention ? item : expirecapturebytes(item));
740
+ await this.adapter.set("captures", stored);
741
+ }
742
+ /** Returns every stored capture record with its metadata, newest first. */
743
+ async getcaptures() {
744
+ return await this.adapter.get("captures") ?? [];
745
+ }
746
+ /** Returns one capture record with its bytes by its id. */
747
+ async getcapture(id) {
748
+ return (await this.getcaptures()).find((item) => item.id === id);
749
+ }
750
+ /** Returns the capture records filtered by run, step and kind. */
751
+ async listcaptures(filter) {
752
+ const records = await this.getcaptures();
753
+ return records.filter((item) => (filter.runid === void 0 || item.runid === filter.runid) && (filter.stepid === void 0 || item.stepid === filter.stepid) && (filter.kind === void 0 || item.kind === filter.kind));
754
+ }
755
+ /** Records one before and after shotpair of the run with its action context. */
756
+ async addpair(pair) {
757
+ const records = await this.getpairs();
758
+ await this.adapter.set("capturepairs", [pair, ...records.filter((item) => item.id !== pair.id)]);
759
+ }
760
+ /** Returns the shotpairs of one run resolved through their before records, newest first; an absent run returns every pair. */
761
+ async getpairs(runid) {
762
+ const records = await this.adapter.get("capturepairs") ?? [];
763
+ if (runid === void 0) return records;
764
+ const runs = /* @__PURE__ */ new Map();
765
+ for (const capture of await this.getcaptures()) runs.set(capture.id, capture.runid);
766
+ return records.filter((item) => runs.get(item.beforeid) === runid);
767
+ }
734
768
  };
769
+ function expirecapturebytes(record2) {
770
+ const { bytes, ...metadata } = record2;
771
+ void bytes;
772
+ return { ...metadata, bytesexpired: true };
773
+ }
735
774
  function randomid() {
736
775
  return crypto.randomUUID();
737
776
  }
@@ -739,16 +778,17 @@ function randomid() {
739
778
  // policy.ts
740
779
  var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts"]);
741
780
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
742
- var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures"]);
781
+ var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
743
782
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
744
783
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
745
- var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract"]);
784
+ var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract", "shotelement"]);
746
785
  var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "pausedownload", "resumedownload", "verifydownload", "writeclipboard", "quarantinedownload", "scanvirus"]);
747
786
  var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
748
787
  var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
749
788
  var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
750
789
  var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
751
790
  var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
791
+ var captureactions = /* @__PURE__ */ new Set(["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"]);
752
792
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
753
793
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
754
794
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
@@ -809,6 +849,92 @@ function isexportkind(kind) {
809
849
  function isfileskind(kind) {
810
850
  return filesactions.has(kind);
811
851
  }
852
+ function iscapturekind(kind) {
853
+ return captureactions.has(kind);
854
+ }
855
+ function capturegate(session, tabid2, origin, now) {
856
+ if (!session || session.stoppedat) return { allowed: false, reason: "No active browser session exists for the capture." };
857
+ if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and cannot capture." };
858
+ if (session.pausedat) return { allowed: false, reason: "The browser session is paused and cannot capture." };
859
+ if (session.tabid !== tabid2) return { allowed: false, reason: `The capture needs the active tab grant of session tab ${session.tabid} and refuses tab ${tabid2}.` };
860
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The capture of ${origin} needs the session origin grants first.` };
861
+ return { allowed: true };
862
+ }
863
+ function validatecaptureoptions(value) {
864
+ if (value === void 0) return { allowed: true };
865
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "The reviewed capture options must be an object in options.capture." };
866
+ const options = value;
867
+ if (options.format !== void 0 && options.format !== "png" && options.format !== "jpeg" && options.format !== "webp") return { allowed: false, reason: "The reviewed capture format must be png, jpeg or webp." };
868
+ if (options.quality !== void 0 && (typeof options.quality !== "number" || !Number.isFinite(options.quality) || options.quality < 0 || options.quality > 100)) return { allowed: false, reason: "The reviewed capture quality must stay between zero and one hundred; any value in that range is the user choice with no code cap." };
869
+ if (options.pixelratio !== void 0 && (typeof options.pixelratio !== "number" || !Number.isFinite(options.pixelratio) || options.pixelratio < 1)) return { allowed: false, reason: "The reviewed pixel ratio starts at one and climbs to any user configured ceiling with no code ceiling." };
870
+ if (options.annotate !== void 0 && typeof options.annotate !== "boolean") return { allowed: false, reason: "The reviewed capture annotation flag must be a boolean." };
871
+ if (options.exporttarget !== void 0 && options.exporttarget !== "memory" && options.exporttarget !== "download" && options.exporttarget !== "clipboard") return { allowed: false, reason: "The reviewed capture export target must be memory, download or clipboard." };
872
+ return { allowed: true };
873
+ }
874
+ function validateregionrect(value) {
875
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed regionrect with x, y, width and height in css pixels is required in options." };
876
+ const rect = value;
877
+ for (const field of ["x", "y", "width", "height"]) {
878
+ if (typeof rect[field] !== "number" || !Number.isFinite(rect[field])) return { allowed: false, reason: `The reviewed regionrect needs a numeric ${field} in css pixels.` };
879
+ }
880
+ if (rect.x < 0 || rect.y < 0) return { allowed: false, reason: "The reviewed regionrect refuses negative coordinates." };
881
+ if (rect.width <= 0 || rect.height <= 0) return { allowed: false, reason: "The reviewed regionrect needs positive width and height values." };
882
+ return { allowed: true };
883
+ }
884
+ function validatecapturenaming(value) {
885
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed capturenaming rule with run, step, sequence and kind flags is required." };
886
+ const rule = value;
887
+ const segments = ["run", "step", "sequence", "kind"];
888
+ for (const key of Object.keys(rule)) {
889
+ if (!segments.includes(key)) return { allowed: false, reason: `The reviewed capturenaming rule refuses the unknown ${key} segment; only run, step, sequence and kind participate.` };
890
+ }
891
+ for (const segment of segments) {
892
+ if (rule[segment] !== void 0 && typeof rule[segment] !== "boolean") return { allowed: false, reason: `The reviewed capturenaming ${segment} flag must be a boolean.` };
893
+ }
894
+ if (!segments.some((segment) => rule[segment] === true)) return { allowed: false, reason: "The reviewed capturenaming rule needs at least one enabled segment of run, step, sequence and kind." };
895
+ return { allowed: true };
896
+ }
897
+ function stitchbudgetallowed(tiles, settle2, wait) {
898
+ if (tiles <= 0) return { allowed: false, reason: "The stitch budget needs at least one tile." };
899
+ if (settle2 < 0 || wait < 0) return { allowed: false, reason: "The reviewed settle and wait windows must be zero or positive milliseconds." };
900
+ if (tiles * settle2 > wait) return { allowed: false, reason: `The stitching scroll budget of ${tiles} tiles at ${settle2} milliseconds exceeds the reviewed wait window of ${wait} milliseconds; review a wider window or a smaller settle.` };
901
+ return { allowed: true };
902
+ }
903
+ function beforeafterwrapallowed(kind) {
904
+ return allowedactions.has(kind) && !captureactions.has(kind);
905
+ }
906
+ function validatecapturegrammar(step, options) {
907
+ const kind = step.kind;
908
+ const optioncheck = validatecaptureoptions(options.capture);
909
+ if (!optioncheck.allowed) return optioncheck;
910
+ if (options.settle !== void 0 && (typeof options.settle !== "number" || !Number.isFinite(options.settle) || options.settle < 0)) return { allowed: false, reason: "The reviewed capture settle window must be zero or a positive number of milliseconds." };
911
+ if (options.overlap !== void 0 && (typeof options.overlap !== "number" || !Number.isInteger(options.overlap) || options.overlap < 0)) return { allowed: false, reason: "The reviewed stitch overlap must be zero or a positive number of rows." };
912
+ if (options.wait !== void 0 && (typeof options.wait !== "number" || !Number.isFinite(options.wait) || options.wait < 0)) return { allowed: false, reason: "The reviewed capture wait window must be zero or a positive number of milliseconds." };
913
+ if (options.naming !== void 0) {
914
+ const namingcheck = validatecapturenaming(options.naming);
915
+ if (!namingcheck.allowed) return namingcheck;
916
+ }
917
+ if (kind === "shotregion") {
918
+ const rectcheck = validateregionrect(options.regionrect);
919
+ if (!rectcheck.allowed) return rectcheck;
920
+ if (options.reviewed !== true) return { allowed: false, reason: "Every reviewed regionrect needs the explicit reviewed flag before shotregion runs." };
921
+ if (options.container !== void 0 && !isnonempty(options.container)) return { allowed: false, reason: "The reviewed scrollable container selector must be a non-empty string." };
922
+ if (options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed container scroll steps must be a positive integer with no code ceiling." };
923
+ }
924
+ if (kind === "contactsheet") {
925
+ const elements = options.elements;
926
+ if (!Array.isArray(elements) || elements.length === 0 || !elements.every((item) => isnonempty(item))) return { allowed: false, reason: "A reviewed non-empty list of element selectors is required in options for the contact sheet; the cell count stays the user choice." };
927
+ const layout = options.sheet;
928
+ if (layout !== void 0) {
929
+ if (!layout || typeof layout !== "object" || Array.isArray(layout)) return { allowed: false, reason: "The reviewed sheetlayout must be an object with cellsize, columns and label." };
930
+ const sheet = layout;
931
+ if (typeof sheet.cellsize !== "number" || !Number.isFinite(sheet.cellsize) || sheet.cellsize <= 0) return { allowed: false, reason: "The reviewed contact sheet cell size must be a positive number of pixels." };
932
+ if (typeof sheet.columns !== "number" || !Number.isInteger(sheet.columns) || sheet.columns < 1) return { allowed: false, reason: "The reviewed contact sheet column count must be a positive integer with no code ceiling." };
933
+ if (sheet.label !== void 0 && sheet.label !== "none" && sheet.label !== "index" && sheet.label !== "selector" && sheet.label !== "both") return { allowed: false, reason: "The reviewed contact sheet label style must be none, index, selector or both." };
934
+ }
935
+ }
936
+ return { allowed: true };
937
+ }
812
938
  function exportgranted(session, origin) {
813
939
  if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
814
940
  return { allowed: true };
@@ -1605,6 +1731,10 @@ function validatestep(step, origin) {
1605
1731
  const filescheck = validatefilesgrammar(step, options);
1606
1732
  if (!filescheck.allowed) return filescheck;
1607
1733
  }
1734
+ if (iscapturekind(step.kind)) {
1735
+ const capturecheck = validatecapturegrammar(step, options);
1736
+ if (!capturecheck.allowed) return capturecheck;
1737
+ }
1608
1738
  if (step.kind === "tabcreate") {
1609
1739
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1610
1740
  if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
@@ -1664,6 +1794,18 @@ function canexecute(input) {
1664
1794
  if (!clipgate.allowed) return clipgate;
1665
1795
  }
1666
1796
  if (input.step.kind === "interceptmime" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The download interception is outside the session origin grants." };
1797
+ if (iscapturekind(input.step.kind)) {
1798
+ const capturegatecheck = capturegate(input.session, input.tabid, input.origin, now);
1799
+ if (!capturegatecheck.allowed) return capturegatecheck;
1800
+ let captureoptions = {};
1801
+ try {
1802
+ captureoptions = parseoptions(input.step);
1803
+ } catch {
1804
+ captureoptions = {};
1805
+ }
1806
+ const target = captureoptions.capture?.exporttarget;
1807
+ if (target !== void 0 && target !== "memory" && target !== "download" && target !== "clipboard") return { allowed: false, reason: "The capture export target must be memory, download or clipboard." };
1808
+ }
1667
1809
  if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
1668
1810
  let options = {};
1669
1811
  try {
@@ -1774,9 +1916,20 @@ function recorddownload(progress, planid, stepid, entry, now) {
1774
1916
  const outcome = { stepid, ok: entry.state === "complete", summary: `Download ${entry.index + 1} of ${entry.url} ended in the ${entry.state} state.`, details: { download: entry }, at: now };
1775
1917
  return recordoutcome(base, planid, outcome, now);
1776
1918
  }
1919
+ function recordcapture(progress, planid, stepid, capture, now) {
1920
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1921
+ const bytes = capture.bytes?.length ?? 0;
1922
+ const outcome = { stepid, ok: true, summary: `Captured a ${capture.format} ${capture.kind} shot of ${capture.width} by ${capture.height} pixels with ${bytes} character${bytes === 1 ? "" : "s"} of image data.`, details: { capture: { id: capture.id, kind: capture.kind, format: capture.format, width: capture.width, height: capture.height, bytes } }, at: now };
1923
+ return recordoutcome(base, planid, outcome, now);
1924
+ }
1925
+ function recordpair(progress, planid, stepid, pair, now) {
1926
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1927
+ const outcome = { stepid, ok: true, summary: `Paired the before shot ${pair.beforeid} with the after shot ${pair.afterid} around the ${pair.actionkind} action.`, details: { shotpair: { id: pair.id, beforeid: pair.beforeid, afterid: pair.afterid, actionkind: pair.actionkind, ...pair.target !== void 0 ? { target: pair.target } : {}, ...pair.domsnapshotid !== void 0 ? { domsnapshotid: pair.domsnapshotid } : {} } }, at: now };
1928
+ return recordoutcome(base, planid, outcome, now);
1929
+ }
1777
1930
 
1778
1931
  // version.ts
1779
- var packageversion = "1.1.39";
1932
+ var packageversion = "1.1.40";
1780
1933
 
1781
1934
  // types.ts
1782
1935
  var protocolversion = packageversion;
@@ -1840,7 +1993,7 @@ function requestbody(input) {
1840
1993
  return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
1841
1994
  }
1842
1995
  function outcomeresponse(input) {
1843
- return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {} });
1996
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {} });
1844
1997
  }
1845
1998
  function mapresponse(input) {
1846
1999
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
@@ -1902,6 +2055,195 @@ function netlogreport(input) {
1902
2055
  function quarantinereport(input) {
1903
2056
  return { version: protocolversion, entries: input.entries };
1904
2057
  }
2058
+ function capturereport(input) {
2059
+ return { version: protocolversion, records: input.records, pairs: input.pairs };
2060
+ }
2061
+
2062
+ // capture.ts
2063
+ var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
2064
+ function captureoptionsof(value) {
2065
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2066
+ const options = value;
2067
+ const normalized = {};
2068
+ if (options.format === "png" || options.format === "jpeg" || options.format === "webp") normalized.format = options.format;
2069
+ if (typeof options.quality === "number" && Number.isFinite(options.quality)) normalized.quality = options.quality;
2070
+ if (typeof options.pixelratio === "number" && Number.isFinite(options.pixelratio)) normalized.pixelratio = options.pixelratio;
2071
+ if (typeof options.annotate === "boolean") normalized.annotate = options.annotate;
2072
+ if (options.exporttarget === "memory" || options.exporttarget === "download" || options.exporttarget === "clipboard") normalized.exporttarget = options.exporttarget;
2073
+ return normalized;
2074
+ }
2075
+ function capturevisible(input) {
2076
+ const ratio = input.options.pixelratio ?? 1;
2077
+ return {
2078
+ id: input.id,
2079
+ runid: input.runid,
2080
+ stepid: input.stepid,
2081
+ kind: "shotview",
2082
+ format: input.options.format ?? "png",
2083
+ width: Math.round(input.viewport.width * ratio),
2084
+ height: Math.round(input.viewport.height * ratio),
2085
+ capturedat: input.at,
2086
+ bytes: input.dataurl,
2087
+ ...input.name !== void 0 ? { name: input.name } : {},
2088
+ ...input.options.annotate === true ? { annotated: true } : {},
2089
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
2090
+ ...input.target !== void 0 ? { target: input.target } : {}
2091
+ };
2092
+ }
2093
+ function capturestitched(input) {
2094
+ const ratio = input.options.pixelratio ?? 1;
2095
+ return {
2096
+ id: input.id,
2097
+ runid: input.runid,
2098
+ stepid: input.stepid,
2099
+ kind: "shotfullpage",
2100
+ format: input.options.format ?? "png",
2101
+ width: Math.round(input.plan.scrollwidth * ratio),
2102
+ height: Math.round(input.plan.scrollheight * ratio),
2103
+ capturedat: input.at,
2104
+ bytes: input.dataurl,
2105
+ ...input.name !== void 0 ? { name: input.name } : {},
2106
+ ...input.options.annotate === true ? { annotated: true } : {},
2107
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {}
2108
+ };
2109
+ }
2110
+ function captureelement(input) {
2111
+ const ratio = input.options.pixelratio ?? 1;
2112
+ const scaled = scaledrect(input.rect, ratio);
2113
+ return {
2114
+ id: input.id,
2115
+ runid: input.runid,
2116
+ stepid: input.stepid,
2117
+ kind: "shotelement",
2118
+ format: input.options.format ?? "png",
2119
+ width: scaled.width,
2120
+ height: scaled.height,
2121
+ capturedat: input.at,
2122
+ bytes: input.dataurl,
2123
+ ...input.name !== void 0 ? { name: input.name } : {},
2124
+ ...input.options.annotate === true ? { annotated: true } : {},
2125
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
2126
+ ...input.target !== void 0 ? { target: input.target } : {}
2127
+ };
2128
+ }
2129
+ function captureregion(input) {
2130
+ const ratio = input.options.pixelratio ?? 1;
2131
+ const scaled = scaledrect(input.rect, ratio);
2132
+ return {
2133
+ id: input.id,
2134
+ runid: input.runid,
2135
+ stepid: input.stepid,
2136
+ kind: "shotregion",
2137
+ format: input.options.format ?? "png",
2138
+ width: scaled.width,
2139
+ height: scaled.height,
2140
+ capturedat: input.at,
2141
+ bytes: input.dataurl,
2142
+ ...input.name !== void 0 ? { name: input.name } : {},
2143
+ ...input.options.annotate === true ? { annotated: true } : {},
2144
+ ...input.options.exporttarget !== void 0 ? { exporttarget: input.options.exporttarget } : {},
2145
+ ...input.target !== void 0 ? { target: input.target } : {}
2146
+ };
2147
+ }
2148
+ function pairstates(before, after, action, at, id) {
2149
+ if (!before) return { skipped: "before", reason: "The before shot was not captured, so no state pair exists." };
2150
+ if (!after) return { skipped: "after", reason: `The action of kind ${action.kind} failed before the after shot, so the state pair is skipped.` };
2151
+ return {
2152
+ pair: {
2153
+ id,
2154
+ beforeid: before.id,
2155
+ afterid: after.id,
2156
+ actionkind: action.kind,
2157
+ ...action.target !== void 0 ? { target: action.target } : {},
2158
+ ...action.domsnapshotid !== void 0 ? { domsnapshotid: action.domsnapshotid } : {},
2159
+ at
2160
+ },
2161
+ reason: `Paired the before shot ${before.id} with the after shot ${after.id} around the ${action.kind} action.`
2162
+ };
2163
+ }
2164
+ function capturestates(input) {
2165
+ if (input.policy !== "beforeafter") return { reason: `The ${input.policy} capture policy takes no state pair around the ${input.actionkind} action.` };
2166
+ return pairstates(input.before, input.after, { kind: input.actionkind, ...input.target !== void 0 ? { target: input.target } : {}, ...input.domsnapshotid !== void 0 ? { domsnapshotid: input.domsnapshotid } : {} }, input.at, input.id);
2167
+ }
2168
+ function buildstitchplan(input) {
2169
+ const overlap = Math.max(0, Math.round(input.overlap ?? 0));
2170
+ const stepy = Math.max(1, input.viewportheight - overlap);
2171
+ const columns = Math.max(1, Math.ceil(input.scrollwidth / input.viewportwidth));
2172
+ const rows = input.scrollheight <= input.viewportheight ? 1 : Math.max(1, Math.ceil((input.scrollheight - overlap) / stepy));
2173
+ const tiles = [];
2174
+ for (let column = 0; column < columns; column += 1) {
2175
+ for (let row = 0; row < rows; row += 1) {
2176
+ const x = Math.min(column * input.viewportwidth, Math.max(0, input.scrollwidth - input.viewportwidth));
2177
+ const y = rows === 1 ? 0 : Math.min(row * stepy, Math.max(0, input.scrollheight - input.viewportheight));
2178
+ tiles.push({ x: Math.round(x), y: Math.round(y) });
2179
+ }
2180
+ }
2181
+ return { columns, rows, tiles, overlap, scrollwidth: Math.round(input.scrollwidth), scrollheight: Math.round(input.scrollheight), viewportwidth: Math.round(input.viewportwidth), viewportheight: Math.round(input.viewportheight) };
2182
+ }
2183
+ function seamweights(overlap) {
2184
+ if (overlap <= 0) return [];
2185
+ const weights = [];
2186
+ for (let index = 0; index < overlap; index += 1) weights.push((index + 1) / (overlap + 1));
2187
+ return weights;
2188
+ }
2189
+ function fixedheadermatch(band, firstband) {
2190
+ if (band.length === 0 || band.length !== firstband.length) return false;
2191
+ return band.every((value, index) => value === firstband[index]);
2192
+ }
2193
+ function scaledrect(rect, pixelratio) {
2194
+ const ratio = pixelratio >= 1 ? pixelratio : 1;
2195
+ return { x: Math.round(rect.x * ratio), y: Math.round(rect.y * ratio), width: Math.round(rect.width * ratio), height: Math.round(rect.height * ratio) };
2196
+ }
2197
+ function croprect(rect, viewport) {
2198
+ const x = Math.max(0, rect.x);
2199
+ const y = Math.max(0, rect.y);
2200
+ return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(0, Math.min(rect.width, viewport.width - x))), height: Math.round(Math.max(0, Math.min(rect.height, viewport.height - y))) };
2201
+ }
2202
+ function regionsteps(containerheight, viewportstep) {
2203
+ if (containerheight <= 0 || viewportstep <= 0) return [0];
2204
+ const steps = [];
2205
+ for (let top = 0; top < containerheight; top += viewportstep) {
2206
+ const clamped = Math.min(top, Math.max(0, containerheight - viewportstep));
2207
+ if (!steps.includes(clamped)) steps.push(clamped);
2208
+ }
2209
+ return steps;
2210
+ }
2211
+ function buildsheet(cells, layout) {
2212
+ const columns = Math.max(1, Math.round(layout.columns));
2213
+ const rows = Math.max(1, Math.ceil(cells.length / columns));
2214
+ const placed = cells.map((cell, index) => {
2215
+ const column = index % columns;
2216
+ const row = Math.floor(index / columns);
2217
+ const label = cell.label ?? "";
2218
+ const caption = layout.label === "none" ? "" : layout.label === "index" ? `${index + 1}` : layout.label === "selector" ? cell.selector : label ? `${index + 1} \xB7 ${cell.selector} \xB7 ${label}` : `${index + 1} \xB7 ${cell.selector}`;
2219
+ return { index, column, row, selector: cell.selector, label, caption };
2220
+ });
2221
+ return { columns, rows, cells: placed };
2222
+ }
2223
+ function capturepart(value) {
2224
+ return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
2225
+ }
2226
+ function buildname(rule, parts, extension) {
2227
+ const segments = [];
2228
+ if (rule.run) segments.push(capturepart(parts.run));
2229
+ if (rule.step) segments.push(capturepart(parts.step));
2230
+ if (rule.sequence) segments.push(String(Math.max(0, Math.round(parts.sequence))));
2231
+ if (rule.kind) segments.push(capturepart(parts.kind));
2232
+ const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
2233
+ return `${(segments.length > 0 ? segments : ["capture"]).join("-")}.${safeextension}`;
2234
+ }
2235
+ function annotationplanof(input) {
2236
+ const inset = Math.min(24, Math.max(8, Math.round(Math.min(input.width, input.height) / 12)));
2237
+ const plan = {
2238
+ marker: { x: inset, y: inset, number: Math.max(1, Math.round(input.step)) },
2239
+ footer: `${new Date(input.at).toISOString()} \xB7 ${input.url}`
2240
+ };
2241
+ if (input.rect !== void 0) {
2242
+ const expansion = 2;
2243
+ plan.outline = { x: Math.round(input.rect.x - expansion), y: Math.round(input.rect.y - expansion), width: Math.round(input.rect.width + expansion * 2), height: Math.round(input.rect.height + expansion * 2) };
2244
+ }
2245
+ return plan;
2246
+ }
1905
2247
 
1906
2248
  // extension/browsertabs.ts
1907
2249
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -3018,12 +3360,12 @@ function scanverdictof(response) {
3018
3360
  function released(entry, ref, at) {
3019
3361
  return { ...entry, release: ref, updatedat: at };
3020
3362
  }
3021
- function capturepart(value) {
3363
+ function capturepart2(value) {
3022
3364
  return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
3023
3365
  }
3024
3366
  function capturefilename(name, extension) {
3025
3367
  const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
3026
- return `${capturepart(name.task)}-${capturepart(name.step)}-${name.sequence}.${safeextension}`;
3368
+ return `${capturepart2(name.task)}-${capturepart2(name.step)}-${name.sequence}.${safeextension}`;
3027
3369
  }
3028
3370
  function advancecounter(counters, base) {
3029
3371
  const sequence = (counters[base] ?? 0) + 1;
@@ -3102,8 +3444,9 @@ function stepoptions2(step) {
3102
3444
  }
3103
3445
  async function refreshcapabilities() {
3104
3446
  const report = await readcapabilities();
3105
- await memory.setcapabilities(report);
3106
- return report;
3447
+ const withcaptures = { ...report, captures: [...capturekinds] };
3448
+ await memory.setcapabilities(withcaptures);
3449
+ return withcaptures;
3107
3450
  }
3108
3451
  async function activecontext() {
3109
3452
  const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
@@ -3256,6 +3599,7 @@ function stepauditkind(step, ok) {
3256
3599
  if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
3257
3600
  if (step.kind === "consentpassword") return "consent";
3258
3601
  if (step.kind === "handoffcaptcha") return "handoff";
3602
+ if (iscapturekind(step.kind)) return "capture";
3259
3603
  if (isfileskind(step.kind)) {
3260
3604
  if (step.kind === "interceptmime") return "intercept";
3261
3605
  if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
@@ -4941,6 +5285,374 @@ async function reconcilmimefilter() {
4941
5285
  }
4942
5286
  reconcilmimefilter().catch(() => {
4943
5287
  });
5288
+ var stitchprogress = /* @__PURE__ */ new Map();
5289
+ function stepcaptureoptions(step) {
5290
+ return captureoptionsof(stepoptions2(step).capture);
5291
+ }
5292
+ async function blobtodataurl(blob) {
5293
+ const buffer = new Uint8Array(await blob.arrayBuffer());
5294
+ let binary = "";
5295
+ const chunk = 32768;
5296
+ for (let index = 0; index < buffer.length; index += chunk) binary += String.fromCharCode(...buffer.subarray(index, index + chunk));
5297
+ return `data:${blob.type};base64,${btoa(binary)}`;
5298
+ }
5299
+ async function tabshot(format, quality) {
5300
+ const shot = { format: format === "jpeg" ? "jpeg" : "png" };
5301
+ if (format === "jpeg" && quality !== void 0) shot.quality = Math.max(0, Math.min(100, Math.round(quality)));
5302
+ return chrome.tabs.captureVisibleTab(chrome.windows.WINDOW_ID_CURRENT, shot);
5303
+ }
5304
+ async function bridgecall(tabid2, name, ...args) {
5305
+ await chrome.scripting.executeScript({ target: { tabId: tabid2 }, files: ["pagebridge.js"] });
5306
+ const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: (bridgeName, bridgeArgs) => {
5307
+ const bridge = globalThis.devthinkbridge;
5308
+ if (!bridge) throw new Error("Devthink page bridge is unavailable.");
5309
+ const seam = bridge[bridgeName];
5310
+ if (typeof seam !== "function") throw new Error(`The page bridge has no ${bridgeName} capture seam.`);
5311
+ return seam(...bridgeArgs);
5312
+ }, args: [name, args] });
5313
+ return result[0]?.result;
5314
+ }
5315
+ function tileband(bitmap, rows) {
5316
+ if (rows <= 0 || bitmap.height <= 0) return [];
5317
+ const canvas = new OffscreenCanvas(bitmap.width, Math.min(rows, bitmap.height));
5318
+ const context = canvas.getContext("2d");
5319
+ if (!context) return [];
5320
+ context.drawImage(bitmap, 0, 0);
5321
+ const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
5322
+ const band = [];
5323
+ const pixels = canvas.width * canvas.height;
5324
+ const stride = Math.max(1, Math.floor(pixels / 64));
5325
+ for (let pixel = 0; pixel < pixels; pixel += stride) {
5326
+ const index = pixel * 4;
5327
+ band.push((data[index] ?? 0) + (data[index + 1] ?? 0) * 256 + (data[index + 2] ?? 0) * 65536 + (data[index + 3] ?? 0) * 16777216);
5328
+ }
5329
+ return band;
5330
+ }
5331
+ function drawannotations(context, plan, width, height, ratio) {
5332
+ context.save();
5333
+ context.scale(ratio, ratio);
5334
+ context.fillStyle = "rgba(47,128,237,.92)";
5335
+ context.beginPath();
5336
+ context.arc(plan.marker.x, plan.marker.y, 14, 0, Math.PI * 2);
5337
+ context.fill();
5338
+ context.fillStyle = "#ffffff";
5339
+ context.font = "bold 14px sans-serif";
5340
+ context.textBaseline = "middle";
5341
+ context.textAlign = "center";
5342
+ context.fillText(String(plan.marker.number), plan.marker.x, plan.marker.y);
5343
+ if (plan.outline !== void 0) {
5344
+ context.strokeStyle = "#2f80ed";
5345
+ context.lineWidth = 2;
5346
+ context.strokeRect(plan.outline.x, plan.outline.y, plan.outline.width, plan.outline.height);
5347
+ }
5348
+ const footery = Math.max(12, height - 22);
5349
+ context.fillStyle = "rgba(23,32,44,.86)";
5350
+ context.fillRect(0, footery, width, 22);
5351
+ context.fillStyle = "#ffffff";
5352
+ context.font = "12px sans-serif";
5353
+ context.textAlign = "left";
5354
+ context.fillText(plan.footer.slice(0, 160), 8, footery + 11);
5355
+ context.restore();
5356
+ }
5357
+ async function canvasdataurl(canvas, format, quality) {
5358
+ const type = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
5359
+ const encoded = format === "png" ? await canvas.convertToBlob({ type }) : await canvas.convertToBlob({ type, quality: Math.max(0, Math.min(1, (quality ?? 100) / 100)) });
5360
+ return blobtodataurl(encoded);
5361
+ }
5362
+ async function composestitched(tiles, width, height, overlap, ratio, options) {
5363
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(width * ratio)), Math.max(1, Math.round(height * ratio)));
5364
+ const context = canvas.getContext("2d");
5365
+ if (!context) throw new Error("The stitcher could not create a canvas context.");
5366
+ const weights = seamweights(overlap);
5367
+ const firstband = overlap > 0 && tiles[0] ? tileband(tiles[0].bitmap, overlap) : [];
5368
+ for (const tile of tiles) {
5369
+ const band = overlap > 0 ? tileband(tile.bitmap, overlap) : [];
5370
+ const repeated = overlap > 0 && !tile.firstofcolumn && fixedheadermatch(band, firstband);
5371
+ if (repeated) {
5372
+ const skipped = Math.min(overlap, tile.bitmap.height);
5373
+ context.drawImage(tile.bitmap, 0, skipped, tile.bitmap.width, tile.bitmap.height - skipped, tile.x, tile.y + skipped, tile.bitmap.width, tile.bitmap.height - skipped);
5374
+ continue;
5375
+ }
5376
+ if (weights.length > 0 && !tile.firstofcolumn) {
5377
+ for (let row = 0; row < weights.length && row < tile.bitmap.height; row += 1) {
5378
+ context.globalAlpha = weights[row] ?? 1;
5379
+ context.drawImage(tile.bitmap, 0, row, tile.bitmap.width, 1, tile.x, tile.y + row, tile.bitmap.width, 1);
5380
+ }
5381
+ context.globalAlpha = 1;
5382
+ const below = Math.min(overlap, tile.bitmap.height);
5383
+ context.drawImage(tile.bitmap, 0, below, tile.bitmap.width, tile.bitmap.height - below, tile.x, tile.y + below, tile.bitmap.width, tile.bitmap.height - below);
5384
+ continue;
5385
+ }
5386
+ context.drawImage(tile.bitmap, tile.x, tile.y);
5387
+ }
5388
+ if (options.annotate) drawannotations(context, options.annotate, width, height, ratio);
5389
+ return canvasdataurl(canvas, options.format, options.quality);
5390
+ }
5391
+ async function decodeTile(dataurl, x, y, firstofcolumn) {
5392
+ const blob = await (await fetch(dataurl)).blob();
5393
+ return { bitmap: await createImageBitmap(blob), x, y, firstofcolumn };
5394
+ }
5395
+ async function croptoRect(dataurl, rect, ratio, options) {
5396
+ const blob = await (await fetch(dataurl)).blob();
5397
+ const bitmap = await createImageBitmap(blob);
5398
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(rect.width * ratio)), Math.max(1, Math.round(rect.height * ratio)));
5399
+ const context = canvas.getContext("2d");
5400
+ if (!context) throw new Error("The crop could not create a canvas context.");
5401
+ context.drawImage(bitmap, rect.x, rect.y, rect.width, rect.height, 0, 0, canvas.width, canvas.height);
5402
+ if (options.annotate) drawannotations(context, options.annotate, rect.width, rect.height, ratio);
5403
+ return canvasdataurl(canvas, options.format, options.quality);
5404
+ }
5405
+ async function encodecanvas(width, height, draw, options) {
5406
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
5407
+ const context = canvas.getContext("2d");
5408
+ if (!context) throw new Error("The capture canvas could not create a context.");
5409
+ await draw(context);
5410
+ if (options.annotate) drawannotations(context, options.annotate, canvas.width / (options.annotateratio ?? 1), canvas.height / (options.annotateratio ?? 1), options.annotateratio ?? 1);
5411
+ return canvasdataurl(canvas, options.format, options.quality);
5412
+ }
5413
+ async function capturenamefor(step, plan, kind, format) {
5414
+ const naming = stepoptions2(step).naming;
5415
+ const rule = naming && typeof naming === "object" && !Array.isArray(naming) ? naming : { run: true, step: true, sequence: true, kind: true };
5416
+ const counters = (await memory.getcapturecounters()).find((item) => item.taskid === plan.id);
5417
+ const advanced = advancecounter(counters?.counters ?? {}, step.id);
5418
+ await memory.setcapturecounter({ taskid: plan.id, counters: advanced.counters, at: Date.now() });
5419
+ return buildname(rule, { run: plan.id, step: step.id, sequence: advanced.sequence, kind }, format);
5420
+ }
5421
+ async function routecapture(record2, session, plan, step, origin) {
5422
+ const target = record2.exporttarget ?? "memory";
5423
+ if (target === "clipboard") {
5424
+ const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
5425
+ if (!granted) throw new Error("The clipboard capture export needs the clipboardwrite capability; request it from the review panel.");
5426
+ const blob = await (await fetch(record2.bytes ?? "")).blob();
5427
+ await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
5428
+ await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes ?? ""), length: (record2.bytes ?? "").length }, origin, step.id, Date.now()));
5429
+ void session;
5430
+ void plan;
5431
+ return { target, destination: "clipboard" };
5432
+ }
5433
+ if (target === "download") {
5434
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
5435
+ if (!granted) throw new Error("The download capture export needs the downloads capability; request it from the review panel.");
5436
+ await chrome.downloads.download({ url: record2.bytes ?? "", filename: record2.name ?? `capture.${record2.format}` });
5437
+ return { target, destination: "reviewed download flow" };
5438
+ }
5439
+ return { target, destination: "session memory" };
5440
+ }
5441
+ async function runcapturepolicy() {
5442
+ return (await memory.getsettings())?.capturepolicy ?? "manual";
5443
+ }
5444
+ async function grabstateshot(step, session, plan, tabid2, phase) {
5445
+ const options = stepcaptureoptions(step);
5446
+ const format = options.format ?? "png";
5447
+ const shot = await tabshot(format, options.quality);
5448
+ const measured = await bridgecall(tabid2, "measurepage");
5449
+ const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, annotate: options.annotate ?? true }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl: shot, at: Date.now(), id: randomid(), name: `${phase}-${await capturenamefor(step, plan, "state", format)}` });
5450
+ await memory.addcapture(record2);
5451
+ return record2;
5452
+ }
5453
+ async function storecapture(record2, session, plan, step, origin) {
5454
+ await memory.addcapture(record2);
5455
+ const routed = await routecapture(record2, session, plan, step, origin);
5456
+ await memory.setprogress(recordcapture(await memory.getprogress(), plan.id, step.id, record2, Date.now()));
5457
+ await refreshbadge();
5458
+ return { record: record2, routed };
5459
+ }
5460
+ async function executecapturestep(step, session, plan, tabid2, origin) {
5461
+ const options = stepcaptureoptions(step);
5462
+ const format = options.format ?? "png";
5463
+ const ratio = options.pixelratio ?? 1;
5464
+ const runpolicy = await runcapturepolicy();
5465
+ const annotate = options.annotate ?? runpolicy === "annotated";
5466
+ const stepnumber = plan.steps.findIndex((candidate) => candidate.id === step.id) + 1;
5467
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
5468
+ const pageurl = await chrome.tabs.get(tabid2).then((tab) => tab.url ?? origin).catch(() => origin);
5469
+ if (step.kind === "shotview") {
5470
+ const measured = await bridgecall(tabid2, "measurepage");
5471
+ const raw = await tabshot(format, options.quality);
5472
+ const marker = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: measured.viewportwidth, height: measured.viewportheight, url: pageurl, at: Date.now() }) : void 0;
5473
+ const dataurl = format === "png" && ratio === 1 && !marker ? raw : await encodecanvas(measured.viewportwidth * ratio, measured.viewportheight * ratio, async (context) => {
5474
+ const bitmap = await createImageBitmap(await (await fetch(raw)).blob());
5475
+ context.drawImage(bitmap, 0, 0, context.canvas.width, context.canvas.height);
5476
+ }, { format, quality: options.quality, annotate: marker, annotateratio: ratio });
5477
+ const record2 = capturevisible({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, viewport: { width: measured.viewportwidth, height: measured.viewportheight }, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotview", format) });
5478
+ const stored = await storecapture(record2, session, plan, step, origin);
5479
+ await audit("capture", `Captured the visible viewport at ${record2.width} by ${record2.height} pixels in ${format} for step ${step.id}, routed to ${stored.routed.destination}.`, extra);
5480
+ return { ok: true, summary: `Captured the visible viewport at ${record2.width} by ${record2.height} pixels.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target } } };
5481
+ }
5482
+ if (step.kind === "shotfullpage") {
5483
+ const rawoptions = stepoptions2(step);
5484
+ const settle2 = typeof rawoptions.settle === "number" ? rawoptions.settle : 150;
5485
+ const overlap = typeof rawoptions.overlap === "number" ? rawoptions.overlap : 0;
5486
+ const wait = typeof rawoptions.wait === "number" ? rawoptions.wait : void 0;
5487
+ const state = await bridgecall(tabid2, "preparecapture");
5488
+ try {
5489
+ const measured = await bridgecall(tabid2, "measurepage");
5490
+ const stitch = buildstitchplan({ scrollwidth: measured.scrollwidth, scrollheight: measured.scrollheight, viewportwidth: measured.viewportwidth, viewportheight: measured.viewportheight, overlap });
5491
+ if (wait !== void 0) {
5492
+ const budget = stitchbudgetallowed(stitch.tiles.length, settle2, wait);
5493
+ if (!budget.allowed) throw new Error(budget.reason ?? "The stitching scroll budget exceeds the reviewed wait window.");
5494
+ }
5495
+ const tiles = [];
5496
+ let done = 0;
5497
+ for (let index = 0; index < stitch.tiles.length; index += 1) {
5498
+ const tile = stitch.tiles[index];
5499
+ if (!tile) continue;
5500
+ await bridgecall(tabid2, "scrollcapture", tile.x, tile.y);
5501
+ await bridgecall(tabid2, "waitsettle", settle2);
5502
+ const shot = await tabshot("png", void 0);
5503
+ tiles.push(await decodeTile(shot, tile.x, tile.y, index % stitch.rows === 0));
5504
+ done += 1;
5505
+ stitchprogress.set(step.id, { stepid: step.id, done, total: stitch.tiles.length });
5506
+ }
5507
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: stitch.scrollwidth, height: stitch.scrollheight, url: pageurl, at: Date.now() }) : void 0;
5508
+ const dataurl = await composestitched(tiles, stitch.scrollwidth, stitch.scrollheight, overlap, ratio, { format, quality: options.quality, annotate: annotation });
5509
+ const record2 = capturestitched({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, plan: stitch, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotfullpage", format) });
5510
+ const stored = await storecapture(record2, session, plan, step, origin);
5511
+ await audit("capture", `Captured the full page of ${stitch.scrollwidth} by ${stitch.scrollheight} css pixels from ${stitch.tiles.length} stitched tile${stitch.tiles.length === 1 ? "" : "s"} with ${overlap} overlap row${overlap === 1 ? "" : "s"}, routed to ${stored.routed.destination}.`, extra);
5512
+ return { ok: true, summary: `Captured the full page from ${stitch.tiles.length} stitched tiles.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, tiles: stitch.tiles.length, columns: stitch.columns, rows: stitch.rows, overlap } };
5513
+ } finally {
5514
+ stitchprogress.delete(step.id);
5515
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
5516
+ }
5517
+ }
5518
+ if (step.kind === "shotelement") {
5519
+ const selector = step.target ?? "";
5520
+ const settle2 = typeof stepoptions2(step).settle === "number" ? stepoptions2(step).settle : 150;
5521
+ const measured = await bridgecall(tabid2, "measurepage");
5522
+ const targetinfo = await bridgecall(tabid2, "elementrect", selector);
5523
+ if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
5524
+ const rect = targetinfo.rect;
5525
+ const crossing = targetinfo.crossesviewport === true;
5526
+ const state = await bridgecall(tabid2, "preparecapture");
5527
+ try {
5528
+ let dataurl = "";
5529
+ if (!crossing) {
5530
+ const scrollto = Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2));
5531
+ await bridgecall(tabid2, "scrollcapture", 0, scrollto);
5532
+ await bridgecall(tabid2, "waitsettle", settle2);
5533
+ const shot = await tabshot("png", void 0);
5534
+ const crop = croprect({ x: rect.x, y: rect.y - scrollto, width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
5535
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
5536
+ dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
5537
+ } else {
5538
+ const tops = regionsteps(rect.height, measured.viewportheight);
5539
+ const slices = [];
5540
+ for (let index = 0; index < tops.length; index += 1) {
5541
+ const top = tops[index] ?? 0;
5542
+ await bridgecall(tabid2, "scrollcapture", 0, rect.y + top);
5543
+ await bridgecall(tabid2, "waitsettle", settle2);
5544
+ const shot = await tabshot("png", void 0);
5545
+ const crop = croprect({ x: rect.x, y: 0, width: rect.width, height: Math.min(measured.viewportheight, rect.height - top) }, { width: measured.viewportwidth, height: measured.viewportheight });
5546
+ const slicedata = await croptoRect(shot, crop, 1, { format: "png" });
5547
+ slices.push(await decodeTile(slicedata, 0, top, index === 0));
5548
+ }
5549
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: Math.min(rect.width, measured.viewportwidth), height: rect.height, rect: { x: 0, y: 0, width: Math.min(rect.width, measured.viewportwidth), height: rect.height }, url: pageurl, at: Date.now() }) : void 0;
5550
+ dataurl = await composestitched(slices, Math.min(rect.width, measured.viewportwidth), rect.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
5551
+ }
5552
+ const record2 = captureelement({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotelement", format), target: selector });
5553
+ const stored = await storecapture(record2, session, plan, step, origin);
5554
+ await audit("capture", `Captured the element ${selector} at ${rect.width} by ${rect.height} css pixels${crossing ? " through the tiled fallback" : ""}, routed to ${stored.routed.destination}.`, extra);
5555
+ return { ok: true, summary: `Captured the element ${selector}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect, tiledfallback: crossing, target: selector } };
5556
+ } finally {
5557
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
5558
+ }
5559
+ }
5560
+ if (step.kind === "shotregion") {
5561
+ const rawoptions = stepoptions2(step);
5562
+ const rect = rawoptions.regionrect;
5563
+ if (!rect) throw new Error("A reviewed regionrect is required in options.");
5564
+ const container = typeof rawoptions.container === "string" ? rawoptions.container : void 0;
5565
+ const measured = await bridgecall(tabid2, "measurepage");
5566
+ const state = await bridgecall(tabid2, "preparecapture");
5567
+ try {
5568
+ let dataurl = "";
5569
+ let captured = rect;
5570
+ if (container) {
5571
+ const info = await bridgecall(tabid2, "scrollcontainercapture", container, 0);
5572
+ if (!info.ok || info.height === void 0 || info.viewportheight === void 0) throw new Error(info.summary);
5573
+ const steps = regionsteps(info.height, info.viewportheight);
5574
+ const strips = [];
5575
+ for (let index = 0; index < steps.length; index += 1) {
5576
+ await bridgecall(tabid2, "scrollcontainercapture", container, steps[index] ?? 0);
5577
+ await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
5578
+ const shot = await tabshot("png", void 0);
5579
+ strips.push(await decodeTile(shot, 0, steps[index] ?? 0, index === 0));
5580
+ }
5581
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: rect.width, height: info.height, rect, url: pageurl, at: Date.now() }) : void 0;
5582
+ dataurl = await composestitched(strips, rect.width, info.height, 0, ratio, { format, quality: options.quality, annotate: annotation });
5583
+ captured = { x: rect.x, y: rect.y, width: rect.width, height: info.height };
5584
+ } else {
5585
+ await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)));
5586
+ await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
5587
+ const shot = await tabshot("png", void 0);
5588
+ const crop = croprect({ x: rect.x, y: rect.y - Math.max(0, rect.y - Math.floor((measured.viewportheight - rect.height) / 2)), width: rect.width, height: rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
5589
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width: crop.width, height: crop.height, rect: { x: 0, y: 0, width: crop.width, height: crop.height }, url: pageurl, at: Date.now() }) : void 0;
5590
+ dataurl = await croptoRect(shot, crop, ratio, { format, quality: options.quality, annotate: annotation });
5591
+ captured = crop;
5592
+ }
5593
+ const record2 = captureregion({ runid: plan.id, stepid: step.id, options: { ...options, ...annotate ? { annotate: true } : {} }, rect: captured, dataurl, at: Date.now(), id: randomid(), name: await capturenamefor(step, plan, "shotregion", format), ...container ? { target: container } : {} });
5594
+ const stored = await storecapture(record2, session, plan, step, origin);
5595
+ await audit("capture", `Captured the reviewed region at ${captured.width} by ${captured.height} css pixels${container ? ` of the scrollable container ${container}` : ""}, routed to ${stored.routed.destination}.`, extra);
5596
+ return { ok: true, summary: `Captured the reviewed region${container ? ` of container ${container}` : ""}.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, rect: captured, ...container ? { container } : {} } };
5597
+ } finally {
5598
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
5599
+ }
5600
+ }
5601
+ if (step.kind === "contactsheet") {
5602
+ const rawoptions = stepoptions2(step);
5603
+ const elements = (Array.isArray(rawoptions.elements) ? rawoptions.elements : []).filter((item) => typeof item === "string" && item.trim().length > 0);
5604
+ const layout = rawoptions.sheet && typeof rawoptions.sheet === "object" && !Array.isArray(rawoptions.sheet) ? rawoptions.sheet : { cellsize: 240, columns: 3, label: "both" };
5605
+ const measured = await bridgecall(tabid2, "measurepage");
5606
+ const state = await bridgecall(tabid2, "preparecapture");
5607
+ try {
5608
+ const cells = [];
5609
+ for (const selector of elements) {
5610
+ const targetinfo = await bridgecall(tabid2, "elementrect", selector);
5611
+ if (!targetinfo.ok || !targetinfo.rect) throw new Error(targetinfo.summary);
5612
+ await bridgecall(tabid2, "scrollcapture", 0, Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)));
5613
+ await bridgecall(tabid2, "waitsettle", typeof rawoptions.settle === "number" ? rawoptions.settle : 150);
5614
+ const shot = await tabshot("png", void 0);
5615
+ const crop = croprect({ x: 0, y: targetinfo.rect.y - Math.max(0, targetinfo.rect.y - Math.floor((measured.viewportheight - targetinfo.rect.height) / 2)), width: targetinfo.rect.width, height: targetinfo.rect.height }, { width: measured.viewportwidth, height: measured.viewportheight });
5616
+ const celldata = await croptoRect(shot, crop, 1, { format: "png" });
5617
+ cells.push({ selector, dataurl: celldata, width: crop.width, height: crop.height });
5618
+ }
5619
+ const placed = buildsheet(cells.map((cell) => ({ selector: cell.selector })), layout);
5620
+ const width = placed.columns * layout.cellsize;
5621
+ const rows = placed.rows;
5622
+ const height = rows * (layout.cellsize + 28);
5623
+ const annotation = annotate ? annotationplanof({ step: Math.max(1, stepnumber), width, height, url: pageurl, at: Date.now() }) : void 0;
5624
+ const dataurl = await encodecanvas(width * ratio, height * ratio, async (context) => {
5625
+ context.scale(ratio, ratio);
5626
+ for (let index = 0; index < cells.length; index += 1) {
5627
+ const cell = cells[index];
5628
+ const place = placed.cells[index];
5629
+ if (!cell || !place) continue;
5630
+ const bitmap = await createImageBitmap(await (await fetch(cell.dataurl)).blob());
5631
+ const scaled = Math.min(layout.cellsize / Math.max(1, bitmap.width), layout.cellsize / Math.max(1, bitmap.height));
5632
+ const drawwidth = Math.max(1, Math.round(bitmap.width * scaled));
5633
+ const drawheight = Math.max(1, Math.round(bitmap.height * scaled));
5634
+ context.drawImage(bitmap, place.column * layout.cellsize + Math.floor((layout.cellsize - drawwidth) / 2), place.row * (layout.cellsize + 28) + Math.floor((layout.cellsize - drawheight) / 2), drawwidth, drawheight);
5635
+ if (place.caption) {
5636
+ context.fillStyle = "rgba(23,32,44,.86)";
5637
+ context.fillRect(place.column * layout.cellsize, place.row * (layout.cellsize + 28) + layout.cellsize, layout.cellsize, 28);
5638
+ context.fillStyle = "#ffffff";
5639
+ context.font = "12px sans-serif";
5640
+ context.textAlign = "left";
5641
+ context.textBaseline = "middle";
5642
+ context.fillText(place.caption.slice(0, Math.floor(layout.cellsize / 7)), place.column * layout.cellsize + 6, place.row * (layout.cellsize + 28) + layout.cellsize + 14);
5643
+ }
5644
+ }
5645
+ }, { format, quality: options.quality, annotate: annotation, annotateratio: ratio });
5646
+ const record2 = { id: randomid(), runid: plan.id, stepid: step.id, kind: "contactsheet", format, width: Math.round(width * ratio), height: Math.round(height * ratio), capturedat: Date.now(), bytes: dataurl, name: await capturenamefor(step, plan, "contactsheet", format), ...annotate ? { annotated: true } : {}, ...options.exporttarget !== void 0 ? { exporttarget: options.exporttarget } : {} };
5647
+ const stored = await storecapture(record2, session, plan, step, origin);
5648
+ await audit("capture", `Tiled ${cells.length} element capture${cells.length === 1 ? "" : "s"} into one labeled ${placed.columns} column contact sheet, routed to ${stored.routed.destination}.`, extra);
5649
+ return { ok: true, summary: `Tiled ${cells.length} element captures into one contact sheet.`, details: { capture: { id: record2.id, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, bytes: (record2.bytes ?? "").length, exporttarget: stored.routed.target }, cells: placed.cells, columns: placed.columns, rows: placed.rows } };
5650
+ } finally {
5651
+ await bridgecall(tabid2, "restorecapture", state).catch(() => void 0);
5652
+ }
5653
+ }
5654
+ throw new Error("Unsupported capture kind.");
5655
+ }
4944
5656
  async function enforcewindowreview(step, session, plan) {
4945
5657
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
4946
5658
  const progress = plan ? await memory.getprogress() : void 0;
@@ -4979,8 +5691,9 @@ async function refreshbadge() {
4979
5691
  const consents = (await memory.getclipconsents()).filter((record2) => record2.approved === void 0).length;
4980
5692
  const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
4981
5693
  const datasets = (await memory.getdatasets()).length;
5694
+ const captures = (await memory.getcaptures()).length;
4982
5695
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
4983
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets;
5696
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures;
4984
5697
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
4985
5698
  });
4986
5699
  }
@@ -4999,46 +5712,69 @@ async function executestep(stepid) {
4999
5712
  }
5000
5713
  let output;
5001
5714
  let watchwindow;
5002
- if (step.kind === "windowclose") {
5003
- await enforcewindowreview(step, session, plan);
5004
- }
5005
- if (istabscommandkind(step.kind)) {
5006
- output = await executetabscommand(step, session, plan, tab.id);
5007
- } else if (isdatasetkind(step.kind)) {
5008
- output = await executedatastep(step, session, plan, tab.id, origin);
5009
- } else if (isfileskind(step.kind)) {
5010
- output = await executefilesstep(step, session, plan, tab.id, origin);
5011
- } else if (isformkind(step.kind)) {
5012
- output = await executeformstep(step, session, plan, tab.id, origin);
5013
- } else if (isbrowserkind(step.kind)) {
5014
- output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
5015
- } else if (step.kind === "keyhold") {
5016
- output = await executekeyhold(step, session, plan, tab.id, origin);
5017
- } else if (step.kind === "keyrelease") {
5018
- output = await executekeyrelease(step, session, plan, tab.id, origin);
5019
- } else if (step.kind === "dismissdialog") {
5020
- output = await executedismissdialog(step, session, plan, tab.id, origin);
5021
- } else if (step.kind === "retryaction") {
5022
- output = await executeretryaction(step, session, plan, tab.id, origin);
5023
- } else if (step.kind === "mapclicks") {
5024
- output = await executemapclicks(step, plan, tab.id, origin);
5025
- } else if (step.kind === "enterframe") {
5026
- output = await executeenterframe(step, plan, tab.id, origin);
5027
- } else if (watchstepkinds.has(step.kind)) {
5028
- if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
5029
- const watched = await executewatchstep(step, session, plan, tab.id, origin);
5030
- output = watched.output;
5031
- watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
5032
- } else if (step.kind === "diffsnapshots") {
5033
- output = await executediffsnapshots(step, session, plan, tab.id, origin);
5034
- } else if (navigationstepkinds.has(step.kind)) {
5035
- output = await executenavigationkind(step, session, plan, tab.id, origin);
5036
- } else {
5037
- if (step.target && freshcheckkinds.has(step.kind)) {
5038
- const fresh = await snapshot(tab.id);
5039
- if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
5715
+ const dispatchreviewedstep = async () => {
5716
+ if (step.kind === "windowclose") {
5717
+ await enforcewindowreview(step, session, plan);
5718
+ }
5719
+ if (istabscommandkind(step.kind)) {
5720
+ output = await executetabscommand(step, session, plan, tab.id);
5721
+ } else if (isdatasetkind(step.kind)) {
5722
+ output = await executedatastep(step, session, plan, tab.id, origin);
5723
+ } else if (isfileskind(step.kind)) {
5724
+ output = await executefilesstep(step, session, plan, tab.id, origin);
5725
+ } else if (isformkind(step.kind)) {
5726
+ output = await executeformstep(step, session, plan, tab.id, origin);
5727
+ } else if (isbrowserkind(step.kind)) {
5728
+ output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
5729
+ } else if (step.kind === "keyhold") {
5730
+ output = await executekeyhold(step, session, plan, tab.id, origin);
5731
+ } else if (step.kind === "keyrelease") {
5732
+ output = await executekeyrelease(step, session, plan, tab.id, origin);
5733
+ } else if (step.kind === "dismissdialog") {
5734
+ output = await executedismissdialog(step, session, plan, tab.id, origin);
5735
+ } else if (step.kind === "retryaction") {
5736
+ output = await executeretryaction(step, session, plan, tab.id, origin);
5737
+ } else if (step.kind === "mapclicks") {
5738
+ output = await executemapclicks(step, plan, tab.id, origin);
5739
+ } else if (step.kind === "enterframe") {
5740
+ output = await executeenterframe(step, plan, tab.id, origin);
5741
+ } else if (watchstepkinds.has(step.kind)) {
5742
+ if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
5743
+ const watched = await executewatchstep(step, session, plan, tab.id, origin);
5744
+ output = watched.output;
5745
+ watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
5746
+ } else if (step.kind === "diffsnapshots") {
5747
+ output = await executediffsnapshots(step, session, plan, tab.id, origin);
5748
+ } else if (navigationstepkinds.has(step.kind)) {
5749
+ output = await executenavigationkind(step, session, plan, tab.id, origin);
5750
+ } else if (iscapturekind(step.kind)) {
5751
+ output = await executecapturestep(step, session, plan, tab.id, origin);
5752
+ } else {
5753
+ if (step.target && freshcheckkinds.has(step.kind)) {
5754
+ const fresh = await snapshot(tab.id);
5755
+ if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
5756
+ }
5757
+ output = await dispatchpagestep(step, tab.id, origin, plan);
5040
5758
  }
5041
- output = await dispatchpagestep(step, tab.id, origin, plan);
5759
+ return output;
5760
+ };
5761
+ const runplan = plan;
5762
+ const capturepolicystate = await runcapturepolicy();
5763
+ if (capturepolicystate === "beforeafter" && session && step.risk !== "read" && beforeafterwrapallowed(step.kind)) {
5764
+ const before = await grabstateshot(step, session, runplan, tab.id, "before");
5765
+ output = await dispatchreviewedstep();
5766
+ if (output?.ok) {
5767
+ const after = await grabstateshot(step, session, runplan, tab.id, "after");
5768
+ const domversion = await memory.getobservationversion();
5769
+ const paired = capturestates({ policy: "beforeafter", before, after, actionkind: step.kind, ...step.target ? { target: step.target } : {}, ...domversion !== void 0 ? { domsnapshotid: String(domversion) } : {}, at: Date.now(), id: randomid() });
5770
+ if (paired.pair) {
5771
+ await memory.addpair(paired.pair);
5772
+ await memory.setprogress(recordpair(await memory.getprogress(), runplan.id, step.id, paired.pair, Date.now()));
5773
+ await audit("capture", `Paired the before shot ${paired.pair.beforeid} with the after shot ${paired.pair.afterid} around the ${step.kind} action${paired.pair.domsnapshotid !== void 0 ? ` with dom snapshot ${paired.pair.domsnapshotid}` : ""}.`, { sessionid: session.id, planid: runplan.id, stepid: step.id });
5774
+ }
5775
+ }
5776
+ } else {
5777
+ output = await dispatchreviewedstep();
5042
5778
  }
5043
5779
  if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
5044
5780
  await recordevidence(step, output, session, plan, origin);
@@ -5196,6 +5932,13 @@ async function handlerequest(message, sender) {
5196
5932
  const capturecounters = await memory.getcapturecounters();
5197
5933
  const inventory = await memory.getinventory();
5198
5934
  const mimefilters = await memory.getmimefilters();
5935
+ const capturemetadata = (await memory.getcaptures()).map((record2) => {
5936
+ const { bytes, ...meta } = record2;
5937
+ void bytes;
5938
+ return meta;
5939
+ });
5940
+ const capturepairs = await memory.getpairs();
5941
+ const runsettings = await memory.getsettings();
5199
5942
  const scanhooks = [];
5200
5943
  for (const hook of await memory.getscanhooks()) {
5201
5944
  scanhooks.push({ ...hook, granted: await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false) });
@@ -5206,7 +5949,7 @@ async function handlerequest(message, sender) {
5206
5949
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
5207
5950
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
5208
5951
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
5209
- return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks };
5952
+ return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
5210
5953
  }
5211
5954
  case "capabilities":
5212
5955
  return refreshcapabilities();
@@ -5248,7 +5991,8 @@ async function handlerequest(message, sender) {
5248
5991
  const outcome = (await memory.getoutcomes()).find((candidate) => candidate.stepid === (input.stepid ?? ""));
5249
5992
  if (!outcome) throw new Error("No outcome exists for the reviewed step.");
5250
5993
  const resolved = outcome.details?.resolvedtarget;
5251
- return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {} }));
5994
+ const capture = outcome.details?.capture;
5995
+ return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {} }));
5252
5996
  }
5253
5997
  case "map": {
5254
5998
  const plan = await memory.getplan();
@@ -5612,6 +6356,53 @@ async function handlerequest(message, sender) {
5612
6356
  await audit("observation", `The review panel exported ${records.length} netlog record${records.length === 1 ? "" : "s"} of the run with every header value redacted.`, { sessionid: session.id });
5613
6357
  return { records, redacted: true, redaction: "every header value is redacted from exported netlogs" };
5614
6358
  }
6359
+ case "capturebytes": {
6360
+ const inputcapture = message;
6361
+ const record2 = await memory.getcapture(inputcapture.id ?? "");
6362
+ if (!record2) throw new Error("No stored capture matches the requested id.");
6363
+ if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
6364
+ return { id: record2.id, runid: record2.runid, kind: record2.kind, format: record2.format, width: record2.width, height: record2.height, capturedat: record2.capturedat, name: record2.name, annotated: record2.annotated === true, bytes: record2.bytes };
6365
+ }
6366
+ case "capturereport":
6367
+ return capturereport({ records: await memory.getcaptures(), pairs: await memory.getpairs() });
6368
+ case "copycapture": {
6369
+ const inputcopy = message;
6370
+ const session = await memory.getsession();
6371
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture clipboard copies stay behind the consent gate of an active session.");
6372
+ const record2 = await memory.getcapture(inputcopy.id ?? "");
6373
+ if (!record2) throw new Error("No stored capture matches the requested id.");
6374
+ if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
6375
+ const granted = await chrome.permissions.contains({ permissions: ["clipboardWrite"] }).catch(() => false);
6376
+ if (!granted) throw new Error("The capture copy needs the clipboardwrite capability; request it from the review panel.");
6377
+ const blob = await (await fetch(record2.bytes)).blob();
6378
+ await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
6379
+ await memory.addclip(clipentryof("screen", { hash: cliphash(record2.bytes), length: record2.bytes.length }, session.origin, record2.stepid, Date.now()));
6380
+ await audit("capture", `The review panel copied capture ${record2.id} of kind ${record2.kind} to the clipboard with payload hash ${cliphash(record2.bytes)}.`, { sessionid: session.id });
6381
+ return { id: record2.id, copied: true };
6382
+ }
6383
+ case "downloadcapture": {
6384
+ const inputdownloadcapture = message;
6385
+ const session = await memory.getsession();
6386
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Capture downloads stay behind the consent gate of an active session.");
6387
+ const record2 = await memory.getcapture(inputdownloadcapture.id ?? "");
6388
+ if (!record2) throw new Error("No stored capture matches the requested id.");
6389
+ if (!record2.bytes) throw new Error("The capture bytes expired from the retention window; the metadata stays for the audit trail.");
6390
+ const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
6391
+ if (!granted) throw new Error("The capture download needs the downloads capability; request it from the review panel.");
6392
+ await chrome.downloads.download({ url: record2.bytes, filename: record2.name ?? `capture.${record2.format}` });
6393
+ await audit("capture", `The review panel downloaded capture ${record2.id} of kind ${record2.kind} through the reviewed download flow.`, { sessionid: session.id });
6394
+ return { id: record2.id, downloaded: true };
6395
+ }
6396
+ case "setcapturepolicy": {
6397
+ const inputpolicy = message;
6398
+ const mode = inputpolicy.mode === "off" || inputpolicy.mode === "manual" || inputpolicy.mode === "annotated" || inputpolicy.mode === "beforeafter" ? inputpolicy.mode : void 0;
6399
+ if (!mode) throw new Error("The capture policy must be off, manual, annotated or beforeafter.");
6400
+ const settings = await memory.getsettings();
6401
+ await memory.setsettings({ ...settings, capturepolicy: mode });
6402
+ await audit("configure", `The user set the capture policy of the run to ${mode}; beforeafter wraps page moving actions with state pairs.`);
6403
+ await refreshbadge();
6404
+ return { capturepolicy: mode };
6405
+ }
5615
6406
  case "stop": {
5616
6407
  const session = await memory.getsession();
5617
6408
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });