@wenathlan/extension 1.1.35 → 1.1.37

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.
@@ -352,19 +352,192 @@ var sessionmemory = class {
352
352
  async setnavstate(tabid2, state) {
353
353
  return this.adapter.set(`navstate${tabid2}`, state);
354
354
  }
355
+ /** Stores one named tab layout with its window bounds and group states, replacing the previous layout of that name. */
356
+ async setlayout(layout) {
357
+ const records = (await this.getlayouts()).filter((item) => item.name !== layout.name);
358
+ await this.adapter.set("layouts", [layout, ...records]);
359
+ }
360
+ /** Returns one saved tab layout by name with its timestamp. */
361
+ async getlayout(name) {
362
+ return (await this.getlayouts()).find((item) => item.name === name);
363
+ }
364
+ /** Returns every saved tab layout with its window bounds and group states. */
365
+ async getlayouts() {
366
+ return await this.adapter.get("layouts") ?? [];
367
+ }
368
+ /** Stores one tab group definition with its color choice and member tabs, replacing the previous definition of that name. */
369
+ async settabgroup(group) {
370
+ const records = (await this.gettabgroups()).filter((item) => item.name !== group.name);
371
+ await this.adapter.set("tabgroups", [...records, group]);
372
+ }
373
+ /** Returns every stored tab group definition with its color choice, newest first. */
374
+ async gettabgroups() {
375
+ return await this.adapter.get("tabgroups") ?? [];
376
+ }
377
+ /** Records one tabmeta record with task provenance, replacing the previous metadata of that tab. */
378
+ async settabmeta(meta) {
379
+ const records = (await this.gettabmetas()).filter((item) => item.tabid !== meta.tabid);
380
+ await this.adapter.set("tabmetas", [...records, meta]);
381
+ }
382
+ /** Returns every stored tabmeta record with task provenance. */
383
+ async gettabmetas() {
384
+ return await this.adapter.get("tabmetas") ?? [];
385
+ }
386
+ /** Records one session snapshot of tabs and windows for later restore. */
387
+ async addsnapshot(snapshot2) {
388
+ const records = await this.getsnapshots();
389
+ await this.adapter.set("snapshots", [snapshot2, ...records]);
390
+ }
391
+ /** Returns every stored session snapshot, newest first. */
392
+ async getsnapshots() {
393
+ return await this.adapter.get("snapshots") ?? [];
394
+ }
395
+ /** Records one closed tab in the history kept for restoretab and reopenrun. */
396
+ async addclosedtab(tab) {
397
+ const records = await this.getclosedtabs();
398
+ await this.adapter.set("closedtabs", [tab, ...records]);
399
+ }
400
+ /** Returns the closed tab history, newest first. */
401
+ async getclosedtabs() {
402
+ return await this.adapter.get("closedtabs") ?? [];
403
+ }
404
+ /** Stores one badge state per task, replacing the previous badge of that task. */
405
+ async setbadge(badge) {
406
+ const records = (await this.getbadges()).filter((item) => item.taskid !== badge.taskid);
407
+ await this.adapter.set("badges", [...records, badge]);
408
+ }
409
+ /** Returns every stored badge state per task. */
410
+ async getbadges() {
411
+ return await this.adapter.get("badges") ?? [];
412
+ }
413
+ /** Records one tab event observed inside a reviewed watchtab registration. */
414
+ async addtabwatchevent(event) {
415
+ const records = await this.gettabwatchevents();
416
+ await this.adapter.set("tabwatchevents", [event, ...records]);
417
+ }
418
+ /** Returns the tab event stream of every reviewed watchtab registration, newest first. */
419
+ async gettabwatchevents() {
420
+ return await this.adapter.get("tabwatchevents") ?? [];
421
+ }
422
+ /** Returns the ids of the scratch windows opened for split work. */
423
+ async getscratchwindows() {
424
+ return await this.adapter.get("scratchwindows") ?? [];
425
+ }
426
+ /** Replaces the scratch window id list after one scratch window opens or closes. */
427
+ async setscratchwindows(ids) {
428
+ return this.adapter.set("scratchwindows", ids);
429
+ }
430
+ /** Returns the pinned control tab state with the live task feed. */
431
+ async getcontroltab() {
432
+ return this.adapter.get("controltab");
433
+ }
434
+ /** Replaces the pinned control tab state. */
435
+ async setcontroltab(state) {
436
+ return this.adapter.set("controltab", state);
437
+ }
438
+ /** Stores one saved form profile under its reviewed name, replacing the previous profile of that name. */
439
+ async setprofile(profile) {
440
+ const records = (await this.getprofiles()).filter((item) => item.name !== profile.name);
441
+ await this.adapter.set("formprofiles", [profile, ...records]);
442
+ }
443
+ /** Returns one saved form profile by its reviewed name. */
444
+ async getprofile(name) {
445
+ return (await this.getprofiles()).find((item) => item.name === name);
446
+ }
447
+ /** Returns every saved form profile with its origin grants, newest first. */
448
+ async getprofiles() {
449
+ return await this.adapter.get("formprofiles") ?? [];
450
+ }
451
+ /** Removes one saved form profile by its reviewed name. */
452
+ async removeprofile(name) {
453
+ const records = (await this.getprofiles()).filter((item) => item.name !== name);
454
+ await this.adapter.set("formprofiles", records);
455
+ }
456
+ /** Records one wizard state with its step history. */
457
+ async addwizard(state) {
458
+ const records = await this.getwizards();
459
+ await this.adapter.set("wizards", [state, ...records]);
460
+ }
461
+ /** Returns every stored wizard state with its step history, newest first. */
462
+ async getwizards() {
463
+ return await this.adapter.get("wizards") ?? [];
464
+ }
465
+ /** Stores one submission ticket with its values hash, replacing the previous ticket of that id. */
466
+ async setticket(ticket) {
467
+ const records = (await this.gettickets()).filter((item) => item.id !== ticket.id);
468
+ await this.adapter.set("submittickets", [ticket, ...records]);
469
+ }
470
+ /** Returns every stored submission ticket with its values hash, newest first. */
471
+ async gettickets() {
472
+ return await this.adapter.get("submittickets") ?? [];
473
+ }
474
+ /** Records one collected error report for correction loops. */
475
+ async adderrorreport(report) {
476
+ const records = await this.geterrorreports();
477
+ await this.adapter.set("errorreports", [report, ...records]);
478
+ }
479
+ /** Returns every stored error report, newest first. */
480
+ async geterrorreports() {
481
+ return await this.adapter.get("errorreports") ?? [];
482
+ }
483
+ /** Records one typeahead pick observed when a reviewed suggestion entry was chosen. */
484
+ async addpick(pick) {
485
+ const records = await this.getpicks();
486
+ await this.adapter.set("typeaheadpicks", [pick, ...records]);
487
+ }
488
+ /** Returns every recorded typeahead pick, newest first. */
489
+ async getpicks() {
490
+ return await this.adapter.get("typeaheadpicks") ?? [];
491
+ }
492
+ /** Records one captcha handoff while the plan waits for the user. */
493
+ async addcaptcha(handoff) {
494
+ const records = await this.getcaptchas();
495
+ await this.adapter.set("captchas", [handoff, ...records]);
496
+ }
497
+ /** Returns every captcha handoff record with its resolution state, newest first. */
498
+ async getcaptchas() {
499
+ return await this.adapter.get("captchas") ?? [];
500
+ }
501
+ /** Resolves one captcha handoff by id once the user finished it. */
502
+ async resolvecaptcha(id, resolvedat) {
503
+ const records = await this.getcaptchas();
504
+ await this.adapter.set("captchas", records.map((handoff) => handoff.id === id && !handoff.resolved ? { ...handoff, resolved: true, resolvedat } : handoff));
505
+ }
506
+ /** Records one login or template detection for its origin. */
507
+ async adddetection(record2) {
508
+ const records = await this.getdetections();
509
+ await this.adapter.set("detections", [record2, ...records]);
510
+ }
511
+ /** Returns every stored login and template detection per origin, newest first. */
512
+ async getdetections() {
513
+ return await this.adapter.get("detections") ?? [];
514
+ }
515
+ /** Stores the reviewed one time code behind the consent gate of an active session. */
516
+ async setcodevalue(value) {
517
+ return this.adapter.set("codevalue", value);
518
+ }
519
+ /** Returns the reviewed one time code, if the user stored one behind the consent gate. */
520
+ async getcodevalue() {
521
+ return this.adapter.get("codevalue");
522
+ }
355
523
  };
356
524
  function randomid() {
357
525
  return crypto.randomUUID();
358
526
  }
359
527
 
360
528
  // policy.ts
361
- 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"]);
529
+ 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"]);
362
530
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
363
- 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"]);
531
+ 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"]);
364
532
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
365
- var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
366
- 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"]);
367
- 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"]);
533
+ var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
534
+ 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"]);
535
+ 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"]);
536
+ 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"]);
537
+ 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"]);
538
+ var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
539
+ var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
540
+ var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
368
541
  function normalizeendpoint(value) {
369
542
  const endpoint = new URL(value.trim());
370
543
  if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
@@ -397,8 +570,168 @@ function requiredcapability(kind) {
397
570
  if (kind === "downloadfile") return "downloads";
398
571
  if (kind === "openclipboard") return "clipboardRead";
399
572
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
573
+ if (tabscommandactions.has(kind)) return "tabs";
400
574
  return void 0;
401
575
  }
576
+ function istabscommandkind(kind) {
577
+ return tabscommandactions.has(kind);
578
+ }
579
+ function islayoutkind(kind) {
580
+ return layoutmutationactions.has(kind);
581
+ }
582
+ function isformkind(kind) {
583
+ return formactions.has(kind);
584
+ }
585
+ function validatefieldmatch(value) {
586
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed field match is required in options." };
587
+ const match = value;
588
+ if (match.mode !== "label" && match.mode !== "placeholder" && match.mode !== "arialabel" && match.mode !== "name") return { allowed: false, reason: "The reviewed field match mode must be label, placeholder, arialabel or name." };
589
+ const key = match.mode === "label" ? "label" : match.mode === "placeholder" ? "placeholder" : match.mode === "arialabel" ? "arialabel" : "name";
590
+ if (!isnonempty(match[key])) return { allowed: false, reason: `The reviewed ${match.mode} field match needs a non-empty ${key}.` };
591
+ return { allowed: true };
592
+ }
593
+ function validateformrecord(value) {
594
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed form record with entries is required in options." };
595
+ const record2 = value;
596
+ if (record2.form !== void 0 && !isnonempty(record2.form)) return { allowed: false, reason: "The reviewed form record form selector must be a non-empty string." };
597
+ if (!Array.isArray(record2.entries) || record2.entries.length === 0) return { allowed: false, reason: "The reviewed form record needs a non-empty list of entries." };
598
+ for (const item of record2.entries) {
599
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed form record entry must be an object." };
600
+ const entry = item;
601
+ const matchcheck = validatefieldmatch(entry.match);
602
+ if (!matchcheck.allowed) return matchcheck;
603
+ if (typeof entry.kind !== "string" || !fieldkinds.includes(entry.kind)) return { allowed: false, reason: "Every reviewed form record entry needs a known field kind." };
604
+ if (typeof entry.value !== "string") return { allowed: false, reason: "Every reviewed form record entry needs a string value." };
605
+ if (entry.kind === "password") return { allowed: false, reason: "Password entries are refused inside form records; use consentpassword with a reviewed consent ref." };
606
+ }
607
+ return { allowed: true };
608
+ }
609
+ function validatevaluegen(value) {
610
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed valuegen rule with a field kind is required in options." };
611
+ const rule = value;
612
+ if (typeof rule.kind !== "string" || !fieldkinds.includes(rule.kind)) return { allowed: false, reason: "The reviewed valuegen kind must be a known field kind." };
613
+ if (rule.locale !== void 0 && !isnonempty(rule.locale)) return { allowed: false, reason: "The reviewed valuegen locale must be a non-empty string." };
614
+ if (rule.seed !== void 0 && (typeof rule.seed !== "number" || !Number.isFinite(rule.seed))) return { allowed: false, reason: "The reviewed valuegen seed must be a finite number." };
615
+ return { allowed: true };
616
+ }
617
+ function validatefieldpairs(options, mode) {
618
+ const pairs = options.fields;
619
+ if (!Array.isArray(pairs) || pairs.length === 0) return { allowed: false, reason: "A reviewed non-empty list of field pairs is required in options." };
620
+ for (const item of pairs) {
621
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed field pair must be an object." };
622
+ const pair = item;
623
+ if (!isnonempty(pair[mode])) return { allowed: false, reason: `Every reviewed field pair needs a non-empty ${mode}.` };
624
+ if (typeof pair.value !== "string" || !pair.value.trim()) return { allowed: false, reason: "Every reviewed field pair needs a non-empty value." };
625
+ }
626
+ return { allowed: true };
627
+ }
628
+ function validatecardsegments(value) {
629
+ if (!Array.isArray(value) || value.length === 0) return { allowed: false, reason: "A reviewed non-empty list of card segments is required in options." };
630
+ for (const item of value) {
631
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { allowed: false, reason: "Every reviewed card segment must be an object." };
632
+ const segment = item;
633
+ const matchcheck = validatefieldmatch(segment.match);
634
+ if (!matchcheck.allowed) return matchcheck;
635
+ if (typeof segment.value !== "string" || !segment.value.trim()) return { allowed: false, reason: "Every reviewed card segment needs a non-empty value." };
636
+ }
637
+ return { allowed: true };
638
+ }
639
+ function validateformgrammar(step, options) {
640
+ const kind = step.kind;
641
+ if (kind === "fillform" || kind === "saveprofiles" && options.formrecord !== void 0) {
642
+ const recordcheck = validateformrecord(options.formrecord);
643
+ if (!recordcheck.allowed) return recordcheck;
644
+ }
645
+ if (kind === "filllabel" || kind === "fillplaceholder") {
646
+ const paircheck = validatefieldpairs(options, kind === "filllabel" ? "label" : "placeholder");
647
+ if (!paircheck.allowed) return paircheck;
648
+ }
649
+ if (kind === "generatevalues" && options.valuegen !== void 0) {
650
+ const rulecheck = validatevaluegen(options.valuegen);
651
+ if (!rulecheck.allowed) return rulecheck;
652
+ }
653
+ if (kind === "saveprofiles" && !isnonempty(options.name)) return { allowed: false, reason: "A reviewed profile name is required in options." };
654
+ if (kind === "submitform" && !isnonempty(options.consentref)) return { allowed: false, reason: "A reviewed consent ref of an approved asksubmit ticket is required in options." };
655
+ if (kind === "retryform") {
656
+ const backoff = options.backoff;
657
+ if (!backoff || typeof backoff !== "object" || Array.isArray(backoff)) return { allowed: false, reason: "A reviewed backoff rule with wait and factor is required in options." };
658
+ const rule = backoff;
659
+ if (typeof rule.wait !== "number" || !Number.isFinite(rule.wait) || rule.wait <= 0) return { allowed: false, reason: "The reviewed retry backoff wait must be a positive number of milliseconds with no code ceiling." };
660
+ if (typeof rule.factor !== "number" || !Number.isFinite(rule.factor) || rule.factor < 1) return { allowed: false, reason: "The reviewed retry backoff factor must be one or greater with no code ceiling." };
661
+ if (options.attempts !== void 0 && (typeof options.attempts !== "number" || !Number.isInteger(options.attempts) || options.attempts < 1)) return { allowed: false, reason: "The reviewed retry attempts must be a positive integer with no code ceiling." };
662
+ }
663
+ if (kind === "runwizard" && options.steps !== void 0 && (typeof options.steps !== "number" || !Number.isInteger(options.steps) || options.steps < 1)) return { allowed: false, reason: "The reviewed wizard step count must be a positive integer with no code ceiling." };
664
+ if (kind === "selectchain") {
665
+ if (!isnonempty(options.child)) return { allowed: false, reason: "A reviewed child selector of the dependent control is required in options." };
666
+ if (!nonnegativeoption(options, "wait")) return { allowed: false, reason: "The reviewed dependent wait must be zero or a positive number of milliseconds." };
667
+ }
668
+ if (kind === "picktypeahead") {
669
+ if (!isnonempty(options.pick)) return { allowed: false, reason: "A reviewed suggestion entry to pick is required in options." };
670
+ if (!nonnegativeoption(options, "timeout")) return { allowed: false, reason: "The reviewed typeahead timeout must be zero or a positive number of milliseconds." };
671
+ }
672
+ if (kind === "pickdate" && !/^\d{4}-\d{2}-\d{2}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed date must use the yyyy-mm-dd form." };
673
+ if (kind === "fillcard") {
674
+ const segmentcheck = validatecardsegments(options.segments);
675
+ if (!segmentcheck.allowed) return segmentcheck;
676
+ if (!nonnegativeoption(options, "pause")) return { allowed: false, reason: "The reviewed card typing pause must be zero or a positive number of milliseconds." };
677
+ }
678
+ if (kind === "fillcode" && !isnonempty(options.source)) return { allowed: false, reason: "A reviewed one time code source is required in options." };
679
+ if (kind === "consentpassword" && !isnonempty(options.consentref)) return { allowed: false, reason: "A reviewed consent ref is required in options before any password is filled." };
680
+ return { allowed: true };
681
+ }
682
+ function submitreviewgranted(steps, submitid) {
683
+ const position = steps.findIndex((candidate) => candidate.id === submitid);
684
+ const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
685
+ return asked ? { allowed: true } : { allowed: false, reason: "Form submission requires an asksubmit review step before it." };
686
+ }
687
+ function passwordconsentgranted(step) {
688
+ let options = {};
689
+ try {
690
+ options = parseoptions(step);
691
+ } catch {
692
+ options = {};
693
+ }
694
+ const consentref = options.consentref;
695
+ if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "A password fill requires a reviewed consent ref in options." };
696
+ return { allowed: true };
697
+ }
698
+ function luhnvalid(digits) {
699
+ let sum = 0;
700
+ let double = false;
701
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
702
+ let value = Number.parseInt(digits[index] ?? "", 10);
703
+ if (!Number.isFinite(value)) return false;
704
+ if (double) {
705
+ value *= 2;
706
+ if (value > 9) value -= 9;
707
+ }
708
+ sum += value;
709
+ double = !double;
710
+ }
711
+ return sum % 10 === 0;
712
+ }
713
+ function generatedvalueallowed(value) {
714
+ const compact = value.replace(/[\s-]/g, "");
715
+ if (/^\d{13,19}$/.test(compact) && luhnvalid(compact) && !compact.startsWith("4111")) return { allowed: false, reason: "The generated value looks like a real card number and is refused; generated card values use the 4111 test prefix." };
716
+ if (/^\d{3}-\d{2}-\d{4}$/.test(value.trim())) return { allowed: false, reason: "The generated value looks like a personal identifier and is refused." };
717
+ return { allowed: true };
718
+ }
719
+ function profilegrantgranted(profile, origin) {
720
+ if (!profile.grants.includes(origin)) return { allowed: false, reason: `The saved profile ${profile.name} is not granted to ${origin}; add the origin to the profile grants first.` };
721
+ return { allowed: true };
722
+ }
723
+ function layoutmutationgranted(session, now) {
724
+ if (!session || session.stoppedat || session.expiresat <= now) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
725
+ return { allowed: true };
726
+ }
727
+ function windowclosegate(tasktabcount, reviewed) {
728
+ if (tasktabcount > 1 && !reviewed) return { allowed: false, reason: `The window holds ${tasktabcount} task tabs and needs explicit review before it closes.` };
729
+ return { allowed: true };
730
+ }
731
+ function tasktabceiling(settings) {
732
+ const ceiling = settings?.tasktabceiling;
733
+ return typeof ceiling === "number" && Number.isFinite(ceiling) && ceiling >= 0 ? ceiling : void 0;
734
+ }
402
735
  function waitduration(step) {
403
736
  const requested = step.value ? Number.parseInt(step.value, 10) : 250;
404
737
  if (!Number.isFinite(requested) || requested < 0) throw new Error("Wait duration must be zero or a positive number of milliseconds.");
@@ -561,6 +894,104 @@ function validateratelimit(value) {
561
894
  if (typeof limit.ceiling !== "number" || !Number.isInteger(limit.ceiling) || limit.ceiling < 1) return { allowed: false, reason: "The reviewed ratelimit ceiling must be a positive integer with no code ceiling." };
562
895
  return { allowed: true };
563
896
  }
897
+ function validatetabquery(value) {
898
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed tabquery with at least one matcher is required in options." };
899
+ const query = value;
900
+ const hasmatcher = query.url !== void 0 || query.title !== void 0 || query.id !== void 0 || query.pattern !== void 0;
901
+ if (!hasmatcher) return { allowed: false, reason: "The reviewed tabquery needs a url, title, id or pattern matcher." };
902
+ if (query.url !== void 0 && !isnonempty(query.url)) return { allowed: false, reason: "The reviewed tabquery url matcher must be a non-empty string." };
903
+ if (query.title !== void 0 && !isnonempty(query.title)) return { allowed: false, reason: "The reviewed tabquery title matcher must be a non-empty string." };
904
+ if (query.pattern !== void 0 && !isnonempty(query.pattern)) return { allowed: false, reason: "The reviewed tabquery pattern matcher must be a non-empty string." };
905
+ if (query.id !== void 0 && (typeof query.id !== "number" || !Number.isInteger(query.id) || query.id < 0)) return { allowed: false, reason: "The reviewed tabquery id matcher must be a non-negative integer tab id." };
906
+ return { allowed: true };
907
+ }
908
+ function validategroupcolor(value) {
909
+ return typeof value === "string" && groupcolors.includes(value);
910
+ }
911
+ function validateidlist(options, key) {
912
+ const ids = options[key];
913
+ return Array.isArray(ids) && ids.length > 0 && ids.every((id) => typeof id === "number" && Number.isInteger(id) && id >= 0);
914
+ }
915
+ function validatetabsgrammar(step, options) {
916
+ const kind = step.kind;
917
+ if (kind === "querytabs" || kind === "closepattern") {
918
+ const querycheck = validatetabquery(options.tabquery);
919
+ if (!querycheck.allowed) return querycheck;
920
+ if (kind === "closepattern" && options.reviewed !== true) return { allowed: false, reason: "The close pattern needs the explicit reviewed flag before any tab closes." };
921
+ }
922
+ if (kind === "duplicatetab" || kind === "pintab" || kind === "mutetab" || kind === "movetab" || kind === "movetabwindow" || kind === "badgetab" || kind === "attachmeta") {
923
+ if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser tab id is required." };
924
+ }
925
+ if (kind === "focuswindow" || kind === "maximizewindow" || kind === "minimizewindow" || kind === "restorewindow") {
926
+ if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser window id is required." };
927
+ }
928
+ if (kind === "pintab" && typeof options.pinned !== "boolean") return { allowed: false, reason: "A reviewed pinned flag is required in options." };
929
+ if (kind === "mutetab" && typeof options.muted !== "boolean") return { allowed: false, reason: "A reviewed muted flag is required in options." };
930
+ if (kind === "movetab") {
931
+ if (typeof options.index !== "number" || !Number.isInteger(options.index) || options.index < 0) return { allowed: false, reason: "A reviewed non-negative target index is required in options." };
932
+ }
933
+ if (kind === "movetabwindow") {
934
+ if (typeof options.windowid !== "number" || !Number.isInteger(options.windowid) || options.windowid < 0) return { allowed: false, reason: "A reviewed target window id is required in options." };
935
+ }
936
+ if (kind === "grouptabs") {
937
+ const group = options.group;
938
+ if (!group || typeof group !== "object" || Array.isArray(group)) return { allowed: false, reason: "A reviewed group with a name is required in options." };
939
+ const spec = group;
940
+ if (!isnonempty(spec.name)) return { allowed: false, reason: "The reviewed group needs a non-empty name." };
941
+ if (!validategroupcolor(spec.color)) return { allowed: false, reason: "The reviewed group color must be a Chromium tab group color." };
942
+ if (!validateidlist(spec, "tabids")) return { allowed: false, reason: "The reviewed group needs a non-empty list of member tab ids." };
943
+ }
944
+ if (kind === "colorgroup") {
945
+ if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed group name is required in options." };
946
+ if (!validategroupcolor(options.color)) return { allowed: false, reason: "The reviewed group color must be a Chromium tab group color." };
947
+ }
948
+ if (kind === "collapsegroup") {
949
+ if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed group name is required in options." };
950
+ if (typeof options.collapsed !== "boolean") return { allowed: false, reason: "A reviewed collapsed flag is required in options." };
951
+ }
952
+ if (kind === "discardtab" || kind === "reloadtabs") {
953
+ if (!isnumericid(step.value) && !validateidlist(options, "tabs")) return { allowed: false, reason: "A numeric tab id or a reviewed list of tab ids is required." };
954
+ }
955
+ if (kind === "zoomin" || kind === "zoomout") {
956
+ if (options.step !== void 0 && (typeof options.step !== "number" || !Number.isFinite(options.step) || options.step <= 0)) return { allowed: false, reason: "The reviewed zoom step must be a positive number with no code ceiling." };
957
+ if (step.value !== void 0 && step.value !== "" && !isnumericid(step.value)) return { allowed: false, reason: "The reviewed zoom target must be a numeric tab id." };
958
+ }
959
+ if (kind === "switchtab") {
960
+ if (options.direction !== "next" && options.direction !== "previous") return { allowed: false, reason: "A reviewed switch direction of next or previous is required in options." };
961
+ }
962
+ if (kind === "restorewindow") {
963
+ const bounds = options.bounds;
964
+ if (bounds !== void 0) {
965
+ if (!bounds || typeof bounds !== "object" || Array.isArray(bounds)) return { allowed: false, reason: "The reviewed window bounds must be an object." };
966
+ const shape = bounds;
967
+ for (const field of ["left", "top", "width", "height"]) {
968
+ if (typeof shape[field] !== "number" || !Number.isFinite(shape[field])) return { allowed: false, reason: "The reviewed window bounds need numeric left, top, width and height." };
969
+ }
970
+ }
971
+ }
972
+ if (kind === "scratchwindow") {
973
+ if (step.value !== void 0 && step.value !== "" && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed scratch window url must use HTTPS." };
974
+ }
975
+ if (kind === "incognitowindow" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS url is required to open an incognito window." };
976
+ if (kind === "restoretab" && step.value !== void 0 && step.value !== "" && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed restore url must use HTTPS." };
977
+ if (kind === "savelayout" || kind === "restorelayout") {
978
+ if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed layout name is required in options." };
979
+ }
980
+ if (kind === "badgetab") {
981
+ if (!isnonempty(options.label)) return { allowed: false, reason: "A reviewed badge label is required in options." };
982
+ if (options.taskid !== void 0 && !isnonempty(options.taskid)) return { allowed: false, reason: "The reviewed badge task id must be a non-empty string." };
983
+ }
984
+ if (kind === "attachmeta") {
985
+ const labels = options.labels;
986
+ const taskrefs = options.taskrefs;
987
+ const haslabels = Array.isArray(labels) && labels.length > 0 && labels.every((label) => isnonempty(label));
988
+ const hastaskrefs = Array.isArray(taskrefs) && taskrefs.length > 0 && taskrefs.every((ref) => isnonempty(ref));
989
+ if (!haslabels && !hastaskrefs) return { allowed: false, reason: "Reviewed labels or task refs are required in options to attach metadata." };
990
+ if (options.provenance !== void 0 && !isnonempty(options.provenance)) return { allowed: false, reason: "The reviewed provenance must be a non-empty string." };
991
+ }
992
+ if (kind === "reopenrun" && !isnonempty(options.run)) return { allowed: false, reason: "A reviewed run id is required in options to reopen its tabs." };
993
+ return { allowed: true };
994
+ }
564
995
  function validatestep(step, origin) {
565
996
  if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
566
997
  if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
@@ -762,6 +1193,24 @@ function validatestep(step, origin) {
762
1193
  const listcheck = validateurllist(options, "urls");
763
1194
  if (!listcheck.allowed) return listcheck;
764
1195
  }
1196
+ if (istabscommandkind(step.kind)) {
1197
+ const tabscheck = validatetabsgrammar(step, options);
1198
+ if (!tabscheck.allowed) return tabscheck;
1199
+ }
1200
+ if (isformkind(step.kind)) {
1201
+ const formcheck = validateformgrammar(step, options);
1202
+ if (!formcheck.allowed) return formcheck;
1203
+ }
1204
+ if (step.kind === "tabcreate") {
1205
+ if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1206
+ 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." };
1207
+ }
1208
+ if (step.kind === "windowcreate") {
1209
+ for (const field of ["left", "top", "width", "height"]) {
1210
+ if (options[field] !== void 0 && (typeof options[field] !== "number" || !Number.isFinite(options[field]))) return { allowed: false, reason: `The reviewed window ${field} must be a number.` };
1211
+ }
1212
+ if (options.state !== void 0 && !["normal", "maximized", "minimized", "fullscreen"].includes(options.state)) return { allowed: false, reason: "The reviewed window state must be normal, maximized, minimized or fullscreen." };
1213
+ }
765
1214
  return { allowed: true };
766
1215
  }
767
1216
  function sessiongate(input) {
@@ -792,6 +1241,16 @@ function canexecute(input) {
792
1241
  if (!navigation.allowed) return navigation;
793
1242
  }
794
1243
  }
1244
+ if (islayoutkind(input.step.kind) && !layoutmutationgranted(input.session, now).allowed) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
1245
+ if (input.step.kind === "submitform" || input.step.kind === "retryform") {
1246
+ if (!input.plan) return { allowed: false, reason: "Form submission requires an asksubmit review step before it." };
1247
+ const reviewgate = submitreviewgranted(input.plan.steps, input.step.id);
1248
+ if (!reviewgate.allowed) return reviewgate;
1249
+ }
1250
+ if (input.step.kind === "consentpassword") {
1251
+ const consentgate = passwordconsentgranted(input.step);
1252
+ if (!consentgate.allowed) return consentgate;
1253
+ }
795
1254
  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") {
796
1255
  let options = {};
797
1256
  try {
@@ -864,9 +1323,29 @@ function recordnaventry(progress, planid, stepid, entry, now) {
864
1323
  const outcome = { stepid, ok: entry.ok, summary: `Navigation list entry ${entry.index + 1} of ${entry.url} ${entry.ok ? "completed" : "failed"}.`, details: { naventry: entry }, at: now };
865
1324
  return recordoutcome(base, planid, outcome, now);
866
1325
  }
1326
+ function assigntasktab(progress, planid, tabid2, now) {
1327
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1328
+ if ((base.tasktabs ?? []).includes(tabid2)) return { ...base, updatedat: now };
1329
+ return { ...base, tasktabs: [...base.tasktabs ?? [], tabid2], updatedat: now };
1330
+ }
1331
+ function tasktabs(progress, planid) {
1332
+ if (!progress || progress.planid !== planid) return [];
1333
+ return progress.tasktabs ?? [];
1334
+ }
1335
+ function wizardcompletion(state) {
1336
+ if (state.steps <= 0) return 0;
1337
+ return Math.min(1, state.completed.filter(Boolean).length / state.steps);
1338
+ }
1339
+ function recordwizardstep(progress, planid, stepid, state, now) {
1340
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1341
+ const executed = Math.min(state.index, state.steps);
1342
+ const done = executed >= state.steps;
1343
+ const outcome = { stepid, ok: done, summary: `Wizard step ${executed} of ${state.steps} ${done ? "completed the wizard" : "executed"}.`, details: { wizard: { index: state.index, steps: state.steps, completed: [...state.completed] } }, at: now };
1344
+ return recordoutcome(base, planid, outcome, now);
1345
+ }
867
1346
 
868
1347
  // version.ts
869
- var packageversion = "1.1.35";
1348
+ var packageversion = "1.1.37";
870
1349
 
871
1350
  // types.ts
872
1351
  var protocolversion = packageversion;
@@ -907,6 +1386,11 @@ function parseproposal(value, origin) {
907
1386
  const options = parseoptions(step);
908
1387
  if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or frame wrapper references an unknown step id.");
909
1388
  }
1389
+ for (const step of steps) {
1390
+ if (step.kind !== "submitform" && step.kind !== "retryform") continue;
1391
+ const review = submitreviewgranted(steps, step.id);
1392
+ if (!review.allowed) throw new Error(review.reason);
1393
+ }
910
1394
  const createdat = Date.now();
911
1395
  const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
912
1396
  const plan = {
@@ -955,6 +1439,18 @@ function trailreport(input) {
955
1439
  function safetyresponse(input) {
956
1440
  return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, verdicts: input.verdicts });
957
1441
  }
1442
+ function layoutreport(input) {
1443
+ return { version: protocolversion, layouts: input.layouts };
1444
+ }
1445
+ function formreportresponse(input) {
1446
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, report: input.report });
1447
+ }
1448
+ function errorreportresponse(input) {
1449
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, report: input.report });
1450
+ }
1451
+ function wizardreport(input) {
1452
+ return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, wizards: input.wizards, picks: input.picks };
1453
+ }
958
1454
 
959
1455
  // extension/browsertabs.ts
960
1456
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -990,8 +1486,9 @@ async function runbrowseraction(step, sessiontabid, windowid) {
990
1486
  return { ok: true, summary: `Listed ${tabs.length} open tab${tabs.length === 1 ? "" : "s"}.`, details: { tabs: tabs.map((tab) => ({ id: tab.id ?? 0, index: tab.index, title: tab.title ?? "", url: tab.url ?? "", active: tab.active, pinned: tab.pinned, audible: tab.audible ?? false })) } };
991
1487
  }
992
1488
  case "tabcreate": {
993
- const created = await chrome.tabs.create({ url: step.value, active: options.active !== false, pinned: options.pinned === true });
994
- return { ok: true, summary: `Opened a new tab for ${step.value}.`, details: { tabid: created?.id ?? 0 } };
1489
+ const targetwindow = typeof options.window === "number" && Number.isFinite(options.window) ? options.window : void 0;
1490
+ const created = await chrome.tabs.create({ url: step.value, active: options.active !== false && options.background !== true, pinned: options.pinned === true, ...targetwindow !== void 0 ? { windowId: targetwindow } : {} });
1491
+ return { ok: true, summary: `Opened a new tab for ${step.value}${options.background === true ? " in the background without activating it" : ""}.`, details: { tabid: created?.id ?? 0, ...targetwindow !== void 0 ? { windowid: targetwindow } : {}, ...options.background === true ? { background: true } : {} } };
995
1492
  }
996
1493
  case "tabactivate": {
997
1494
  await chrome.tabs.update(tabid(step), { active: true });
@@ -1014,8 +1511,11 @@ async function runbrowseraction(step, sessiontabid, windowid) {
1014
1511
  return { ok: true, summary: `Listed ${windows.length} open window${windows.length === 1 ? "" : "s"}.`, details: { windows: windows.map((item) => ({ id: item.id ?? 0, type: item.type, state: item.state ?? "", focused: item.focused })) } };
1015
1512
  }
1016
1513
  case "windowcreate": {
1017
- const created = await chrome.windows.create({ url: step.value ?? "about:blank", ...typeof options.width === "number" ? { width: options.width } : {}, ...typeof options.height === "number" ? { height: options.height } : {} });
1018
- return { ok: true, summary: `Opened a new window for ${step.value}.`, details: { windowid: created?.id ?? 0 } };
1514
+ const bounds = ["left", "top", "width", "height"].filter((field) => typeof options[field] === "number");
1515
+ const geometry = Object.fromEntries(bounds.map((field) => [field, options[field]]));
1516
+ const state = typeof options.state === "string" && ["normal", "maximized", "minimized", "fullscreen"].includes(options.state) ? options.state : void 0;
1517
+ const created = await chrome.windows.create({ url: step.value ?? "about:blank", ...Object.keys(geometry).length > 0 ? geometry : {}, ...state !== void 0 ? { state } : {} });
1518
+ return { ok: true, summary: `Opened a new window for ${step.value}.`, details: { windowid: created?.id ?? 0, ...Object.keys(geometry).length > 0 ? { bounds: geometry } : {}, ...state !== void 0 ? { state } : {} } };
1019
1519
  }
1020
1520
  case "windowclose": {
1021
1521
  await chrome.windows.remove(tabid(step));
@@ -1039,6 +1539,125 @@ async function runbrowseraction(step, sessiontabid, windowid) {
1039
1539
  }
1040
1540
  }
1041
1541
 
1542
+ // extension/tabscommand.ts
1543
+ function parsetabquery(step) {
1544
+ let options = {};
1545
+ try {
1546
+ options = parseoptions(step);
1547
+ } catch {
1548
+ options = {};
1549
+ }
1550
+ const value = options.tabquery;
1551
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1552
+ const query = value;
1553
+ return {
1554
+ ...typeof query.url === "string" && query.url ? { url: query.url } : {},
1555
+ ...typeof query.title === "string" && query.title ? { title: query.title } : {},
1556
+ ...typeof query.id === "number" && Number.isInteger(query.id) && query.id >= 0 ? { id: query.id } : {},
1557
+ ...typeof query.pattern === "string" && query.pattern ? { pattern: query.pattern } : {}
1558
+ };
1559
+ }
1560
+ function tabpatternmatches(pattern, url) {
1561
+ const source = pattern.split("**").map((part) => part.split("*").map((piece) => piece.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")).join(".*");
1562
+ return new RegExp(`^${source}$`).test(url);
1563
+ }
1564
+ function querymatches(query, tabs) {
1565
+ return tabs.filter((tab) => {
1566
+ if (query.id !== void 0 && tab.tabid !== query.id) return false;
1567
+ if (query.url !== void 0 && tab.url !== query.url) return false;
1568
+ if (query.title !== void 0 && !tab.title.toLowerCase().includes(query.title.toLowerCase())) return false;
1569
+ if (query.pattern !== void 0 && !tabpatternmatches(query.pattern, tab.url)) return false;
1570
+ return true;
1571
+ });
1572
+ }
1573
+ function normalizedtaburl(url) {
1574
+ let normalized = url;
1575
+ const hash = normalized.indexOf("#");
1576
+ if (hash >= 0) normalized = normalized.slice(0, hash);
1577
+ while (normalized.length > 1 && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
1578
+ return normalized;
1579
+ }
1580
+ function clonetabs(tabs) {
1581
+ const groups = /* @__PURE__ */ new Map();
1582
+ for (const tab of tabs) {
1583
+ if (!tab.url) continue;
1584
+ const key = normalizedtaburl(tab.url);
1585
+ groups.set(key, [...groups.get(key) ?? [], tab.tabid]);
1586
+ }
1587
+ return [...groups.entries()].filter(([, tabids]) => tabids.length > 1).map(([url, tabids]) => ({ url, tabids }));
1588
+ }
1589
+ function searchtabmatches(tabs, text2) {
1590
+ const needle = text2.trim().toLowerCase();
1591
+ if (!needle) return [];
1592
+ return tabs.filter((tab) => tab.title.toLowerCase().includes(needle) || tab.url.toLowerCase().includes(needle));
1593
+ }
1594
+ function audiotabs(tabs) {
1595
+ return tabs.filter((tab) => tab.audible || tab.muted && tab.audible);
1596
+ }
1597
+ function discardcandidates(tabs) {
1598
+ return tabs.filter((tab) => !tab.active && !tab.pinned && !tab.discarded && tab.url.length > 0);
1599
+ }
1600
+ function buildlayout(name, tabs, windows, groups, scratchwindowids, at) {
1601
+ return {
1602
+ name,
1603
+ tabs: tabs.map((tab) => ({ url: tab.url, title: tab.title, pinned: tab.pinned, index: tab.index, windowid: tab.windowid })),
1604
+ groups: groups.map((group) => ({ name: group.name, color: group.color, tabids: group.tabids.filter((tabid2) => tabs.some((tab) => tab.tabid === tabid2)), collapsed: group.collapsed })),
1605
+ windows: windows.map((item) => ({ windowid: item.windowid, state: { bounds: { left: item.left, top: item.top, width: item.width, height: item.height }, maximized: item.state === "maximized", profile: item.incognito ? "incognito" : scratchwindowids.includes(item.windowid) ? "scratch" : "normal" } })),
1606
+ savedat: at
1607
+ };
1608
+ }
1609
+ function layoutrestoreplan(layout, openurls) {
1610
+ const open = new Set(openurls.map((url) => normalizedtaburl(url)));
1611
+ return layout.tabs.map((tab) => tab.url).filter((url) => url.length > 0 && !open.has(normalizedtaburl(url)));
1612
+ }
1613
+ function regroupaftermoves(groups, tabs, at) {
1614
+ const order = new Map(tabs.map((tab) => [tab.tabid, tab.index]));
1615
+ return groups.map((group) => {
1616
+ const members = group.tabids.filter((tabid2) => order.has(tabid2));
1617
+ if (members.length === 0) return group;
1618
+ const ordered = [...members].sort((left, right) => (order.get(left) ?? 0) - (order.get(right) ?? 0));
1619
+ return ordered.length === group.tabids.length && ordered.every((tabid2, index) => tabid2 === group.tabids[index]) ? group : { ...group, tabids: ordered, savedat: at };
1620
+ });
1621
+ }
1622
+ function tasktabsinwindow(tabs, windowid, tasktabids) {
1623
+ const tasks = new Set(tasktabids);
1624
+ return tabs.filter((tab) => tab.windowid === windowid && tasks.has(tab.tabid)).length;
1625
+ }
1626
+ function closeselection(query, tabs, sessiontabid) {
1627
+ const matches = querymatches(query, tabs);
1628
+ return {
1629
+ targets: matches.filter((tab) => tab.tabid !== sessiontabid),
1630
+ refused: matches.filter((tab) => tab.tabid === sessiontabid)
1631
+ };
1632
+ }
1633
+ function zoomstep(current, direction, step) {
1634
+ const next = direction === "in" ? current + step : current - step;
1635
+ return next > 0 ? Number(next.toFixed(4)) : current;
1636
+ }
1637
+ function switchtarget(tabs, direction, currentindex) {
1638
+ if (tabs.length === 0) return void 0;
1639
+ const offset = direction === "next" ? 1 : -1;
1640
+ return (currentindex + offset + tabs.length) % tabs.length;
1641
+ }
1642
+ function watchtabdispatch(events, watchid, filters) {
1643
+ const allowed = filters.length > 0 ? new Set(filters) : void 0;
1644
+ return events.filter((event) => event.watchid === watchid && (allowed === void 0 || allowed.has(event.event)));
1645
+ }
1646
+ function badgefromprogress(completed, total) {
1647
+ if (total <= 0) return { label: "idle", done: false };
1648
+ if (completed >= total) return { label: "done", done: true };
1649
+ return { label: `${completed}/${total}`, done: false };
1650
+ }
1651
+ function tasktabgauge(used, ceiling) {
1652
+ return { used, ceiling, over: ceiling !== void 0 && used > ceiling };
1653
+ }
1654
+ function windowprofilegrants(profile) {
1655
+ return profile !== "incognito";
1656
+ }
1657
+ function trackedtasktabs(progress, planid) {
1658
+ return tasktabs(progress, planid);
1659
+ }
1660
+
1042
1661
  // extension/pagedialogs.ts
1043
1662
  function parsedialogpolicy(step) {
1044
1663
  let options = {};
@@ -1487,6 +2106,113 @@ function authfor(auths, url) {
1487
2106
  return auths.find((record2) => record2.origin === origin);
1488
2107
  }
1489
2108
 
2109
+ // extension/pageforms.ts
2110
+ var firstnames = { en: ["alex", "jordan", "taylor", "morgan", "casey"], pt: ["ana", "bruno", "carla", "diego", "helena"] };
2111
+ var lastnames = { en: ["brooks", "carter", "diaz", "evans", "reyes"], pt: ["alves", "costa", "lima", "souza", "moraes"] };
2112
+ function localekey(locale) {
2113
+ const normalized = locale.toLowerCase();
2114
+ if (normalized.startsWith("pt")) return "pt";
2115
+ return "en";
2116
+ }
2117
+ function generatevalue(kind, rule) {
2118
+ const seed = typeof rule.seed === "number" && Number.isFinite(rule.seed) ? Math.abs(Math.floor(rule.seed)) : 1;
2119
+ const names = firstnames[localekey(rule.locale ?? "en")] ?? firstnames.en ?? ["alex"];
2120
+ const surnames = lastnames[localekey(rule.locale ?? "en")] ?? lastnames.en ?? ["brooks"];
2121
+ let state = seed * 1103515245 + 12345;
2122
+ const next = () => {
2123
+ state = (state * 1103515245 + 12345) % 2147483648;
2124
+ return state / 2147483648;
2125
+ };
2126
+ const pick = (items) => items[Math.floor(next() * items.length) % items.length] ?? items[0];
2127
+ const digits = (count) => Array.from({ length: count }, () => String(Math.floor(next() * 10))).join("");
2128
+ const person = `${pick(names)} ${pick(surnames)}`;
2129
+ switch (kind) {
2130
+ case "email":
2131
+ return `${person.replace(" ", ".")}${digits(2)}@example.com`;
2132
+ case "phone":
2133
+ return localekey(rule.locale ?? "en") === "pt" ? `+55 (11) 9${digits(4)}-${digits(4)}` : `+1 (555) 010-${digits(4)}`;
2134
+ case "date":
2135
+ return `${2024 + Math.floor(next() * 2)}-${String(1 + Math.floor(next() * 12)).padStart(2, "0")}-${String(1 + Math.floor(next() * 28)).padStart(2, "0")}`;
2136
+ case "number":
2137
+ return String(Math.floor(next() * 1e3));
2138
+ case "select":
2139
+ return `option ${1 + Math.floor(next() * 5)}`;
2140
+ case "check":
2141
+ return next() > 0.5 ? "true" : "false";
2142
+ case "radio":
2143
+ return `choice ${1 + Math.floor(next() * 4)}`;
2144
+ case "file":
2145
+ return `sample${digits(2)}.pdf`;
2146
+ case "password":
2147
+ return `pw-${digits(6)}-${pick(names)}`;
2148
+ case "card":
2149
+ return `4111 ${digits(4)} ${digits(4)} ${digits(4)}`;
2150
+ case "code":
2151
+ return digits(6);
2152
+ default:
2153
+ return person;
2154
+ }
2155
+ }
2156
+ function valueshash(values) {
2157
+ const source = values.map((entry) => `${entry.label}=${entry.value}`).join("|");
2158
+ let hash = 5381;
2159
+ for (let index = 0; index < source.length; index += 1) hash = (hash * 33 ^ source.charCodeAt(index)) >>> 0;
2160
+ return hash.toString(16);
2161
+ }
2162
+ function parseformrecord(value) {
2163
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2164
+ const record2 = value;
2165
+ if (!Array.isArray(record2.entries)) return null;
2166
+ const entries = [];
2167
+ for (const item of record2.entries) {
2168
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
2169
+ const entry = item;
2170
+ const match = entry.match;
2171
+ if (!match || typeof match !== "object" || Array.isArray(match)) continue;
2172
+ const shapes = match;
2173
+ if (typeof shapes.mode !== "string") continue;
2174
+ const fieldmatch = {
2175
+ mode: shapes.mode,
2176
+ ...typeof shapes.label === "string" ? { label: shapes.label } : {},
2177
+ ...typeof shapes.placeholder === "string" ? { placeholder: shapes.placeholder } : {},
2178
+ ...typeof shapes.arialabel === "string" ? { arialabel: shapes.arialabel } : {},
2179
+ ...typeof shapes.name === "string" ? { name: shapes.name } : {}
2180
+ };
2181
+ if (typeof entry.kind !== "string" || typeof entry.value !== "string") continue;
2182
+ entries.push({ match: fieldmatch, kind: entry.kind, value: entry.value });
2183
+ }
2184
+ if (entries.length === 0) return null;
2185
+ return { ...typeof record2.form === "string" && record2.form ? { form: record2.form } : {}, entries };
2186
+ }
2187
+
2188
+ // extension/pagewizards.ts
2189
+ function parsebackoff(step) {
2190
+ let options = {};
2191
+ try {
2192
+ options = parseoptions(step);
2193
+ } catch {
2194
+ options = {};
2195
+ }
2196
+ const backoff = options.backoff;
2197
+ if (!backoff || typeof backoff !== "object" || Array.isArray(backoff)) return null;
2198
+ const rule = backoff;
2199
+ const wait = rule.wait;
2200
+ const factor = rule.factor;
2201
+ if (typeof wait !== "number" || !Number.isFinite(wait) || wait <= 0) return null;
2202
+ if (typeof factor !== "number" || !Number.isFinite(factor) || factor < 1) return null;
2203
+ const attempts = typeof options.attempts === "number" && Number.isInteger(options.attempts) && options.attempts >= 1 ? options.attempts : 2;
2204
+ return { attempts, wait, factor };
2205
+ }
2206
+ function backoffwaits(attempts, wait, factor) {
2207
+ const windows = [];
2208
+ let current = wait;
2209
+ for (let index = 1; index < attempts; index += 1) {
2210
+ windows.push(current);
2211
+ current *= factor;
2212
+ }
2213
+ return windows;
2214
+ }
2215
+
1490
2216
  // extension/background.ts
1491
2217
  var sessionduration = 15 * 60 * 1e3;
1492
2218
  var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
@@ -1496,6 +2222,7 @@ var observationstepkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "
1496
2222
  var navigationstepkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "reloadcache", "stopnav", "waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "navlist", "navprofile", "detecthttp", "readredirects", "readfinalurl", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "trailaudit", "pausenav", "navintent", "navrate", "openclipboard", "checksafe", "batchopen"]);
1497
2223
  var pausenavkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "prefetch", "preconnect", "deeplink", "reopentab"]);
1498
2224
  var ratecheckedkinds = /* @__PURE__ */ new Set(["openlink", "openprivate", "followlink", "spanav", "navlist", "openclipboard", "batchopen", "deeplink", "reopentab"]);
2225
+ var formfillkinds = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "fillcard", "fillcode", "attachfile", "saveprofiles", "runwizard", "selectchain", "picktypeahead", "pickdate"]);
1499
2226
  var evidencepoll = 100;
1500
2227
  var evidencesettle = 5e3;
1501
2228
  var chromestorage = {
@@ -1661,9 +2388,22 @@ function browserauditkind(step) {
1661
2388
  }
1662
2389
  function stepauditkind(step, ok) {
1663
2390
  if (isbrowserkind(step.kind)) return browserauditkind(step);
2391
+ if (istabscommandkind(step.kind)) {
2392
+ if (step.kind === "grouptabs" || step.kind === "colorgroup" || step.kind === "collapsegroup") return "group";
2393
+ if (step.kind === "savelayout" || step.kind === "restorelayout" || step.kind === "snapshotsession" || step.kind === "reopenrun") return "layout";
2394
+ if (step.kind === "discardtab") return "discard";
2395
+ if (step.kind === "badgetab") return "badge";
2396
+ if (step.kind === "watchtab") return "watch";
2397
+ if (step.kind === "maximizewindow" || step.kind === "minimizewindow" || step.kind === "restorewindow" || step.kind === "focuswindow" || step.kind === "scratchwindow" || step.kind === "incognitowindow") return "window";
2398
+ return "tab";
2399
+ }
1664
2400
  if (step.kind === "dismissdialog") return "dialog";
1665
2401
  if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
1666
- if (step.kind === "retryaction") return "retry";
2402
+ if (step.kind === "retryaction" || step.kind === "retryform") return "retry";
2403
+ if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
2404
+ if (step.kind === "consentpassword") return "consent";
2405
+ if (step.kind === "handoffcaptcha") return "handoff";
2406
+ if (formfillkinds.has(step.kind)) return "fill";
1667
2407
  if (pointerkinds.has(step.kind)) return "pointer";
1668
2408
  if (watchstepkinds.has(step.kind)) return "watch";
1669
2409
  if (step.kind === "diffsnapshots") return "diff";
@@ -1900,6 +2640,10 @@ async function tracktabupdate(tabid2, changeinfo) {
1900
2640
  const now = Date.now();
1901
2641
  const url = typeof changeinfo.url === "string" ? changeinfo.url : void 0;
1902
2642
  const status = changeinfo.status;
2643
+ if (typeof changeinfo.title === "string" && changeinfo.title) {
2644
+ lastknowntitles.set(tabid2, changeinfo.title);
2645
+ await recordtabwatchevent("title", tabid2, changeinfo.title);
2646
+ }
1903
2647
  const previous = lastknownurls.get(tabid2);
1904
2648
  if (status === "loading" && url) {
1905
2649
  navbuffers.set(tabid2, [{ event: "beforenavigate", url, timestamp: now }]);
@@ -1924,10 +2668,20 @@ async function tracktabupdate(tabid2, changeinfo) {
1924
2668
  chrome.tabs.onUpdated.addListener((tabid2, changeinfo) => {
1925
2669
  void tracktabupdate(tabid2, changeinfo);
1926
2670
  });
2671
+ chrome.tabs.onActivated.addListener((activeinfo) => {
2672
+ void recordtabwatchevent("activated", activeinfo.tabId);
2673
+ });
1927
2674
  chrome.tabs.onRemoved.addListener((tabid2) => {
1928
2675
  const url = lastknownurls.get(tabid2);
1929
- if (url) void memory.addrecenttab({ url, tabid: tabid2, closedat: Date.now() });
2676
+ const title = lastknowntitles.get(tabid2) ?? "";
2677
+ const windowid = 0;
2678
+ if (url) {
2679
+ void memory.addrecenttab({ url, tabid: tabid2, closedat: Date.now() });
2680
+ void memory.addclosedtab({ url, title, tabid: tabid2, windowid, closedat: Date.now() });
2681
+ }
2682
+ void recordtabwatchevent("closed", tabid2, url);
1930
2683
  lastknownurls.delete(tabid2);
2684
+ lastknowntitles.delete(tabid2);
1931
2685
  navbuffers.delete(tabid2);
1932
2686
  });
1933
2687
  async function recordnavigation(step, session, tabid2) {
@@ -2351,9 +3105,523 @@ async function executenavigationkind(step, session, plan, tabid2, origin) {
2351
3105
  }
2352
3106
  }
2353
3107
  }
3108
+ async function livetabs() {
3109
+ const tabs = await chrome.tabs.query({}).catch(() => []);
3110
+ return tabs.map((tab) => ({
3111
+ tabid: tab.id ?? 0,
3112
+ url: tab.url ?? "",
3113
+ title: tab.title ?? "",
3114
+ index: tab.index,
3115
+ windowid: tab.windowId ?? 0,
3116
+ active: tab.active,
3117
+ pinned: tab.pinned,
3118
+ audible: tab.audible ?? false,
3119
+ muted: tab.mutedInfo?.muted ?? false,
3120
+ discarded: tab.discarded ?? false
3121
+ }));
3122
+ }
3123
+ async function livewindows() {
3124
+ const windows = await chrome.windows.getAll().catch(() => []);
3125
+ return windows.map((item) => ({
3126
+ windowid: item.id ?? 0,
3127
+ left: item.left ?? 0,
3128
+ top: item.top ?? 0,
3129
+ width: item.width ?? 0,
3130
+ height: item.height ?? 0,
3131
+ state: item.state === "maximized" || item.state === "minimized" || item.state === "fullscreen" ? item.state : "normal",
3132
+ incognito: item.incognito ?? false,
3133
+ focused: item.focused
3134
+ }));
3135
+ }
3136
+ var tabwatchbuffers = /* @__PURE__ */ new Map();
3137
+ var lastknowntitles = /* @__PURE__ */ new Map();
3138
+ async function recordtabwatchevent(event, tabid2, detail) {
3139
+ const now = Date.now();
3140
+ for (const watch of await memory.getwatches()) {
3141
+ if (watch.closedat !== void 0 || watch.kind !== "watchtab") continue;
3142
+ if (watchclosed(watch.startedat, watch.lifetime, now)) continue;
3143
+ if (watch.events.length > 0 && !watch.events.includes(event)) continue;
3144
+ const record2 = { watchid: watch.watchid, event, tabid: tabid2, ...detail !== void 0 ? { detail } : {}, at: now };
3145
+ await memory.addtabwatchevent(record2);
3146
+ tabwatchbuffers.set(watch.watchid, [...tabwatchbuffers.get(watch.watchid) ?? [], record2]);
3147
+ }
3148
+ }
3149
+ async function buildtabreport(matches) {
3150
+ const [groups, badges, metas] = await Promise.all([memory.gettabgroups(), memory.getbadges(), memory.gettabmetas()]);
3151
+ const metabytab = new Map(metas.map((meta) => [meta.tabid, meta]));
3152
+ const entries = matches.map((tab) => ({
3153
+ tabid: tab.tabid,
3154
+ url: tab.url,
3155
+ title: tab.title,
3156
+ index: tab.index,
3157
+ windowid: tab.windowid,
3158
+ active: tab.active,
3159
+ pinned: tab.pinned,
3160
+ audible: tab.audible,
3161
+ muted: tab.muted,
3162
+ discarded: tab.discarded,
3163
+ ...metabytab.has(tab.tabid) ? { meta: metabytab.get(tab.tabid) } : {}
3164
+ }));
3165
+ return { matches: entries, groups: groups.map((group) => ({ name: group.name, color: group.color, tabids: group.tabids, collapsed: group.collapsed })), badges };
3166
+ }
3167
+ function commandtabids(step, options) {
3168
+ const listed = Array.isArray(options.tabs) ? options.tabs.filter((item) => typeof item === "number" && Number.isInteger(item) && item >= 0) : [];
3169
+ const single = step.value && /^\d+$/.test(step.value) ? [Number.parseInt(step.value, 10)] : [];
3170
+ return listed.length > 0 ? listed : single;
3171
+ }
3172
+ async function executetabscommand(step, session, plan, sessiontabid) {
3173
+ const options = stepoptions2(step);
3174
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
3175
+ const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
3176
+ const layoutgate = layoutmutationgranted(session, Date.now());
3177
+ if (islayoutkind(step.kind) && !layoutgate.allowed) throw new Error(layoutgate.reason ?? "Group and layout mutations stay inside the active session.");
3178
+ switch (step.kind) {
3179
+ case "querytabs": {
3180
+ const query = parsetabquery(step);
3181
+ if (!query) throw new Error("A reviewed tabquery is required.");
3182
+ const matches = querymatches(query, await livetabs());
3183
+ const report = await buildtabreport(matches);
3184
+ await audit("tab", `Queried the live tab set and matched ${matches.length} tab${matches.length === 1 ? "" : "s"}.`, extra);
3185
+ return { ok: true, summary: `Matched ${matches.length} open tab${matches.length === 1 ? "" : "s"} by the reviewed tabquery.`, details: { report, matches: matches.length } };
3186
+ }
3187
+ case "duplicatetab": {
3188
+ const source = Number.parseInt(step.value ?? "", 10);
3189
+ const created = await chrome.tabs.duplicate(source);
3190
+ await audit("tab", `Duplicated tab ${source} with its history into tab ${created?.id ?? 0}.`, extra);
3191
+ return { ok: true, summary: `Duplicated tab ${source} with its history.`, details: { sourcetab: source, tabid: created?.id ?? 0 } };
3192
+ }
3193
+ case "closepattern": {
3194
+ const query = parsetabquery(step);
3195
+ if (!query) throw new Error("A reviewed tabquery is required.");
3196
+ const tabs = await livetabs();
3197
+ const selection = closeselection(query, tabs, session?.tabid ?? sessiontabid);
3198
+ if (selection.refused.length > 0) throw new Error("The close pattern matches the session tab itself; review the pattern so the session tab survives.");
3199
+ if (selection.targets.length === 0) return { ok: true, summary: "The reviewed close pattern matched no tab outside the session tab.", details: { closed: 0 } };
3200
+ for (const target of selection.targets) await chrome.tabs.remove(target.tabid).catch(() => void 0);
3201
+ await audit("tab", `Closed ${selection.targets.length} tab${selection.targets.length === 1 ? "" : "s"} matching the reviewed close pattern.`, extra);
3202
+ return { ok: true, summary: `Closed ${selection.targets.length} tab${selection.targets.length === 1 ? "" : "s"} matching the reviewed pattern.`, details: { closed: selection.targets.length, urls: selection.targets.map((tab) => tab.url) } };
3203
+ }
3204
+ case "pintab": {
3205
+ const target = Number.parseInt(step.value ?? "", 10);
3206
+ await chrome.tabs.update(target, { pinned: options.pinned === true });
3207
+ await audit("tab", `${options.pinned === true ? "Pinned" : "Unpinned"} tab ${target} by the reviewed flag.`, extra);
3208
+ return { ok: true, summary: `${options.pinned === true ? "Pinned" : "Unpinned"} tab ${target}.`, details: { tabid: target, pinned: options.pinned === true } };
3209
+ }
3210
+ case "mutetab": {
3211
+ const target = Number.parseInt(step.value ?? "", 10);
3212
+ await chrome.tabs.update(target, { muted: options.muted === true });
3213
+ await audit("tab", `${options.muted === true ? "Muted" : "Unmuted"} tab ${target} by the reviewed flag.`, extra);
3214
+ return { ok: true, summary: `${options.muted === true ? "Muted" : "Unmuted"} tab ${target}.`, details: { tabid: target, muted: options.muted === true } };
3215
+ }
3216
+ case "movetab": {
3217
+ const target = Number.parseInt(step.value ?? "", 10);
3218
+ await chrome.tabs.move(target, { index: options.index });
3219
+ const groups = regroupaftermoves(await memory.gettabgroups(), await livetabs(), Date.now());
3220
+ for (const group of groups) await memory.settabgroup(group);
3221
+ await audit("tab", `Moved tab ${target} to index ${options.index} inside its window; group membership is kept.`, extra);
3222
+ return { ok: true, summary: `Moved tab ${target} to index ${options.index}.`, details: { tabid: target, index: options.index } };
3223
+ }
3224
+ case "movetabwindow": {
3225
+ const target = Number.parseInt(step.value ?? "", 10);
3226
+ await chrome.tabs.move(target, { windowId: options.windowid, index: -1 });
3227
+ const groups = regroupaftermoves(await memory.gettabgroups(), await livetabs(), Date.now());
3228
+ for (const group of groups) await memory.settabgroup(group);
3229
+ await audit("tab", `Moved tab ${target} across windows into window ${options.windowid}; group membership is kept.`, extra);
3230
+ return { ok: true, summary: `Moved tab ${target} into window ${options.windowid}.`, details: { tabid: target, windowid: options.windowid } };
3231
+ }
3232
+ case "grouptabs": {
3233
+ const group = options.group;
3234
+ const record2 = { groupid: randomid(), name: String(group.name ?? ""), color: String(group.color ?? "grey"), tabids: Array.isArray(group.tabids) ? group.tabids.filter((item) => typeof item === "number") : [], collapsed: false, savedat: Date.now() };
3235
+ await memory.settabgroup(record2);
3236
+ await audit("group", `Grouped ${record2.tabids.length} tab${record2.tabids.length === 1 ? "" : "s"} under the reviewed name ${record2.name} with color ${record2.color}; membership lives in the Devthink group registry.`, extra);
3237
+ return { ok: true, summary: `Grouped ${record2.tabids.length} tab${record2.tabids.length === 1 ? "" : "s"} under ${record2.name}.`, details: { group: record2 } };
3238
+ }
3239
+ case "colorgroup": {
3240
+ const groups = await memory.gettabgroups();
3241
+ const target = groups.find((group) => group.name === options.name);
3242
+ if (!target) throw new Error(`No tab group named ${options.name} is stored yet.`);
3243
+ const updated = { ...target, color: String(options.color), savedat: Date.now() };
3244
+ await memory.settabgroup(updated);
3245
+ await audit("group", `Set the color of tab group ${updated.name} to ${updated.color}.`, extra);
3246
+ return { ok: true, summary: `Set the color of group ${updated.name} to ${updated.color}.`, details: { group: updated } };
3247
+ }
3248
+ case "collapsegroup": {
3249
+ const groups = await memory.gettabgroups();
3250
+ const target = groups.find((group) => group.name === options.name);
3251
+ if (!target) throw new Error(`No tab group named ${options.name} is stored yet.`);
3252
+ const updated = { ...target, collapsed: options.collapsed === true, savedat: Date.now() };
3253
+ await memory.settabgroup(updated);
3254
+ await audit("group", `${updated.collapsed ? "Collapsed" : "Expanded"} tab group ${updated.name}.`, extra);
3255
+ return { ok: true, summary: `${updated.collapsed ? "Collapsed" : "Expanded"} group ${updated.name}.`, details: { group: updated } };
3256
+ }
3257
+ case "discardtab": {
3258
+ const ids = commandtabids(step, options);
3259
+ if (ids.length === 0) throw new Error("A numeric tab id or a reviewed list of tab ids is required.");
3260
+ const candidates = discardcandidates(await livetabs()).filter((tab) => ids.includes(tab.tabid));
3261
+ const discarded = [];
3262
+ const urls = [];
3263
+ for (const candidate of candidates) {
3264
+ const result = await chrome.tabs.discard(candidate.tabid).catch(() => void 0);
3265
+ if (result) {
3266
+ discarded.push(candidate.tabid);
3267
+ urls.push({ tabid: candidate.tabid, url: candidate.url });
3268
+ }
3269
+ }
3270
+ await audit("discard", `Discarded ${discarded.length} inactive tab${discarded.length === 1 ? "" : "s"} to save memory; the urls survive for on demand restore.`, extra);
3271
+ return { ok: discarded.length > 0, summary: `Discarded ${discarded.length} inactive tab${discarded.length === 1 ? "" : "s"}; their urls stay available for restore.`, details: { discarded, urls, refused: ids.filter((id) => !discarded.includes(id)) } };
3272
+ }
3273
+ case "reloadtabs": {
3274
+ const ids = commandtabids(step, options);
3275
+ if (ids.length === 0) throw new Error("A numeric tab id or a reviewed list of tab ids is required.");
3276
+ for (const id of ids) await chrome.tabs.reload(id).catch(() => void 0);
3277
+ await audit("tab", `Reloaded ${ids.length} reviewed tab${ids.length === 1 ? "" : "s"}.`, extra);
3278
+ return { ok: true, summary: `Reloaded ${ids.length} tab${ids.length === 1 ? "" : "s"}.`, details: { tabs: ids } };
3279
+ }
3280
+ case "zoomin":
3281
+ case "zoomout": {
3282
+ const target = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : sessiontabid;
3283
+ const current = await chrome.tabs.getZoom(target);
3284
+ const next = zoomstep(current, step.kind === "zoomin" ? "in" : "out", typeof options.step === "number" && options.step > 0 ? options.step : 0.1);
3285
+ await chrome.tabs.setZoom(target, next);
3286
+ await audit("tab", `Zoomed tab ${target} ${step.kind === "zoomin" ? "in" : "out"} from ${current} to ${next} by the reviewed step.`, extra);
3287
+ return { ok: true, summary: `Zoomed tab ${target} ${step.kind === "zoomin" ? "in" : "out"} to ${next}.`, details: { tabid: target, from: current, to: next, step: typeof options.step === "number" ? options.step : 0.1 } };
3288
+ }
3289
+ case "switchtab": {
3290
+ const direction = options.direction === "previous" ? "previous" : "next";
3291
+ const focusedwindow = (await livewindows()).find((item) => item.focused)?.windowid ?? 0;
3292
+ const windowtabs = (await livetabs()).filter((tab) => tab.windowid === focusedwindow);
3293
+ const active = windowtabs.find((tab) => tab.active);
3294
+ const target = switchtarget(windowtabs, direction, active?.index ?? 0);
3295
+ const totab = windowtabs.find((tab) => tab.index === target);
3296
+ if (!totab) throw new Error("No neighbor tab is available to switch to.");
3297
+ await chrome.tabs.update(totab.tabid, { active: true });
3298
+ await audit("tab", `Switched to the ${direction} tab ${totab.tabid}.`, extra);
3299
+ return { ok: true, summary: `Switched to the ${direction} tab.`, details: { tabid: totab.tabid, direction } };
3300
+ }
3301
+ case "maximizewindow": {
3302
+ await chrome.windows.update(windowid, { state: "maximized" });
3303
+ await audit("window", `Maximized window ${windowid}.`, extra);
3304
+ return { ok: true, summary: `Maximized window ${windowid}.`, details: { windowid, state: "maximized" } };
3305
+ }
3306
+ case "minimizewindow": {
3307
+ await chrome.windows.update(windowid, { state: "minimized" });
3308
+ await audit("window", `Minimized window ${windowid}.`, extra);
3309
+ return { ok: true, summary: `Minimized window ${windowid}.`, details: { windowid, state: "minimized" } };
3310
+ }
3311
+ case "restorewindow": {
3312
+ const bounds = options.bounds;
3313
+ await chrome.windows.update(windowid, { state: "normal", ...bounds && typeof bounds.left === "number" ? { left: bounds.left } : {}, ...bounds && typeof bounds.top === "number" ? { top: bounds.top } : {}, ...bounds && typeof bounds.width === "number" ? { width: bounds.width } : {}, ...bounds && typeof bounds.height === "number" ? { height: bounds.height } : {} });
3314
+ await audit("window", `Restored window ${windowid} to its reviewed bounds.`, extra);
3315
+ return { ok: true, summary: `Restored window ${windowid} to its reviewed bounds.`, details: { windowid, bounds: bounds ?? null } };
3316
+ }
3317
+ case "focuswindow": {
3318
+ await chrome.windows.update(windowid, { focused: true });
3319
+ await audit("window", `Focused window ${windowid}.`, extra);
3320
+ return { ok: true, summary: `Focused window ${windowid}.`, details: { windowid } };
3321
+ }
3322
+ case "scratchwindow": {
3323
+ const url = typeof step.value === "string" && step.value ? step.value : "about:blank";
3324
+ const created = await chrome.windows.create({ url });
3325
+ const window2 = created?.id ?? 0;
3326
+ await memory.setscratchwindows([...await memory.getscratchwindows(), window2]);
3327
+ await audit("window", `Opened a scratch window ${window2} for split work.`, extra);
3328
+ return { ok: true, summary: `Opened a scratch window for split work.`, details: { windowid: window2, url } };
3329
+ }
3330
+ case "incognitowindow": {
3331
+ const created = await chrome.windows.create({ url: step.value ?? "", incognito: true });
3332
+ await audit("window", `Opened an incognito window ${created?.id ?? 0} for ${step.value} on the explicit reviewed request; the window stays separated from the session grant inheritance.`, extra);
3333
+ return { ok: true, summary: `Opened an incognito window for ${step.value} on explicit request.`, details: { windowid: created?.id ?? 0, url: step.value, grantsinherited: windowprofilegrants("incognito") } };
3334
+ }
3335
+ case "restoretab": {
3336
+ const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
3337
+ let url = step.value && /^https:\/\//.test(step.value) ? step.value : void 0;
3338
+ if (!url) {
3339
+ const closed = (await memory.getclosedtabs()).find((entry) => !open.includes(entry.url));
3340
+ if (!closed) throw new Error("No closed tab is available to restore from the session history.");
3341
+ url = closed.url;
3342
+ }
3343
+ const created = await chrome.tabs.create({ url, active: true });
3344
+ await audit("tab", `Restored the closed tab ${url} from the session history.`, extra);
3345
+ return { ok: true, summary: `Restored ${url} from the closed tab history.`, details: { url, tabid: created?.id ?? 0 } };
3346
+ }
3347
+ case "savelayout": {
3348
+ const name = typeof options.name === "string" ? options.name : "";
3349
+ const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
3350
+ const layout = buildlayout(name, tabs, windows, groups, scratch, Date.now());
3351
+ await memory.setlayout(layout);
3352
+ await audit("layout", `Saved the tab layout ${name} with ${layout.tabs.length} tab${layout.tabs.length === 1 ? "" : "s"}, ${layout.groups.length} group${layout.groups.length === 1 ? "" : "s"} and ${layout.windows.length} window bound${layout.windows.length === 1 ? "" : "s"}.`, extra);
3353
+ return { ok: true, summary: `Saved the tab layout ${name}.`, details: { layout } };
3354
+ }
3355
+ case "restorelayout": {
3356
+ const name = typeof options.name === "string" ? options.name : "";
3357
+ const layout = await memory.getlayout(name);
3358
+ if (!layout) throw new Error(`No tab layout named ${name} is stored yet.`);
3359
+ const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
3360
+ const urls = layoutrestoreplan(layout, open);
3361
+ const created = [];
3362
+ for (const url of urls) {
3363
+ const tab = await chrome.tabs.create({ url, active: created.length === 0 });
3364
+ created.push(tab?.id ?? 0);
3365
+ }
3366
+ await audit("layout", `Restored the tab layout ${name}: ${created.length} tab${created.length === 1 ? "" : "s"} reopened, ${layout.tabs.length - created.length} already open.`, extra);
3367
+ return { ok: true, summary: `Restored the tab layout ${name}.`, details: { name, reopened: created.length, alreadyopen: layout.tabs.length - created.length, tabs: created } };
3368
+ }
3369
+ case "findclones": {
3370
+ const clones = clonetabs(await livetabs());
3371
+ await audit("tab", `Detected ${clones.length} duplicate url group${clones.length === 1 ? "" : "s"} across the open tabs.`, extra);
3372
+ return { ok: true, summary: clones.length === 0 ? "No duplicate tab was detected by normalized url comparison." : `Detected ${clones.length} duplicate url group${clones.length === 1 ? "" : "s"}.`, details: { clones } };
3373
+ }
3374
+ case "searchtabs": {
3375
+ const matches = searchtabmatches(await livetabs(), step.value ?? "");
3376
+ await audit("tab", `Searched the open tabs and matched ${matches.length} tab${matches.length === 1 ? "" : "s"} for "${step.value}".`, extra);
3377
+ return { ok: true, summary: `Matched ${matches.length} open tab${matches.length === 1 ? "" : "s"} for "${step.value}".`, details: { matches } };
3378
+ }
3379
+ case "badgetab": {
3380
+ const target = Number.parseInt(step.value ?? "", 10);
3381
+ const badge = { tabid: target, taskid: typeof options.taskid === "string" && options.taskid ? options.taskid : plan.id, label: typeof options.label === "string" ? options.label : "", setat: Date.now() };
3382
+ await memory.setbadge(badge);
3383
+ await refreshbadge();
3384
+ await audit("badge", `Set the task badge of tab ${target} to ${badge.label} for task ${badge.taskid}.`, extra);
3385
+ return { ok: true, summary: `Set the badge of tab ${target} to ${badge.label}.`, details: { badge } };
3386
+ }
3387
+ case "attachmeta": {
3388
+ const target = Number.parseInt(step.value ?? "", 10);
3389
+ const meta = {
3390
+ tabid: target,
3391
+ taskrefs: Array.isArray(options.taskrefs) ? options.taskrefs.filter((item) => typeof item === "string" && item.trim().length > 0) : [],
3392
+ provenance: typeof options.provenance === "string" && options.provenance ? options.provenance : "plan step",
3393
+ labels: Array.isArray(options.labels) ? options.labels.filter((item) => typeof item === "string" && item.trim().length > 0) : [],
3394
+ at: Date.now()
3395
+ };
3396
+ await memory.settabmeta(meta);
3397
+ await memory.setprogress(assigntasktab(await memory.getprogress(), plan.id, target, Date.now()));
3398
+ await audit("tab", `Attached metadata to tab ${target} with ${meta.labels.length} label${meta.labels.length === 1 ? "" : "s"} and ${meta.taskrefs.length} task ref${meta.taskrefs.length === 1 ? "" : "s"}; the tab joins the task progress.`, extra);
3399
+ return { ok: true, summary: `Attached metadata to tab ${target} for task routing.`, details: { meta } };
3400
+ }
3401
+ case "listaudio": {
3402
+ const playing = audiotabs(await livetabs());
3403
+ await audit("tab", `Listed ${playing.length} tab${playing.length === 1 ? "" : "s"} that are playing audio.`, extra);
3404
+ return { ok: true, summary: `${playing.length} tab${playing.length === 1 ? " is" : "s are"} playing audio.`, details: { audio: playing } };
3405
+ }
3406
+ case "reopenrun": {
3407
+ const run = typeof options.run === "string" ? options.run : "";
3408
+ const snapshots = await memory.getsnapshots();
3409
+ const snapshot2 = snapshots.find((item) => item.sessionid === run) ?? snapshots.find((item) => item.id === run);
3410
+ if (!snapshot2) throw new Error(`No stored session snapshot exists for the run ${run}.`);
3411
+ const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
3412
+ const urls = layoutrestoreplan(snapshot2.layout, open);
3413
+ const created = [];
3414
+ for (const url of urls) {
3415
+ const tab = await chrome.tabs.create({ url, active: created.length === 0 });
3416
+ created.push(tab?.id ?? 0);
3417
+ }
3418
+ await audit("layout", `Reopened ${created.length} tab${created.length === 1 ? "" : "s"} of the previous run ${run}.`, extra);
3419
+ return { ok: true, summary: `Reopened ${created.length} tab${created.length === 1 ? "" : "s"} of the previous run.`, details: { run, reopened: created.length, tabs: created } };
3420
+ }
3421
+ case "snapshotsession": {
3422
+ const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
3423
+ const layout = buildlayout(`session ${(/* @__PURE__ */ new Date()).toISOString()}`, tabs, windows, groups, scratch, Date.now());
3424
+ const snapshot2 = { id: randomid(), ...session ? { sessionid: session.id } : {}, layout, capturedat: Date.now() };
3425
+ await memory.addsnapshot(snapshot2);
3426
+ await audit("layout", `Captured the full session snapshot with ${layout.tabs.length} tab${layout.tabs.length === 1 ? "" : "s"} and ${layout.windows.length} window${layout.windows.length === 1 ? "" : "s"}.`, extra);
3427
+ return { ok: true, summary: `Captured the session snapshot of ${layout.tabs.length} tabs and ${layout.windows.length} windows.`, details: { snapshot: snapshot2 } };
3428
+ }
3429
+ case "watchtab": {
3430
+ const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
3431
+ if (lifetime <= 0) throw new Error("A reviewed watch lifetime window in milliseconds is required in options.");
3432
+ const events = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
3433
+ const watchid = typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : randomid();
3434
+ const watch = { watchid, kind: "watchtab", stepid: step.id, sessionid: session?.id ?? plan.id, origin: session?.origin ?? plan.origin, scopes: [], events, startedat: Date.now(), lifetime };
3435
+ await memory.addwatch(watch);
3436
+ await audit("watch", `Watchtab registered under id ${watchid} for the reviewed lifetime of ${lifetime} milliseconds${events.length > 0 ? ` over events ${events.join(", ")}` : " over title, activation and closure events"}.`, extra);
3437
+ await new Promise((resolve) => setTimeout(resolve, lifetime));
3438
+ const observed = watchtabdispatch(tabwatchbuffers.get(watchid) ?? [], watchid, events);
3439
+ await memory.closewatch(watchid, Date.now());
3440
+ tabwatchbuffers.delete(watchid);
3441
+ await audit("watch", `Watchtab ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds with ${observed.length} observed event${observed.length === 1 ? "" : "s"}.`, extra);
3442
+ return { ok: true, summary: `Observed ${observed.length} tab event${observed.length === 1 ? "" : "s"} inside the reviewed lifetime.`, details: { watchid, events: observed } };
3443
+ }
3444
+ default:
3445
+ return { ok: false, summary: "Unsupported tabs and windows command." };
3446
+ }
3447
+ }
3448
+ async function executesaveprofiles(step, session, origin) {
3449
+ const options = stepoptions2(step);
3450
+ const record2 = parseformrecord(options.formrecord);
3451
+ const name = typeof options.name === "string" ? options.name : "";
3452
+ if (!name || !record2) throw new Error("A reviewed profile name and form record are required.");
3453
+ const grants = session?.grants ?? (session ? [session.origin] : [origin]);
3454
+ const profile = { name, fields: record2.entries, grants, savedat: Date.now() };
3455
+ await memory.setprofile(profile);
3456
+ await audit("fill", `Form profile ${name} stored locally with ${profile.fields.length} field entries behind the origin grants of ${grants.join(", ")}; password entries are refused.`, { ...session ? { sessionid: session.id } : {} });
3457
+ return { ok: true, summary: `Stored the form profile ${name} locally with ${profile.fields.length} field entries.`, details: { profile: { name: profile.name, fields: profile.fields.length, grants: profile.grants } } };
3458
+ }
3459
+ async function executeasksubmit(step, session, plan, tabid2, origin) {
3460
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
3461
+ const values = Array.isArray(output?.details?.values) ? output?.details?.values : [];
3462
+ const ticket = { id: randomid(), form: step.value ?? "", valueshash: valueshash(values), consentref: step.id, at: Date.now() };
3463
+ await memory.setticket(ticket);
3464
+ await audit("submit", `Asksubmit prompt opened for form ${ticket.form || "the reviewed form"} with the values hash ${ticket.valueshash}; the submission waits for the user approval.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3465
+ await refreshbadge();
3466
+ return { ok: true, summary: `Asksubmit prompt opened for form ${ticket.form || "the reviewed form"}; the submission waits for your approval.`, details: { ticket, values } };
3467
+ }
3468
+ async function executesubmitform(step, session, plan, tabid2, origin) {
3469
+ const consentref = typeof stepoptions2(step).consentref === "string" ? stepoptions2(step).consentref : "";
3470
+ const ticket = (await memory.gettickets()).find((item) => item.approved === true && (item.consentref === consentref || item.id === consentref));
3471
+ if (!ticket) throw new Error("No approved asksubmit ticket matches the reviewed consent ref; approve the submission in the review panel first.");
3472
+ const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The form submission returned no result." };
3473
+ await audit("submit", `Form ${ticket.form || "the reviewed form"} submitted through its owning form under ticket ${ticket.id} with values hash ${ticket.valueshash} and outcome ${output.ok ? "delivered" : "refused"}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3474
+ return { ...output, details: { ...output.details ?? {}, ticket: { id: ticket.id, valueshash: ticket.valueshash, consentref: ticket.consentref } } };
3475
+ }
3476
+ async function executeretryform(step, session, plan, tabid2, origin) {
3477
+ const rule = parsebackoff(step);
3478
+ if (!rule) throw new Error("A reviewed backoff rule with wait and factor is required.");
3479
+ const windows = backoffwaits(rule.attempts, rule.wait, rule.factor);
3480
+ let attempts = 0;
3481
+ let output;
3482
+ while (attempts < rule.attempts) {
3483
+ attempts += 1;
3484
+ output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The retried submission returned no result." };
3485
+ if (output.ok) break;
3486
+ const waitwindow = windows[attempts - 1];
3487
+ if (waitwindow !== void 0 && attempts < rule.attempts) await new Promise((resolve) => setTimeout(resolve, waitwindow));
3488
+ }
3489
+ await audit("retry", `Form submission retried ${attempts} time${attempts === 1 ? "" : "s"} with the reviewed backoff windows ${windows.join(", ") || "none"} milliseconds; outcome ${output?.ok ? "delivered" : "refused"}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3490
+ return { ok: Boolean(output?.ok), summary: output?.summary ?? "The retried submission returned no result.", details: { attempts, windows, ok: Boolean(output?.ok) } };
3491
+ }
3492
+ async function executeconsentpassword(step, session, plan, tabid2, origin) {
3493
+ const consentref = typeof stepoptions2(step).consentref === "string" ? stepoptions2(step).consentref : "";
3494
+ const gate = passwordconsentgranted(step);
3495
+ if (!gate.allowed) throw new Error(gate.reason ?? "A password fill requires a reviewed consent ref.");
3496
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
3497
+ await audit("consent", `Password field filled after the explicit consent ref ${consentref}; the value never appears in the audit trail.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3498
+ return output ?? { ok: false, summary: "The password fill returned no result." };
3499
+ }
3500
+ async function executeattachfile(step, session, plan, tabid2, origin) {
3501
+ const name = step.value ?? "";
3502
+ const artifacts = await memory.getartifacts();
3503
+ const artifact = artifacts.find((item) => item.name === name || item.id === name);
3504
+ if (!artifact) throw new Error(`No generated artifact named ${name} exists in the run store yet.`);
3505
+ const derived = { ...step, options: JSON.stringify({ ...stepoptions2(step), artifact: artifact.id, artifactname: artifact.name }) };
3506
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
3507
+ await audit("fill", `Artifact ${artifact.name} of kind ${artifact.kind} attached to the reviewed file input inside the form submission.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3508
+ return { ...output ?? { ok: false, summary: "The artifact attachment returned no result." }, details: { ...output?.details ?? {}, artifact } };
3509
+ }
3510
+ async function executecaptchahandoff(step, session, plan, tabid2, origin) {
3511
+ const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The captcha probe returned no result." };
3512
+ if (output.details?.captcha !== true) return { ok: true, summary: "No captcha was detected; the plan continues.", details: { captcha: false } };
3513
+ const handoff = { id: randomid(), origin, resolved: false, openedat: Date.now() };
3514
+ await memory.addcaptcha(handoff);
3515
+ if (session && !session.pausedat && !session.stoppedat) await memory.setsession({ ...session, pausedat: Date.now() });
3516
+ await audit("handoff", `Captcha detected on ${origin}; control handed back to the user and the plan pauses until the handoff ${handoff.id} resolves.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3517
+ await refreshbadge();
3518
+ return { ok: true, summary: "Captcha detected; control is yours and the plan waits until you resolve the handoff.", details: { captcha: true, handoff } };
3519
+ }
3520
+ async function executeformstep(step, session, plan, tabid2, origin) {
3521
+ switch (step.kind) {
3522
+ case "saveprofiles":
3523
+ return executesaveprofiles(step, session, origin);
3524
+ case "asksubmit":
3525
+ return executeasksubmit(step, session, plan, tabid2, origin);
3526
+ case "submitform":
3527
+ return executesubmitform(step, session, plan, tabid2, origin);
3528
+ case "retryform":
3529
+ return executeretryform(step, session, plan, tabid2, origin);
3530
+ case "consentpassword":
3531
+ return executeconsentpassword(step, session, plan, tabid2, origin);
3532
+ case "attachfile":
3533
+ return executeattachfile(step, session, plan, tabid2, origin);
3534
+ case "handoffcaptcha":
3535
+ return executecaptchahandoff(step, session, plan, tabid2, origin);
3536
+ case "fillcode": {
3537
+ const stored = await memory.getcodevalue();
3538
+ const source = typeof stepoptions2(step).source === "string" ? stepoptions2(step).source : "";
3539
+ const derived = stored !== void 0 && source === "reviewed" ? { ...step, value: stored } : step;
3540
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
3541
+ await audit("fill", `One time code typed from the reviewed source ${source}${stored !== void 0 ? " through the consent gated code entry" : ""}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
3542
+ return output ?? { ok: false, summary: "The one time code fill returned no result." };
3543
+ }
3544
+ default: {
3545
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
3546
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
3547
+ if (step.kind === "runwizard" && output?.details?.wizard && typeof output.details.wizard === "object") {
3548
+ const state = output.details.wizard;
3549
+ await memory.addwizard(state);
3550
+ await memory.setprogress(recordwizardstep(await memory.getprogress(), plan.id, step.id, state, Date.now()));
3551
+ await audit("fill", `Wizard advanced to step ${Math.min(state.index, state.steps)} of ${state.steps} with a completion share of ${Math.round(wizardcompletion(state) * 100)} percent.`, extra);
3552
+ }
3553
+ if (step.kind === "picktypeahead" && typeof output?.details?.pick === "string") {
3554
+ const pick = { field: step.target ?? "", query: step.value ?? "", pick: output.details.pick, at: Date.now() };
3555
+ await memory.addpick(pick);
3556
+ }
3557
+ if (step.kind === "readerrors" && Array.isArray(output?.details?.errors)) {
3558
+ const report = { form: step.target ?? "", errors: output?.details?.errors, at: Date.now() };
3559
+ await memory.adderrorreport(report);
3560
+ await audit("fill", `Collected ${report.errors.length} inline validation message${report.errors.length === 1 ? "" : "s"} for the correction loop.`, extra);
3561
+ }
3562
+ if ((step.kind === "detectlogin" || step.kind === "detecttemplate") && output?.ok) {
3563
+ const detected = step.kind === "detectlogin" ? output.details?.login === true : output.details?.template === "signup" || output.details?.template === "checkout";
3564
+ if (detected) {
3565
+ const kind = step.kind === "detectlogin" ? "login" : output.details?.template === "checkout" ? "checkout" : "signup";
3566
+ const markers = Array.isArray(output.details?.markers) ? output.details?.markers : [];
3567
+ const record2 = { origin, kind, markers, at: Date.now() };
3568
+ await memory.adddetection(record2);
3569
+ await audit("fill", `${kind} shape detected on ${origin} with markers ${markers.join(", ") || "none"}; sensitive work stays behind the consent gates.`, extra);
3570
+ }
3571
+ }
3572
+ if (step.kind === "skiphoneypot" && Array.isArray(output?.details?.skipped)) {
3573
+ await audit("fill", `Honeypot survey flagged ${output.details.skipped.length} field${output.details.skipped.length === 1 ? "" : "s"} as skipped so fills never trip them.`, extra);
3574
+ }
3575
+ if (step.kind === "fillform" || step.kind === "filllabel" || step.kind === "fillplaceholder") {
3576
+ const filled = typeof output?.details?.filled === "number" ? output.details.filled : 0;
3577
+ const skipped = Array.isArray(output?.details?.skipped) ? output.details.skipped.length : 0;
3578
+ await audit("fill", `Filled ${filled} reviewed field${filled === 1 ? "" : "s"}${skipped > 0 ? ` and skipped ${skipped} honeypot field${skipped === 1 ? "" : "s"}` : ""}.`, extra);
3579
+ }
3580
+ if (step.kind === "generatevalues") {
3581
+ const count = Array.isArray(output?.details?.values) ? output.details.values.length : 0;
3582
+ await audit("fill", `Generated ${count} realistic value${count === 1 ? "" : "s"} with the reviewed seed and locale; real looking card numbers and personal identifiers are refused.`, extra);
3583
+ }
3584
+ return output ?? { ok: false, summary: "The forms and data step returned no result." };
3585
+ }
3586
+ }
3587
+ }
3588
+ async function enforcewindowreview(step, session, plan) {
3589
+ const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
3590
+ const progress = plan ? await memory.getprogress() : void 0;
3591
+ const tasktabids = plan ? trackedtasktabs(progress, plan.id) : [];
3592
+ const count = tasktabsinwindow(await livetabs(), windowid, tasktabids);
3593
+ const gate = windowclosegate(count, stepoptions2(step).reviewed === true);
3594
+ if (!gate.allowed) throw new Error(gate.reason ?? "The window close needs explicit review.");
3595
+ if (session && count > 0) await audit("window", `Window ${windowid} closes while holding ${count} task tab${count === 1 ? "" : "s"} under the explicit reviewed flag.`, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid: step.id });
3596
+ }
3597
+ async function updatetaskbadges(plan, progress) {
3598
+ if (!plan || !progress || progress.planid !== plan.id) return;
3599
+ const state = badgefromprogress(progress.completedsteps.length, plan.steps.length);
3600
+ for (const tabid2 of tasktabs(progress, plan.id)) {
3601
+ await memory.setbadge({ tabid: tabid2, taskid: plan.id, label: state.label, setat: Date.now() });
3602
+ }
3603
+ }
3604
+ async function togglecontroltab(enabled) {
3605
+ const current = await memory.getcontroltab();
3606
+ if (current && current.tabid) await chrome.tabs.remove(current.tabid).catch(() => void 0);
3607
+ if (!enabled) {
3608
+ const closed = { tabid: 0, enabled: false, updatedat: Date.now() };
3609
+ await memory.setcontroltab(closed);
3610
+ await audit("tab", "The pinned control tab was closed.");
3611
+ return closed;
3612
+ }
3613
+ const created = await chrome.tabs.create({ url: chrome.runtime.getURL("sidepanel.html"), pinned: true, active: false });
3614
+ const state = { tabid: created?.id ?? 0, enabled: true, updatedat: Date.now() };
3615
+ await memory.setcontroltab(state);
3616
+ await audit("tab", `The pinned control tab ${state.tabid} was opened with the live task feed.`);
3617
+ return state;
3618
+ }
2354
3619
  async function refreshbadge() {
2355
3620
  const queues = await memory.getnavqueues();
2356
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0);
3621
+ const badges = await memory.getbadges();
3622
+ const prompts = (await memory.gettickets()).filter((ticket) => ticket.approved === void 0).length;
3623
+ const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
3624
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts;
2357
3625
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
2358
3626
  });
2359
3627
  }
@@ -2372,7 +3640,14 @@ async function executestep(stepid) {
2372
3640
  }
2373
3641
  let output;
2374
3642
  let watchwindow;
2375
- if (isbrowserkind(step.kind)) {
3643
+ if (step.kind === "windowclose") {
3644
+ await enforcewindowreview(step, session, plan);
3645
+ }
3646
+ if (istabscommandkind(step.kind)) {
3647
+ output = await executetabscommand(step, session, plan, tab.id);
3648
+ } else if (isformkind(step.kind)) {
3649
+ output = await executeformstep(step, session, plan, tab.id, origin);
3650
+ } else if (isbrowserkind(step.kind)) {
2376
3651
  output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
2377
3652
  } else if (step.kind === "keyhold") {
2378
3653
  output = await executekeyhold(step, session, plan, tab.id, origin);
@@ -2404,6 +3679,9 @@ async function executestep(stepid) {
2404
3679
  }
2405
3680
  if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
2406
3681
  await recordevidence(step, output, session, plan, origin);
3682
+ if (output?.ok && plan && typeof output.details?.tabid === "number") {
3683
+ await memory.setprogress(assigntasktab(await memory.getprogress(), plan.id, output.details.tabid, Date.now()));
3684
+ }
2407
3685
  const summary = output?.summary ?? "The page action returned no result.";
2408
3686
  const resolved = output?.details?.resolvedtarget;
2409
3687
  if (resolved) {
@@ -2418,6 +3696,8 @@ async function executestep(stepid) {
2418
3696
  const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
2419
3697
  const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
2420
3698
  await memory.setprogress(tracked);
3699
+ await updatetaskbadges(plan, tracked);
3700
+ await refreshbadge();
2421
3701
  if (iscomplete(tracked, plan) && plan.state === "approved") {
2422
3702
  const done = { ...plan, state: "completed", completedat: Date.now() };
2423
3703
  await memory.setplan(done);
@@ -2506,10 +3786,31 @@ async function handlerequest(message, sender) {
2506
3786
  const navcontrol = await memory.getnavcontrol();
2507
3787
  const navqueues = await memory.getnavqueues();
2508
3788
  const artifacts = await memory.getartifacts();
3789
+ const tabs = await livetabs().catch(() => []);
3790
+ const windows = await livewindows().catch(() => []);
3791
+ const layouts = await memory.getlayouts();
3792
+ const tabgroups = await memory.gettabgroups();
3793
+ const tabmetas = await memory.gettabmetas();
3794
+ const badges = await memory.getbadges();
3795
+ const snapshots = await memory.getsnapshots();
3796
+ const closedtabs = await memory.getclosedtabs();
3797
+ const controltab = await memory.getcontroltab();
3798
+ const tabwatchevents = await memory.gettabwatchevents();
3799
+ const profiles = await memory.getprofiles();
3800
+ const tickets = await memory.gettickets();
3801
+ const wizards = await memory.getwizards();
3802
+ const picks = await memory.getpicks();
3803
+ const errorreports = await memory.geterrorreports();
3804
+ const captchas = await memory.getcaptchas();
3805
+ const detections = await memory.getdetections();
3806
+ const codeentry = await memory.getcodevalue();
3807
+ const clones = clonetabs(tabs);
3808
+ const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
3809
+ const report = await buildtabreport(tabs);
2509
3810
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
2510
3811
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
2511
3812
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
2512
- 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 };
3813
+ 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 } : {} };
2513
3814
  }
2514
3815
  case "capabilities":
2515
3816
  return refreshcapabilities();
@@ -2607,6 +3908,184 @@ async function handlerequest(message, sender) {
2607
3908
  if (!plan) throw new Error("No plan is available for a safety envelope.");
2608
3909
  return JSON.parse(safetyresponse({ verdicts: await memory.getsafeties(), plan }));
2609
3910
  }
3911
+ case "jumptotab": {
3912
+ const inputtab = message;
3913
+ if (typeof inputtab.tabid !== "number") throw new Error("A numeric tab id is required to jump.");
3914
+ await chrome.tabs.update(inputtab.tabid, { active: true }).catch(() => {
3915
+ throw new Error("The tab to jump to is no longer open.");
3916
+ });
3917
+ await audit("tab", `The review panel jumped to tab ${inputtab.tabid}.`);
3918
+ return { tabid: inputtab.tabid };
3919
+ }
3920
+ case "tabsearch": {
3921
+ const inputsearch = message;
3922
+ const granted = await chrome.permissions.contains({ permissions: ["tabs"] });
3923
+ if (!granted) throw new Error("The tabs capability has not been granted; request it from the review panel.");
3924
+ const matches = searchtabmatches(await livetabs(), inputsearch.text ?? "");
3925
+ await audit("tab", `The review panel searched the open tabs for "${inputsearch.text ?? ""}" and matched ${matches.length} tab${matches.length === 1 ? "" : "s"}.`);
3926
+ return { matches };
3927
+ }
3928
+ case "savelayout": {
3929
+ const session = await memory.getsession();
3930
+ const gate = layoutmutationgranted(session, Date.now());
3931
+ if (!gate.allowed) throw new Error(gate.reason);
3932
+ const inputlayout = message;
3933
+ if (!inputlayout.name?.trim()) throw new Error("A layout name is required.");
3934
+ const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
3935
+ const layout = buildlayout(inputlayout.name.trim(), tabs, windows, groups, scratch, Date.now());
3936
+ await memory.setlayout(layout);
3937
+ await audit("layout", `The review panel saved the tab layout ${layout.name} with ${layout.tabs.length} tabs and ${layout.windows.length} window bounds.`, { ...session ? { sessionid: session.id } : {} });
3938
+ return layout;
3939
+ }
3940
+ case "restorelayout": {
3941
+ const session = await memory.getsession();
3942
+ const gate = layoutmutationgranted(session, Date.now());
3943
+ if (!gate.allowed) throw new Error(gate.reason);
3944
+ const inputlayout = message;
3945
+ const layout = await memory.getlayout(inputlayout.name ?? "");
3946
+ if (!layout) throw new Error(`No tab layout named ${inputlayout.name ?? ""} is stored yet.`);
3947
+ const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
3948
+ const urls = layoutrestoreplan(layout, open);
3949
+ const opened = [];
3950
+ for (const url of urls) {
3951
+ const created = await chrome.tabs.create({ url, active: opened.length === 0 });
3952
+ opened.push(created?.id ?? 0);
3953
+ }
3954
+ await audit("layout", `The review panel restored the tab layout ${layout.name}: ${opened.length} tab${opened.length === 1 ? "" : "s"} reopened.`, { ...session ? { sessionid: session.id } : {} });
3955
+ return { name: layout.name, reopened: opened.length };
3956
+ }
3957
+ case "restoresnapshot": {
3958
+ const session = await memory.getsession();
3959
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Snapshot restore stays behind the consent gate of an active session.");
3960
+ const inputsnapshot = message;
3961
+ const snapshot2 = (await memory.getsnapshots()).find((item) => item.id === inputsnapshot.id);
3962
+ if (!snapshot2) throw new Error("No stored session snapshot matches the requested id.");
3963
+ const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
3964
+ const urls = layoutrestoreplan(snapshot2.layout, open);
3965
+ const opened = [];
3966
+ for (const url of urls) {
3967
+ const created = await chrome.tabs.create({ url, active: opened.length === 0 });
3968
+ opened.push(created?.id ?? 0);
3969
+ }
3970
+ await audit("layout", `The review panel restored the session snapshot ${snapshot2.id}: ${opened.length} tab${opened.length === 1 ? "" : "s"} reopened.`, { sessionid: session.id });
3971
+ return { id: snapshot2.id, reopened: opened.length };
3972
+ }
3973
+ case "controltab": {
3974
+ const inputcontrol = message;
3975
+ const state = await togglecontroltab(inputcontrol.enabled === true);
3976
+ const settings = await memory.getsettings();
3977
+ await memory.setsettings({ ...settings, controltab: state.enabled });
3978
+ return state;
3979
+ }
3980
+ case "settasktabceiling": {
3981
+ const inputceiling = message;
3982
+ const settings = await memory.getsettings();
3983
+ const ceiling = typeof inputceiling.ceiling === "number" && Number.isFinite(inputceiling.ceiling) && inputceiling.ceiling >= 0 ? inputceiling.ceiling : void 0;
3984
+ await memory.setsettings({ ...settings, ...ceiling !== void 0 ? { tasktabceiling: ceiling } : {} });
3985
+ await audit("configure", `The user set the concurrent task tab ceiling to ${ceiling === void 0 ? "no ceiling" : ceiling}; the value stays a user choice with no code cap.`);
3986
+ return { tasktabceiling: ceiling };
3987
+ }
3988
+ case "windowstate": {
3989
+ const inputwindow = message;
3990
+ if (typeof inputwindow.windowid !== "number" || !inputwindow.state || !["normal", "maximized", "minimized", "fullscreen"].includes(inputwindow.state)) throw new Error("A numeric window id and a known window state are required.");
3991
+ await chrome.windows.update(inputwindow.windowid, { state: inputwindow.state });
3992
+ await audit("window", `The popup set window ${inputwindow.windowid} to the ${inputwindow.state} state.`);
3993
+ return { windowid: inputwindow.windowid, state: inputwindow.state };
3994
+ }
3995
+ case "closewindow": {
3996
+ const inputclose = message;
3997
+ if (typeof inputclose.windowid !== "number") throw new Error("A numeric window id is required.");
3998
+ const session = await memory.getsession();
3999
+ const plan = await memory.getplan();
4000
+ const progress = plan ? await memory.getprogress() : void 0;
4001
+ const tasktabids = plan && progress ? trackedtasktabs(progress, plan.id) : [];
4002
+ const count = tasktabsinwindow(await livetabs(), inputclose.windowid, tasktabids);
4003
+ const gate = windowclosegate(count, inputclose.reviewed === true);
4004
+ if (!gate.allowed) throw new Error(gate.reason);
4005
+ await chrome.windows.remove(inputclose.windowid);
4006
+ await audit("window", `The review panel closed window ${inputclose.windowid}${count > 0 ? ` while holding ${count} task tab${count === 1 ? "" : "s"} under explicit review` : ""}.`, { ...session ? { sessionid: session.id } : {} });
4007
+ return { windowid: inputclose.windowid, closed: true };
4008
+ }
4009
+ case "applyprofile": {
4010
+ const session = await memory.getsession();
4011
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Profile application stays inside the consent gate of an active session.");
4012
+ const inputprofile = message;
4013
+ const profile = await memory.getprofile(inputprofile.name ?? "");
4014
+ if (!profile) throw new Error(`No form profile named ${inputprofile.name ?? ""} is stored yet.`);
4015
+ const gate = profilegrantgranted(profile, session.origin);
4016
+ if (!gate.allowed) throw new Error(gate.reason);
4017
+ await audit("fill", `Form profile ${profile.name} applied as the reviewed field map for ${session.origin} under its origin grants.`, { sessionid: session.id });
4018
+ return { profile: { name: profile.name, fields: profile.fields, grants: profile.grants } };
4019
+ }
4020
+ case "approvesubmit": {
4021
+ const inputticket = message;
4022
+ const tickets = await memory.gettickets();
4023
+ const ticket = tickets.find((item) => item.id === inputticket.id);
4024
+ if (!ticket) throw new Error("No submission ticket matches the requested id.");
4025
+ const updated = { ...ticket, approved: inputticket.approved !== false };
4026
+ await memory.setticket(updated);
4027
+ const session = await memory.getsession();
4028
+ await audit("submit", `Submission ticket ${ticket.id} for form ${ticket.form} ${updated.approved ? "approved" : "declined"} by the user with values hash ${ticket.valueshash}.`, { ...session ? { sessionid: session.id } : {} });
4029
+ await refreshbadge();
4030
+ return updated;
4031
+ }
4032
+ case "resolvecaptcha": {
4033
+ const open = (await memory.getcaptchas()).find((item) => !item.resolved);
4034
+ if (!open) throw new Error("No open captcha handoff exists.");
4035
+ await memory.resolvecaptcha(open.id, Date.now());
4036
+ const session = await memory.getsession();
4037
+ if (session?.pausedat && !session.stoppedat) {
4038
+ const resumed = { id: session.id, tabid: session.tabid, origin: session.origin, startedat: session.startedat, expiresat: session.expiresat, ...session.grants ? { grants: session.grants } : {} };
4039
+ await memory.setsession(resumed);
4040
+ }
4041
+ await audit("handoff", `Captcha handoff ${open.id} resolved by the user after ${Date.now() - open.openedat} milliseconds; the plan continues.`, { ...session ? { sessionid: session.id } : {} });
4042
+ await refreshbadge();
4043
+ return { resolved: true, id: open.id };
4044
+ }
4045
+ case "storecode": {
4046
+ const session = await memory.getsession();
4047
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("The one time code entry stays behind the consent gate of an active session.");
4048
+ const inputcode = message;
4049
+ if (!inputcode.code?.trim()) throw new Error("A non-empty one time code is required.");
4050
+ await memory.setcodevalue(inputcode.code.trim());
4051
+ await audit("consent", `A one time code was stored behind the consent gate of the active session; the value never appears in the audit trail.`, { sessionid: session.id });
4052
+ return { stored: true };
4053
+ }
4054
+ case "regeneratevalue": {
4055
+ const inputvalue = message;
4056
+ if (typeof inputvalue.field !== "string" || !inputvalue.field) throw new Error("A field kind is required to regenerate a value.");
4057
+ const rule = {};
4058
+ if (typeof inputvalue.locale === "string" && inputvalue.locale) rule.locale = inputvalue.locale;
4059
+ if (typeof inputvalue.seed === "number" && Number.isFinite(inputvalue.seed)) rule.seed = inputvalue.seed;
4060
+ const value = generatevalue(inputvalue.field, rule);
4061
+ const verdict = generatedvalueallowed(value);
4062
+ if (!verdict.allowed) throw new Error(verdict.reason ?? "The regenerated value was refused.");
4063
+ return { kind: inputvalue.field, value, locale: rule.locale ?? "en", seed: rule.seed ?? 1 };
4064
+ }
4065
+ case "removeprofile": {
4066
+ const inputprofile = message;
4067
+ const profile = await memory.getprofile(inputprofile.name ?? "");
4068
+ if (!profile) throw new Error(`No form profile named ${inputprofile.name ?? ""} is stored yet.`);
4069
+ await memory.removeprofile(profile.name);
4070
+ const session = await memory.getsession();
4071
+ await audit("fill", `Form profile ${profile.name} removed from local memory by the user.`, { ...session ? { sessionid: session.id } : {} });
4072
+ return { removed: true, name: profile.name };
4073
+ }
4074
+ case "formreport": {
4075
+ const plan = await memory.getplan();
4076
+ if (!plan) throw new Error("No plan is available for a form report envelope.");
4077
+ const outcome = (await memory.getoutcomes()).find((candidate) => plan.steps.some((step) => step.id === candidate.stepid && step.kind === "detectfields"));
4078
+ const report = outcome?.details?.report;
4079
+ if (!report) throw new Error("No form report has been captured yet.");
4080
+ return JSON.parse(formreportresponse({ report, plan }));
4081
+ }
4082
+ case "errorreport": {
4083
+ const plan = await memory.getplan();
4084
+ if (!plan) throw new Error("No plan is available for an error report envelope.");
4085
+ const report = (await memory.geterrorreports())[0];
4086
+ if (!report) throw new Error("No error report has been collected yet.");
4087
+ return JSON.parse(errorreportresponse({ report, plan }));
4088
+ }
2610
4089
  case "stop": {
2611
4090
  const session = await memory.getsession();
2612
4091
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });