@wenathlan/extension 1.1.35 → 1.1.36
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.
- package/README.md +6 -5
- package/dist/index.js +222 -5
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +39 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +13 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +15 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +128 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +883 -13
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +2 -2
- package/extension/dist/pagebridge.js.map +2 -2
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +23 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +208 -1
- package/extension/dist/sidepanel.js.map +3 -3
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -352,19 +352,105 @@ 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
|
+
}
|
|
355
438
|
};
|
|
356
439
|
function randomid() {
|
|
357
440
|
return crypto.randomUUID();
|
|
358
441
|
}
|
|
359
442
|
|
|
360
443
|
// 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"]);
|
|
444
|
+
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"]);
|
|
362
445
|
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"]);
|
|
446
|
+
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"]);
|
|
364
447
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
365
|
-
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
|
|
448
|
+
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
366
449
|
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"]);
|
|
450
|
+
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"]);
|
|
451
|
+
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"]);
|
|
452
|
+
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
453
|
+
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
368
454
|
function normalizeendpoint(value) {
|
|
369
455
|
const endpoint = new URL(value.trim());
|
|
370
456
|
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
@@ -397,8 +483,27 @@ function requiredcapability(kind) {
|
|
|
397
483
|
if (kind === "downloadfile") return "downloads";
|
|
398
484
|
if (kind === "openclipboard") return "clipboardRead";
|
|
399
485
|
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
486
|
+
if (tabscommandactions.has(kind)) return "tabs";
|
|
400
487
|
return void 0;
|
|
401
488
|
}
|
|
489
|
+
function istabscommandkind(kind) {
|
|
490
|
+
return tabscommandactions.has(kind);
|
|
491
|
+
}
|
|
492
|
+
function islayoutkind(kind) {
|
|
493
|
+
return layoutmutationactions.has(kind);
|
|
494
|
+
}
|
|
495
|
+
function layoutmutationgranted(session, now) {
|
|
496
|
+
if (!session || session.stoppedat || session.expiresat <= now) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
|
|
497
|
+
return { allowed: true };
|
|
498
|
+
}
|
|
499
|
+
function windowclosegate(tasktabcount, reviewed) {
|
|
500
|
+
if (tasktabcount > 1 && !reviewed) return { allowed: false, reason: `The window holds ${tasktabcount} task tabs and needs explicit review before it closes.` };
|
|
501
|
+
return { allowed: true };
|
|
502
|
+
}
|
|
503
|
+
function tasktabceiling(settings) {
|
|
504
|
+
const ceiling = settings?.tasktabceiling;
|
|
505
|
+
return typeof ceiling === "number" && Number.isFinite(ceiling) && ceiling >= 0 ? ceiling : void 0;
|
|
506
|
+
}
|
|
402
507
|
function waitduration(step) {
|
|
403
508
|
const requested = step.value ? Number.parseInt(step.value, 10) : 250;
|
|
404
509
|
if (!Number.isFinite(requested) || requested < 0) throw new Error("Wait duration must be zero or a positive number of milliseconds.");
|
|
@@ -561,6 +666,104 @@ function validateratelimit(value) {
|
|
|
561
666
|
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
667
|
return { allowed: true };
|
|
563
668
|
}
|
|
669
|
+
function validatetabquery(value) {
|
|
670
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed tabquery with at least one matcher is required in options." };
|
|
671
|
+
const query = value;
|
|
672
|
+
const hasmatcher = query.url !== void 0 || query.title !== void 0 || query.id !== void 0 || query.pattern !== void 0;
|
|
673
|
+
if (!hasmatcher) return { allowed: false, reason: "The reviewed tabquery needs a url, title, id or pattern matcher." };
|
|
674
|
+
if (query.url !== void 0 && !isnonempty(query.url)) return { allowed: false, reason: "The reviewed tabquery url matcher must be a non-empty string." };
|
|
675
|
+
if (query.title !== void 0 && !isnonempty(query.title)) return { allowed: false, reason: "The reviewed tabquery title matcher must be a non-empty string." };
|
|
676
|
+
if (query.pattern !== void 0 && !isnonempty(query.pattern)) return { allowed: false, reason: "The reviewed tabquery pattern matcher must be a non-empty string." };
|
|
677
|
+
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." };
|
|
678
|
+
return { allowed: true };
|
|
679
|
+
}
|
|
680
|
+
function validategroupcolor(value) {
|
|
681
|
+
return typeof value === "string" && groupcolors.includes(value);
|
|
682
|
+
}
|
|
683
|
+
function validateidlist(options, key) {
|
|
684
|
+
const ids = options[key];
|
|
685
|
+
return Array.isArray(ids) && ids.length > 0 && ids.every((id) => typeof id === "number" && Number.isInteger(id) && id >= 0);
|
|
686
|
+
}
|
|
687
|
+
function validatetabsgrammar(step, options) {
|
|
688
|
+
const kind = step.kind;
|
|
689
|
+
if (kind === "querytabs" || kind === "closepattern") {
|
|
690
|
+
const querycheck = validatetabquery(options.tabquery);
|
|
691
|
+
if (!querycheck.allowed) return querycheck;
|
|
692
|
+
if (kind === "closepattern" && options.reviewed !== true) return { allowed: false, reason: "The close pattern needs the explicit reviewed flag before any tab closes." };
|
|
693
|
+
}
|
|
694
|
+
if (kind === "duplicatetab" || kind === "pintab" || kind === "mutetab" || kind === "movetab" || kind === "movetabwindow" || kind === "badgetab" || kind === "attachmeta") {
|
|
695
|
+
if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser tab id is required." };
|
|
696
|
+
}
|
|
697
|
+
if (kind === "focuswindow" || kind === "maximizewindow" || kind === "minimizewindow" || kind === "restorewindow") {
|
|
698
|
+
if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser window id is required." };
|
|
699
|
+
}
|
|
700
|
+
if (kind === "pintab" && typeof options.pinned !== "boolean") return { allowed: false, reason: "A reviewed pinned flag is required in options." };
|
|
701
|
+
if (kind === "mutetab" && typeof options.muted !== "boolean") return { allowed: false, reason: "A reviewed muted flag is required in options." };
|
|
702
|
+
if (kind === "movetab") {
|
|
703
|
+
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." };
|
|
704
|
+
}
|
|
705
|
+
if (kind === "movetabwindow") {
|
|
706
|
+
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." };
|
|
707
|
+
}
|
|
708
|
+
if (kind === "grouptabs") {
|
|
709
|
+
const group = options.group;
|
|
710
|
+
if (!group || typeof group !== "object" || Array.isArray(group)) return { allowed: false, reason: "A reviewed group with a name is required in options." };
|
|
711
|
+
const spec = group;
|
|
712
|
+
if (!isnonempty(spec.name)) return { allowed: false, reason: "The reviewed group needs a non-empty name." };
|
|
713
|
+
if (!validategroupcolor(spec.color)) return { allowed: false, reason: "The reviewed group color must be a Chromium tab group color." };
|
|
714
|
+
if (!validateidlist(spec, "tabids")) return { allowed: false, reason: "The reviewed group needs a non-empty list of member tab ids." };
|
|
715
|
+
}
|
|
716
|
+
if (kind === "colorgroup") {
|
|
717
|
+
if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed group name is required in options." };
|
|
718
|
+
if (!validategroupcolor(options.color)) return { allowed: false, reason: "The reviewed group color must be a Chromium tab group color." };
|
|
719
|
+
}
|
|
720
|
+
if (kind === "collapsegroup") {
|
|
721
|
+
if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed group name is required in options." };
|
|
722
|
+
if (typeof options.collapsed !== "boolean") return { allowed: false, reason: "A reviewed collapsed flag is required in options." };
|
|
723
|
+
}
|
|
724
|
+
if (kind === "discardtab" || kind === "reloadtabs") {
|
|
725
|
+
if (!isnumericid(step.value) && !validateidlist(options, "tabs")) return { allowed: false, reason: "A numeric tab id or a reviewed list of tab ids is required." };
|
|
726
|
+
}
|
|
727
|
+
if (kind === "zoomin" || kind === "zoomout") {
|
|
728
|
+
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." };
|
|
729
|
+
if (step.value !== void 0 && step.value !== "" && !isnumericid(step.value)) return { allowed: false, reason: "The reviewed zoom target must be a numeric tab id." };
|
|
730
|
+
}
|
|
731
|
+
if (kind === "switchtab") {
|
|
732
|
+
if (options.direction !== "next" && options.direction !== "previous") return { allowed: false, reason: "A reviewed switch direction of next or previous is required in options." };
|
|
733
|
+
}
|
|
734
|
+
if (kind === "restorewindow") {
|
|
735
|
+
const bounds = options.bounds;
|
|
736
|
+
if (bounds !== void 0) {
|
|
737
|
+
if (!bounds || typeof bounds !== "object" || Array.isArray(bounds)) return { allowed: false, reason: "The reviewed window bounds must be an object." };
|
|
738
|
+
const shape = bounds;
|
|
739
|
+
for (const field of ["left", "top", "width", "height"]) {
|
|
740
|
+
if (typeof shape[field] !== "number" || !Number.isFinite(shape[field])) return { allowed: false, reason: "The reviewed window bounds need numeric left, top, width and height." };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (kind === "scratchwindow") {
|
|
745
|
+
if (step.value !== void 0 && step.value !== "" && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed scratch window url must use HTTPS." };
|
|
746
|
+
}
|
|
747
|
+
if (kind === "incognitowindow" && !ishttpsurl(step.value)) return { allowed: false, reason: "A reviewed HTTPS url is required to open an incognito window." };
|
|
748
|
+
if (kind === "restoretab" && step.value !== void 0 && step.value !== "" && !ishttpsurl(step.value)) return { allowed: false, reason: "The reviewed restore url must use HTTPS." };
|
|
749
|
+
if (kind === "savelayout" || kind === "restorelayout") {
|
|
750
|
+
if (!isnonempty(options.name)) return { allowed: false, reason: "A reviewed layout name is required in options." };
|
|
751
|
+
}
|
|
752
|
+
if (kind === "badgetab") {
|
|
753
|
+
if (!isnonempty(options.label)) return { allowed: false, reason: "A reviewed badge label is required in options." };
|
|
754
|
+
if (options.taskid !== void 0 && !isnonempty(options.taskid)) return { allowed: false, reason: "The reviewed badge task id must be a non-empty string." };
|
|
755
|
+
}
|
|
756
|
+
if (kind === "attachmeta") {
|
|
757
|
+
const labels = options.labels;
|
|
758
|
+
const taskrefs = options.taskrefs;
|
|
759
|
+
const haslabels = Array.isArray(labels) && labels.length > 0 && labels.every((label) => isnonempty(label));
|
|
760
|
+
const hastaskrefs = Array.isArray(taskrefs) && taskrefs.length > 0 && taskrefs.every((ref) => isnonempty(ref));
|
|
761
|
+
if (!haslabels && !hastaskrefs) return { allowed: false, reason: "Reviewed labels or task refs are required in options to attach metadata." };
|
|
762
|
+
if (options.provenance !== void 0 && !isnonempty(options.provenance)) return { allowed: false, reason: "The reviewed provenance must be a non-empty string." };
|
|
763
|
+
}
|
|
764
|
+
if (kind === "reopenrun" && !isnonempty(options.run)) return { allowed: false, reason: "A reviewed run id is required in options to reopen its tabs." };
|
|
765
|
+
return { allowed: true };
|
|
766
|
+
}
|
|
564
767
|
function validatestep(step, origin) {
|
|
565
768
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
566
769
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
@@ -762,6 +965,20 @@ function validatestep(step, origin) {
|
|
|
762
965
|
const listcheck = validateurllist(options, "urls");
|
|
763
966
|
if (!listcheck.allowed) return listcheck;
|
|
764
967
|
}
|
|
968
|
+
if (istabscommandkind(step.kind)) {
|
|
969
|
+
const tabscheck = validatetabsgrammar(step, options);
|
|
970
|
+
if (!tabscheck.allowed) return tabscheck;
|
|
971
|
+
}
|
|
972
|
+
if (step.kind === "tabcreate") {
|
|
973
|
+
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
974
|
+
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." };
|
|
975
|
+
}
|
|
976
|
+
if (step.kind === "windowcreate") {
|
|
977
|
+
for (const field of ["left", "top", "width", "height"]) {
|
|
978
|
+
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.` };
|
|
979
|
+
}
|
|
980
|
+
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." };
|
|
981
|
+
}
|
|
765
982
|
return { allowed: true };
|
|
766
983
|
}
|
|
767
984
|
function sessiongate(input) {
|
|
@@ -792,6 +1009,7 @@ function canexecute(input) {
|
|
|
792
1009
|
if (!navigation.allowed) return navigation;
|
|
793
1010
|
}
|
|
794
1011
|
}
|
|
1012
|
+
if (islayoutkind(input.step.kind) && !layoutmutationgranted(input.session, now).allowed) return { allowed: false, reason: "Group and layout mutations stay inside the active session." };
|
|
795
1013
|
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
1014
|
let options = {};
|
|
797
1015
|
try {
|
|
@@ -864,9 +1082,18 @@ function recordnaventry(progress, planid, stepid, entry, now) {
|
|
|
864
1082
|
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
1083
|
return recordoutcome(base, planid, outcome, now);
|
|
866
1084
|
}
|
|
1085
|
+
function assigntasktab(progress, planid, tabid2, now) {
|
|
1086
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1087
|
+
if ((base.tasktabs ?? []).includes(tabid2)) return { ...base, updatedat: now };
|
|
1088
|
+
return { ...base, tasktabs: [...base.tasktabs ?? [], tabid2], updatedat: now };
|
|
1089
|
+
}
|
|
1090
|
+
function tasktabs(progress, planid) {
|
|
1091
|
+
if (!progress || progress.planid !== planid) return [];
|
|
1092
|
+
return progress.tasktabs ?? [];
|
|
1093
|
+
}
|
|
867
1094
|
|
|
868
1095
|
// version.ts
|
|
869
|
-
var packageversion = "1.1.
|
|
1096
|
+
var packageversion = "1.1.36";
|
|
870
1097
|
|
|
871
1098
|
// types.ts
|
|
872
1099
|
var protocolversion = packageversion;
|
|
@@ -955,6 +1182,9 @@ function trailreport(input) {
|
|
|
955
1182
|
function safetyresponse(input) {
|
|
956
1183
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, verdicts: input.verdicts });
|
|
957
1184
|
}
|
|
1185
|
+
function layoutreport(input) {
|
|
1186
|
+
return { version: protocolversion, layouts: input.layouts };
|
|
1187
|
+
}
|
|
958
1188
|
|
|
959
1189
|
// extension/browsertabs.ts
|
|
960
1190
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -990,8 +1220,9 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
990
1220
|
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
1221
|
}
|
|
992
1222
|
case "tabcreate": {
|
|
993
|
-
const
|
|
994
|
-
|
|
1223
|
+
const targetwindow = typeof options.window === "number" && Number.isFinite(options.window) ? options.window : void 0;
|
|
1224
|
+
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 } : {} });
|
|
1225
|
+
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
1226
|
}
|
|
996
1227
|
case "tabactivate": {
|
|
997
1228
|
await chrome.tabs.update(tabid(step), { active: true });
|
|
@@ -1014,8 +1245,11 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
1014
1245
|
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
1246
|
}
|
|
1016
1247
|
case "windowcreate": {
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1248
|
+
const bounds = ["left", "top", "width", "height"].filter((field) => typeof options[field] === "number");
|
|
1249
|
+
const geometry = Object.fromEntries(bounds.map((field) => [field, options[field]]));
|
|
1250
|
+
const state = typeof options.state === "string" && ["normal", "maximized", "minimized", "fullscreen"].includes(options.state) ? options.state : void 0;
|
|
1251
|
+
const created = await chrome.windows.create({ url: step.value ?? "about:blank", ...Object.keys(geometry).length > 0 ? geometry : {}, ...state !== void 0 ? { state } : {} });
|
|
1252
|
+
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
1253
|
}
|
|
1020
1254
|
case "windowclose": {
|
|
1021
1255
|
await chrome.windows.remove(tabid(step));
|
|
@@ -1039,6 +1273,125 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
1039
1273
|
}
|
|
1040
1274
|
}
|
|
1041
1275
|
|
|
1276
|
+
// extension/tabscommand.ts
|
|
1277
|
+
function parsetabquery(step) {
|
|
1278
|
+
let options = {};
|
|
1279
|
+
try {
|
|
1280
|
+
options = parseoptions(step);
|
|
1281
|
+
} catch {
|
|
1282
|
+
options = {};
|
|
1283
|
+
}
|
|
1284
|
+
const value = options.tabquery;
|
|
1285
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1286
|
+
const query = value;
|
|
1287
|
+
return {
|
|
1288
|
+
...typeof query.url === "string" && query.url ? { url: query.url } : {},
|
|
1289
|
+
...typeof query.title === "string" && query.title ? { title: query.title } : {},
|
|
1290
|
+
...typeof query.id === "number" && Number.isInteger(query.id) && query.id >= 0 ? { id: query.id } : {},
|
|
1291
|
+
...typeof query.pattern === "string" && query.pattern ? { pattern: query.pattern } : {}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function tabpatternmatches(pattern, url) {
|
|
1295
|
+
const source = pattern.split("**").map((part) => part.split("*").map((piece) => piece.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")).join(".*");
|
|
1296
|
+
return new RegExp(`^${source}$`).test(url);
|
|
1297
|
+
}
|
|
1298
|
+
function querymatches(query, tabs) {
|
|
1299
|
+
return tabs.filter((tab) => {
|
|
1300
|
+
if (query.id !== void 0 && tab.tabid !== query.id) return false;
|
|
1301
|
+
if (query.url !== void 0 && tab.url !== query.url) return false;
|
|
1302
|
+
if (query.title !== void 0 && !tab.title.toLowerCase().includes(query.title.toLowerCase())) return false;
|
|
1303
|
+
if (query.pattern !== void 0 && !tabpatternmatches(query.pattern, tab.url)) return false;
|
|
1304
|
+
return true;
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
function normalizedtaburl(url) {
|
|
1308
|
+
let normalized = url;
|
|
1309
|
+
const hash = normalized.indexOf("#");
|
|
1310
|
+
if (hash >= 0) normalized = normalized.slice(0, hash);
|
|
1311
|
+
while (normalized.length > 1 && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
1312
|
+
return normalized;
|
|
1313
|
+
}
|
|
1314
|
+
function clonetabs(tabs) {
|
|
1315
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1316
|
+
for (const tab of tabs) {
|
|
1317
|
+
if (!tab.url) continue;
|
|
1318
|
+
const key = normalizedtaburl(tab.url);
|
|
1319
|
+
groups.set(key, [...groups.get(key) ?? [], tab.tabid]);
|
|
1320
|
+
}
|
|
1321
|
+
return [...groups.entries()].filter(([, tabids]) => tabids.length > 1).map(([url, tabids]) => ({ url, tabids }));
|
|
1322
|
+
}
|
|
1323
|
+
function searchtabmatches(tabs, text2) {
|
|
1324
|
+
const needle = text2.trim().toLowerCase();
|
|
1325
|
+
if (!needle) return [];
|
|
1326
|
+
return tabs.filter((tab) => tab.title.toLowerCase().includes(needle) || tab.url.toLowerCase().includes(needle));
|
|
1327
|
+
}
|
|
1328
|
+
function audiotabs(tabs) {
|
|
1329
|
+
return tabs.filter((tab) => tab.audible || tab.muted && tab.audible);
|
|
1330
|
+
}
|
|
1331
|
+
function discardcandidates(tabs) {
|
|
1332
|
+
return tabs.filter((tab) => !tab.active && !tab.pinned && !tab.discarded && tab.url.length > 0);
|
|
1333
|
+
}
|
|
1334
|
+
function buildlayout(name, tabs, windows, groups, scratchwindowids, at) {
|
|
1335
|
+
return {
|
|
1336
|
+
name,
|
|
1337
|
+
tabs: tabs.map((tab) => ({ url: tab.url, title: tab.title, pinned: tab.pinned, index: tab.index, windowid: tab.windowid })),
|
|
1338
|
+
groups: groups.map((group) => ({ name: group.name, color: group.color, tabids: group.tabids.filter((tabid2) => tabs.some((tab) => tab.tabid === tabid2)), collapsed: group.collapsed })),
|
|
1339
|
+
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" } })),
|
|
1340
|
+
savedat: at
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function layoutrestoreplan(layout, openurls) {
|
|
1344
|
+
const open = new Set(openurls.map((url) => normalizedtaburl(url)));
|
|
1345
|
+
return layout.tabs.map((tab) => tab.url).filter((url) => url.length > 0 && !open.has(normalizedtaburl(url)));
|
|
1346
|
+
}
|
|
1347
|
+
function regroupaftermoves(groups, tabs, at) {
|
|
1348
|
+
const order = new Map(tabs.map((tab) => [tab.tabid, tab.index]));
|
|
1349
|
+
return groups.map((group) => {
|
|
1350
|
+
const members = group.tabids.filter((tabid2) => order.has(tabid2));
|
|
1351
|
+
if (members.length === 0) return group;
|
|
1352
|
+
const ordered = [...members].sort((left, right) => (order.get(left) ?? 0) - (order.get(right) ?? 0));
|
|
1353
|
+
return ordered.length === group.tabids.length && ordered.every((tabid2, index) => tabid2 === group.tabids[index]) ? group : { ...group, tabids: ordered, savedat: at };
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
function tasktabsinwindow(tabs, windowid, tasktabids) {
|
|
1357
|
+
const tasks = new Set(tasktabids);
|
|
1358
|
+
return tabs.filter((tab) => tab.windowid === windowid && tasks.has(tab.tabid)).length;
|
|
1359
|
+
}
|
|
1360
|
+
function closeselection(query, tabs, sessiontabid) {
|
|
1361
|
+
const matches = querymatches(query, tabs);
|
|
1362
|
+
return {
|
|
1363
|
+
targets: matches.filter((tab) => tab.tabid !== sessiontabid),
|
|
1364
|
+
refused: matches.filter((tab) => tab.tabid === sessiontabid)
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function zoomstep(current, direction, step) {
|
|
1368
|
+
const next = direction === "in" ? current + step : current - step;
|
|
1369
|
+
return next > 0 ? Number(next.toFixed(4)) : current;
|
|
1370
|
+
}
|
|
1371
|
+
function switchtarget(tabs, direction, currentindex) {
|
|
1372
|
+
if (tabs.length === 0) return void 0;
|
|
1373
|
+
const offset = direction === "next" ? 1 : -1;
|
|
1374
|
+
return (currentindex + offset + tabs.length) % tabs.length;
|
|
1375
|
+
}
|
|
1376
|
+
function watchtabdispatch(events, watchid, filters) {
|
|
1377
|
+
const allowed = filters.length > 0 ? new Set(filters) : void 0;
|
|
1378
|
+
return events.filter((event) => event.watchid === watchid && (allowed === void 0 || allowed.has(event.event)));
|
|
1379
|
+
}
|
|
1380
|
+
function badgefromprogress(completed, total) {
|
|
1381
|
+
if (total <= 0) return { label: "idle", done: false };
|
|
1382
|
+
if (completed >= total) return { label: "done", done: true };
|
|
1383
|
+
return { label: `${completed}/${total}`, done: false };
|
|
1384
|
+
}
|
|
1385
|
+
function tasktabgauge(used, ceiling) {
|
|
1386
|
+
return { used, ceiling, over: ceiling !== void 0 && used > ceiling };
|
|
1387
|
+
}
|
|
1388
|
+
function windowprofilegrants(profile) {
|
|
1389
|
+
return profile !== "incognito";
|
|
1390
|
+
}
|
|
1391
|
+
function trackedtasktabs(progress, planid) {
|
|
1392
|
+
return tasktabs(progress, planid);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1042
1395
|
// extension/pagedialogs.ts
|
|
1043
1396
|
function parsedialogpolicy(step) {
|
|
1044
1397
|
let options = {};
|
|
@@ -1661,6 +2014,15 @@ function browserauditkind(step) {
|
|
|
1661
2014
|
}
|
|
1662
2015
|
function stepauditkind(step, ok) {
|
|
1663
2016
|
if (isbrowserkind(step.kind)) return browserauditkind(step);
|
|
2017
|
+
if (istabscommandkind(step.kind)) {
|
|
2018
|
+
if (step.kind === "grouptabs" || step.kind === "colorgroup" || step.kind === "collapsegroup") return "group";
|
|
2019
|
+
if (step.kind === "savelayout" || step.kind === "restorelayout" || step.kind === "snapshotsession" || step.kind === "reopenrun") return "layout";
|
|
2020
|
+
if (step.kind === "discardtab") return "discard";
|
|
2021
|
+
if (step.kind === "badgetab") return "badge";
|
|
2022
|
+
if (step.kind === "watchtab") return "watch";
|
|
2023
|
+
if (step.kind === "maximizewindow" || step.kind === "minimizewindow" || step.kind === "restorewindow" || step.kind === "focuswindow" || step.kind === "scratchwindow" || step.kind === "incognitowindow") return "window";
|
|
2024
|
+
return "tab";
|
|
2025
|
+
}
|
|
1664
2026
|
if (step.kind === "dismissdialog") return "dialog";
|
|
1665
2027
|
if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
|
|
1666
2028
|
if (step.kind === "retryaction") return "retry";
|
|
@@ -1900,6 +2262,10 @@ async function tracktabupdate(tabid2, changeinfo) {
|
|
|
1900
2262
|
const now = Date.now();
|
|
1901
2263
|
const url = typeof changeinfo.url === "string" ? changeinfo.url : void 0;
|
|
1902
2264
|
const status = changeinfo.status;
|
|
2265
|
+
if (typeof changeinfo.title === "string" && changeinfo.title) {
|
|
2266
|
+
lastknowntitles.set(tabid2, changeinfo.title);
|
|
2267
|
+
await recordtabwatchevent("title", tabid2, changeinfo.title);
|
|
2268
|
+
}
|
|
1903
2269
|
const previous = lastknownurls.get(tabid2);
|
|
1904
2270
|
if (status === "loading" && url) {
|
|
1905
2271
|
navbuffers.set(tabid2, [{ event: "beforenavigate", url, timestamp: now }]);
|
|
@@ -1924,10 +2290,20 @@ async function tracktabupdate(tabid2, changeinfo) {
|
|
|
1924
2290
|
chrome.tabs.onUpdated.addListener((tabid2, changeinfo) => {
|
|
1925
2291
|
void tracktabupdate(tabid2, changeinfo);
|
|
1926
2292
|
});
|
|
2293
|
+
chrome.tabs.onActivated.addListener((activeinfo) => {
|
|
2294
|
+
void recordtabwatchevent("activated", activeinfo.tabId);
|
|
2295
|
+
});
|
|
1927
2296
|
chrome.tabs.onRemoved.addListener((tabid2) => {
|
|
1928
2297
|
const url = lastknownurls.get(tabid2);
|
|
1929
|
-
|
|
2298
|
+
const title = lastknowntitles.get(tabid2) ?? "";
|
|
2299
|
+
const windowid = 0;
|
|
2300
|
+
if (url) {
|
|
2301
|
+
void memory.addrecenttab({ url, tabid: tabid2, closedat: Date.now() });
|
|
2302
|
+
void memory.addclosedtab({ url, title, tabid: tabid2, windowid, closedat: Date.now() });
|
|
2303
|
+
}
|
|
2304
|
+
void recordtabwatchevent("closed", tabid2, url);
|
|
1930
2305
|
lastknownurls.delete(tabid2);
|
|
2306
|
+
lastknowntitles.delete(tabid2);
|
|
1931
2307
|
navbuffers.delete(tabid2);
|
|
1932
2308
|
});
|
|
1933
2309
|
async function recordnavigation(step, session, tabid2) {
|
|
@@ -2351,9 +2727,382 @@ async function executenavigationkind(step, session, plan, tabid2, origin) {
|
|
|
2351
2727
|
}
|
|
2352
2728
|
}
|
|
2353
2729
|
}
|
|
2730
|
+
async function livetabs() {
|
|
2731
|
+
const tabs = await chrome.tabs.query({}).catch(() => []);
|
|
2732
|
+
return tabs.map((tab) => ({
|
|
2733
|
+
tabid: tab.id ?? 0,
|
|
2734
|
+
url: tab.url ?? "",
|
|
2735
|
+
title: tab.title ?? "",
|
|
2736
|
+
index: tab.index,
|
|
2737
|
+
windowid: tab.windowId ?? 0,
|
|
2738
|
+
active: tab.active,
|
|
2739
|
+
pinned: tab.pinned,
|
|
2740
|
+
audible: tab.audible ?? false,
|
|
2741
|
+
muted: tab.mutedInfo?.muted ?? false,
|
|
2742
|
+
discarded: tab.discarded ?? false
|
|
2743
|
+
}));
|
|
2744
|
+
}
|
|
2745
|
+
async function livewindows() {
|
|
2746
|
+
const windows = await chrome.windows.getAll().catch(() => []);
|
|
2747
|
+
return windows.map((item) => ({
|
|
2748
|
+
windowid: item.id ?? 0,
|
|
2749
|
+
left: item.left ?? 0,
|
|
2750
|
+
top: item.top ?? 0,
|
|
2751
|
+
width: item.width ?? 0,
|
|
2752
|
+
height: item.height ?? 0,
|
|
2753
|
+
state: item.state === "maximized" || item.state === "minimized" || item.state === "fullscreen" ? item.state : "normal",
|
|
2754
|
+
incognito: item.incognito ?? false,
|
|
2755
|
+
focused: item.focused
|
|
2756
|
+
}));
|
|
2757
|
+
}
|
|
2758
|
+
var tabwatchbuffers = /* @__PURE__ */ new Map();
|
|
2759
|
+
var lastknowntitles = /* @__PURE__ */ new Map();
|
|
2760
|
+
async function recordtabwatchevent(event, tabid2, detail) {
|
|
2761
|
+
const now = Date.now();
|
|
2762
|
+
for (const watch of await memory.getwatches()) {
|
|
2763
|
+
if (watch.closedat !== void 0 || watch.kind !== "watchtab") continue;
|
|
2764
|
+
if (watchclosed(watch.startedat, watch.lifetime, now)) continue;
|
|
2765
|
+
if (watch.events.length > 0 && !watch.events.includes(event)) continue;
|
|
2766
|
+
const record2 = { watchid: watch.watchid, event, tabid: tabid2, ...detail !== void 0 ? { detail } : {}, at: now };
|
|
2767
|
+
await memory.addtabwatchevent(record2);
|
|
2768
|
+
tabwatchbuffers.set(watch.watchid, [...tabwatchbuffers.get(watch.watchid) ?? [], record2]);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
async function buildtabreport(matches) {
|
|
2772
|
+
const [groups, badges, metas] = await Promise.all([memory.gettabgroups(), memory.getbadges(), memory.gettabmetas()]);
|
|
2773
|
+
const metabytab = new Map(metas.map((meta) => [meta.tabid, meta]));
|
|
2774
|
+
const entries = matches.map((tab) => ({
|
|
2775
|
+
tabid: tab.tabid,
|
|
2776
|
+
url: tab.url,
|
|
2777
|
+
title: tab.title,
|
|
2778
|
+
index: tab.index,
|
|
2779
|
+
windowid: tab.windowid,
|
|
2780
|
+
active: tab.active,
|
|
2781
|
+
pinned: tab.pinned,
|
|
2782
|
+
audible: tab.audible,
|
|
2783
|
+
muted: tab.muted,
|
|
2784
|
+
discarded: tab.discarded,
|
|
2785
|
+
...metabytab.has(tab.tabid) ? { meta: metabytab.get(tab.tabid) } : {}
|
|
2786
|
+
}));
|
|
2787
|
+
return { matches: entries, groups: groups.map((group) => ({ name: group.name, color: group.color, tabids: group.tabids, collapsed: group.collapsed })), badges };
|
|
2788
|
+
}
|
|
2789
|
+
function commandtabids(step, options) {
|
|
2790
|
+
const listed = Array.isArray(options.tabs) ? options.tabs.filter((item) => typeof item === "number" && Number.isInteger(item) && item >= 0) : [];
|
|
2791
|
+
const single = step.value && /^\d+$/.test(step.value) ? [Number.parseInt(step.value, 10)] : [];
|
|
2792
|
+
return listed.length > 0 ? listed : single;
|
|
2793
|
+
}
|
|
2794
|
+
async function executetabscommand(step, session, plan, sessiontabid) {
|
|
2795
|
+
const options = stepoptions2(step);
|
|
2796
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
2797
|
+
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
2798
|
+
const layoutgate = layoutmutationgranted(session, Date.now());
|
|
2799
|
+
if (islayoutkind(step.kind) && !layoutgate.allowed) throw new Error(layoutgate.reason ?? "Group and layout mutations stay inside the active session.");
|
|
2800
|
+
switch (step.kind) {
|
|
2801
|
+
case "querytabs": {
|
|
2802
|
+
const query = parsetabquery(step);
|
|
2803
|
+
if (!query) throw new Error("A reviewed tabquery is required.");
|
|
2804
|
+
const matches = querymatches(query, await livetabs());
|
|
2805
|
+
const report = await buildtabreport(matches);
|
|
2806
|
+
await audit("tab", `Queried the live tab set and matched ${matches.length} tab${matches.length === 1 ? "" : "s"}.`, extra);
|
|
2807
|
+
return { ok: true, summary: `Matched ${matches.length} open tab${matches.length === 1 ? "" : "s"} by the reviewed tabquery.`, details: { report, matches: matches.length } };
|
|
2808
|
+
}
|
|
2809
|
+
case "duplicatetab": {
|
|
2810
|
+
const source = Number.parseInt(step.value ?? "", 10);
|
|
2811
|
+
const created = await chrome.tabs.duplicate(source);
|
|
2812
|
+
await audit("tab", `Duplicated tab ${source} with its history into tab ${created?.id ?? 0}.`, extra);
|
|
2813
|
+
return { ok: true, summary: `Duplicated tab ${source} with its history.`, details: { sourcetab: source, tabid: created?.id ?? 0 } };
|
|
2814
|
+
}
|
|
2815
|
+
case "closepattern": {
|
|
2816
|
+
const query = parsetabquery(step);
|
|
2817
|
+
if (!query) throw new Error("A reviewed tabquery is required.");
|
|
2818
|
+
const tabs = await livetabs();
|
|
2819
|
+
const selection = closeselection(query, tabs, session?.tabid ?? sessiontabid);
|
|
2820
|
+
if (selection.refused.length > 0) throw new Error("The close pattern matches the session tab itself; review the pattern so the session tab survives.");
|
|
2821
|
+
if (selection.targets.length === 0) return { ok: true, summary: "The reviewed close pattern matched no tab outside the session tab.", details: { closed: 0 } };
|
|
2822
|
+
for (const target of selection.targets) await chrome.tabs.remove(target.tabid).catch(() => void 0);
|
|
2823
|
+
await audit("tab", `Closed ${selection.targets.length} tab${selection.targets.length === 1 ? "" : "s"} matching the reviewed close pattern.`, extra);
|
|
2824
|
+
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) } };
|
|
2825
|
+
}
|
|
2826
|
+
case "pintab": {
|
|
2827
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2828
|
+
await chrome.tabs.update(target, { pinned: options.pinned === true });
|
|
2829
|
+
await audit("tab", `${options.pinned === true ? "Pinned" : "Unpinned"} tab ${target} by the reviewed flag.`, extra);
|
|
2830
|
+
return { ok: true, summary: `${options.pinned === true ? "Pinned" : "Unpinned"} tab ${target}.`, details: { tabid: target, pinned: options.pinned === true } };
|
|
2831
|
+
}
|
|
2832
|
+
case "mutetab": {
|
|
2833
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2834
|
+
await chrome.tabs.update(target, { muted: options.muted === true });
|
|
2835
|
+
await audit("tab", `${options.muted === true ? "Muted" : "Unmuted"} tab ${target} by the reviewed flag.`, extra);
|
|
2836
|
+
return { ok: true, summary: `${options.muted === true ? "Muted" : "Unmuted"} tab ${target}.`, details: { tabid: target, muted: options.muted === true } };
|
|
2837
|
+
}
|
|
2838
|
+
case "movetab": {
|
|
2839
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2840
|
+
await chrome.tabs.move(target, { index: options.index });
|
|
2841
|
+
const groups = regroupaftermoves(await memory.gettabgroups(), await livetabs(), Date.now());
|
|
2842
|
+
for (const group of groups) await memory.settabgroup(group);
|
|
2843
|
+
await audit("tab", `Moved tab ${target} to index ${options.index} inside its window; group membership is kept.`, extra);
|
|
2844
|
+
return { ok: true, summary: `Moved tab ${target} to index ${options.index}.`, details: { tabid: target, index: options.index } };
|
|
2845
|
+
}
|
|
2846
|
+
case "movetabwindow": {
|
|
2847
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
2848
|
+
await chrome.tabs.move(target, { windowId: options.windowid, index: -1 });
|
|
2849
|
+
const groups = regroupaftermoves(await memory.gettabgroups(), await livetabs(), Date.now());
|
|
2850
|
+
for (const group of groups) await memory.settabgroup(group);
|
|
2851
|
+
await audit("tab", `Moved tab ${target} across windows into window ${options.windowid}; group membership is kept.`, extra);
|
|
2852
|
+
return { ok: true, summary: `Moved tab ${target} into window ${options.windowid}.`, details: { tabid: target, windowid: options.windowid } };
|
|
2853
|
+
}
|
|
2854
|
+
case "grouptabs": {
|
|
2855
|
+
const group = options.group;
|
|
2856
|
+
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() };
|
|
2857
|
+
await memory.settabgroup(record2);
|
|
2858
|
+
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);
|
|
2859
|
+
return { ok: true, summary: `Grouped ${record2.tabids.length} tab${record2.tabids.length === 1 ? "" : "s"} under ${record2.name}.`, details: { group: record2 } };
|
|
2860
|
+
}
|
|
2861
|
+
case "colorgroup": {
|
|
2862
|
+
const groups = await memory.gettabgroups();
|
|
2863
|
+
const target = groups.find((group) => group.name === options.name);
|
|
2864
|
+
if (!target) throw new Error(`No tab group named ${options.name} is stored yet.`);
|
|
2865
|
+
const updated = { ...target, color: String(options.color), savedat: Date.now() };
|
|
2866
|
+
await memory.settabgroup(updated);
|
|
2867
|
+
await audit("group", `Set the color of tab group ${updated.name} to ${updated.color}.`, extra);
|
|
2868
|
+
return { ok: true, summary: `Set the color of group ${updated.name} to ${updated.color}.`, details: { group: updated } };
|
|
2869
|
+
}
|
|
2870
|
+
case "collapsegroup": {
|
|
2871
|
+
const groups = await memory.gettabgroups();
|
|
2872
|
+
const target = groups.find((group) => group.name === options.name);
|
|
2873
|
+
if (!target) throw new Error(`No tab group named ${options.name} is stored yet.`);
|
|
2874
|
+
const updated = { ...target, collapsed: options.collapsed === true, savedat: Date.now() };
|
|
2875
|
+
await memory.settabgroup(updated);
|
|
2876
|
+
await audit("group", `${updated.collapsed ? "Collapsed" : "Expanded"} tab group ${updated.name}.`, extra);
|
|
2877
|
+
return { ok: true, summary: `${updated.collapsed ? "Collapsed" : "Expanded"} group ${updated.name}.`, details: { group: updated } };
|
|
2878
|
+
}
|
|
2879
|
+
case "discardtab": {
|
|
2880
|
+
const ids = commandtabids(step, options);
|
|
2881
|
+
if (ids.length === 0) throw new Error("A numeric tab id or a reviewed list of tab ids is required.");
|
|
2882
|
+
const candidates = discardcandidates(await livetabs()).filter((tab) => ids.includes(tab.tabid));
|
|
2883
|
+
const discarded = [];
|
|
2884
|
+
const urls = [];
|
|
2885
|
+
for (const candidate of candidates) {
|
|
2886
|
+
const result = await chrome.tabs.discard(candidate.tabid).catch(() => void 0);
|
|
2887
|
+
if (result) {
|
|
2888
|
+
discarded.push(candidate.tabid);
|
|
2889
|
+
urls.push({ tabid: candidate.tabid, url: candidate.url });
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
await audit("discard", `Discarded ${discarded.length} inactive tab${discarded.length === 1 ? "" : "s"} to save memory; the urls survive for on demand restore.`, extra);
|
|
2893
|
+
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)) } };
|
|
2894
|
+
}
|
|
2895
|
+
case "reloadtabs": {
|
|
2896
|
+
const ids = commandtabids(step, options);
|
|
2897
|
+
if (ids.length === 0) throw new Error("A numeric tab id or a reviewed list of tab ids is required.");
|
|
2898
|
+
for (const id of ids) await chrome.tabs.reload(id).catch(() => void 0);
|
|
2899
|
+
await audit("tab", `Reloaded ${ids.length} reviewed tab${ids.length === 1 ? "" : "s"}.`, extra);
|
|
2900
|
+
return { ok: true, summary: `Reloaded ${ids.length} tab${ids.length === 1 ? "" : "s"}.`, details: { tabs: ids } };
|
|
2901
|
+
}
|
|
2902
|
+
case "zoomin":
|
|
2903
|
+
case "zoomout": {
|
|
2904
|
+
const target = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : sessiontabid;
|
|
2905
|
+
const current = await chrome.tabs.getZoom(target);
|
|
2906
|
+
const next = zoomstep(current, step.kind === "zoomin" ? "in" : "out", typeof options.step === "number" && options.step > 0 ? options.step : 0.1);
|
|
2907
|
+
await chrome.tabs.setZoom(target, next);
|
|
2908
|
+
await audit("tab", `Zoomed tab ${target} ${step.kind === "zoomin" ? "in" : "out"} from ${current} to ${next} by the reviewed step.`, extra);
|
|
2909
|
+
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 } };
|
|
2910
|
+
}
|
|
2911
|
+
case "switchtab": {
|
|
2912
|
+
const direction = options.direction === "previous" ? "previous" : "next";
|
|
2913
|
+
const focusedwindow = (await livewindows()).find((item) => item.focused)?.windowid ?? 0;
|
|
2914
|
+
const windowtabs = (await livetabs()).filter((tab) => tab.windowid === focusedwindow);
|
|
2915
|
+
const active = windowtabs.find((tab) => tab.active);
|
|
2916
|
+
const target = switchtarget(windowtabs, direction, active?.index ?? 0);
|
|
2917
|
+
const totab = windowtabs.find((tab) => tab.index === target);
|
|
2918
|
+
if (!totab) throw new Error("No neighbor tab is available to switch to.");
|
|
2919
|
+
await chrome.tabs.update(totab.tabid, { active: true });
|
|
2920
|
+
await audit("tab", `Switched to the ${direction} tab ${totab.tabid}.`, extra);
|
|
2921
|
+
return { ok: true, summary: `Switched to the ${direction} tab.`, details: { tabid: totab.tabid, direction } };
|
|
2922
|
+
}
|
|
2923
|
+
case "maximizewindow": {
|
|
2924
|
+
await chrome.windows.update(windowid, { state: "maximized" });
|
|
2925
|
+
await audit("window", `Maximized window ${windowid}.`, extra);
|
|
2926
|
+
return { ok: true, summary: `Maximized window ${windowid}.`, details: { windowid, state: "maximized" } };
|
|
2927
|
+
}
|
|
2928
|
+
case "minimizewindow": {
|
|
2929
|
+
await chrome.windows.update(windowid, { state: "minimized" });
|
|
2930
|
+
await audit("window", `Minimized window ${windowid}.`, extra);
|
|
2931
|
+
return { ok: true, summary: `Minimized window ${windowid}.`, details: { windowid, state: "minimized" } };
|
|
2932
|
+
}
|
|
2933
|
+
case "restorewindow": {
|
|
2934
|
+
const bounds = options.bounds;
|
|
2935
|
+
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 } : {} });
|
|
2936
|
+
await audit("window", `Restored window ${windowid} to its reviewed bounds.`, extra);
|
|
2937
|
+
return { ok: true, summary: `Restored window ${windowid} to its reviewed bounds.`, details: { windowid, bounds: bounds ?? null } };
|
|
2938
|
+
}
|
|
2939
|
+
case "focuswindow": {
|
|
2940
|
+
await chrome.windows.update(windowid, { focused: true });
|
|
2941
|
+
await audit("window", `Focused window ${windowid}.`, extra);
|
|
2942
|
+
return { ok: true, summary: `Focused window ${windowid}.`, details: { windowid } };
|
|
2943
|
+
}
|
|
2944
|
+
case "scratchwindow": {
|
|
2945
|
+
const url = typeof step.value === "string" && step.value ? step.value : "about:blank";
|
|
2946
|
+
const created = await chrome.windows.create({ url });
|
|
2947
|
+
const window2 = created?.id ?? 0;
|
|
2948
|
+
await memory.setscratchwindows([...await memory.getscratchwindows(), window2]);
|
|
2949
|
+
await audit("window", `Opened a scratch window ${window2} for split work.`, extra);
|
|
2950
|
+
return { ok: true, summary: `Opened a scratch window for split work.`, details: { windowid: window2, url } };
|
|
2951
|
+
}
|
|
2952
|
+
case "incognitowindow": {
|
|
2953
|
+
const created = await chrome.windows.create({ url: step.value ?? "", incognito: true });
|
|
2954
|
+
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);
|
|
2955
|
+
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") } };
|
|
2956
|
+
}
|
|
2957
|
+
case "restoretab": {
|
|
2958
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
2959
|
+
let url = step.value && /^https:\/\//.test(step.value) ? step.value : void 0;
|
|
2960
|
+
if (!url) {
|
|
2961
|
+
const closed = (await memory.getclosedtabs()).find((entry) => !open.includes(entry.url));
|
|
2962
|
+
if (!closed) throw new Error("No closed tab is available to restore from the session history.");
|
|
2963
|
+
url = closed.url;
|
|
2964
|
+
}
|
|
2965
|
+
const created = await chrome.tabs.create({ url, active: true });
|
|
2966
|
+
await audit("tab", `Restored the closed tab ${url} from the session history.`, extra);
|
|
2967
|
+
return { ok: true, summary: `Restored ${url} from the closed tab history.`, details: { url, tabid: created?.id ?? 0 } };
|
|
2968
|
+
}
|
|
2969
|
+
case "savelayout": {
|
|
2970
|
+
const name = typeof options.name === "string" ? options.name : "";
|
|
2971
|
+
const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
|
|
2972
|
+
const layout = buildlayout(name, tabs, windows, groups, scratch, Date.now());
|
|
2973
|
+
await memory.setlayout(layout);
|
|
2974
|
+
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);
|
|
2975
|
+
return { ok: true, summary: `Saved the tab layout ${name}.`, details: { layout } };
|
|
2976
|
+
}
|
|
2977
|
+
case "restorelayout": {
|
|
2978
|
+
const name = typeof options.name === "string" ? options.name : "";
|
|
2979
|
+
const layout = await memory.getlayout(name);
|
|
2980
|
+
if (!layout) throw new Error(`No tab layout named ${name} is stored yet.`);
|
|
2981
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
2982
|
+
const urls = layoutrestoreplan(layout, open);
|
|
2983
|
+
const created = [];
|
|
2984
|
+
for (const url of urls) {
|
|
2985
|
+
const tab = await chrome.tabs.create({ url, active: created.length === 0 });
|
|
2986
|
+
created.push(tab?.id ?? 0);
|
|
2987
|
+
}
|
|
2988
|
+
await audit("layout", `Restored the tab layout ${name}: ${created.length} tab${created.length === 1 ? "" : "s"} reopened, ${layout.tabs.length - created.length} already open.`, extra);
|
|
2989
|
+
return { ok: true, summary: `Restored the tab layout ${name}.`, details: { name, reopened: created.length, alreadyopen: layout.tabs.length - created.length, tabs: created } };
|
|
2990
|
+
}
|
|
2991
|
+
case "findclones": {
|
|
2992
|
+
const clones = clonetabs(await livetabs());
|
|
2993
|
+
await audit("tab", `Detected ${clones.length} duplicate url group${clones.length === 1 ? "" : "s"} across the open tabs.`, extra);
|
|
2994
|
+
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 } };
|
|
2995
|
+
}
|
|
2996
|
+
case "searchtabs": {
|
|
2997
|
+
const matches = searchtabmatches(await livetabs(), step.value ?? "");
|
|
2998
|
+
await audit("tab", `Searched the open tabs and matched ${matches.length} tab${matches.length === 1 ? "" : "s"} for "${step.value}".`, extra);
|
|
2999
|
+
return { ok: true, summary: `Matched ${matches.length} open tab${matches.length === 1 ? "" : "s"} for "${step.value}".`, details: { matches } };
|
|
3000
|
+
}
|
|
3001
|
+
case "badgetab": {
|
|
3002
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
3003
|
+
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() };
|
|
3004
|
+
await memory.setbadge(badge);
|
|
3005
|
+
await refreshbadge();
|
|
3006
|
+
await audit("badge", `Set the task badge of tab ${target} to ${badge.label} for task ${badge.taskid}.`, extra);
|
|
3007
|
+
return { ok: true, summary: `Set the badge of tab ${target} to ${badge.label}.`, details: { badge } };
|
|
3008
|
+
}
|
|
3009
|
+
case "attachmeta": {
|
|
3010
|
+
const target = Number.parseInt(step.value ?? "", 10);
|
|
3011
|
+
const meta = {
|
|
3012
|
+
tabid: target,
|
|
3013
|
+
taskrefs: Array.isArray(options.taskrefs) ? options.taskrefs.filter((item) => typeof item === "string" && item.trim().length > 0) : [],
|
|
3014
|
+
provenance: typeof options.provenance === "string" && options.provenance ? options.provenance : "plan step",
|
|
3015
|
+
labels: Array.isArray(options.labels) ? options.labels.filter((item) => typeof item === "string" && item.trim().length > 0) : [],
|
|
3016
|
+
at: Date.now()
|
|
3017
|
+
};
|
|
3018
|
+
await memory.settabmeta(meta);
|
|
3019
|
+
await memory.setprogress(assigntasktab(await memory.getprogress(), plan.id, target, Date.now()));
|
|
3020
|
+
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);
|
|
3021
|
+
return { ok: true, summary: `Attached metadata to tab ${target} for task routing.`, details: { meta } };
|
|
3022
|
+
}
|
|
3023
|
+
case "listaudio": {
|
|
3024
|
+
const playing = audiotabs(await livetabs());
|
|
3025
|
+
await audit("tab", `Listed ${playing.length} tab${playing.length === 1 ? "" : "s"} that are playing audio.`, extra);
|
|
3026
|
+
return { ok: true, summary: `${playing.length} tab${playing.length === 1 ? " is" : "s are"} playing audio.`, details: { audio: playing } };
|
|
3027
|
+
}
|
|
3028
|
+
case "reopenrun": {
|
|
3029
|
+
const run = typeof options.run === "string" ? options.run : "";
|
|
3030
|
+
const snapshots = await memory.getsnapshots();
|
|
3031
|
+
const snapshot2 = snapshots.find((item) => item.sessionid === run) ?? snapshots.find((item) => item.id === run);
|
|
3032
|
+
if (!snapshot2) throw new Error(`No stored session snapshot exists for the run ${run}.`);
|
|
3033
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
3034
|
+
const urls = layoutrestoreplan(snapshot2.layout, open);
|
|
3035
|
+
const created = [];
|
|
3036
|
+
for (const url of urls) {
|
|
3037
|
+
const tab = await chrome.tabs.create({ url, active: created.length === 0 });
|
|
3038
|
+
created.push(tab?.id ?? 0);
|
|
3039
|
+
}
|
|
3040
|
+
await audit("layout", `Reopened ${created.length} tab${created.length === 1 ? "" : "s"} of the previous run ${run}.`, extra);
|
|
3041
|
+
return { ok: true, summary: `Reopened ${created.length} tab${created.length === 1 ? "" : "s"} of the previous run.`, details: { run, reopened: created.length, tabs: created } };
|
|
3042
|
+
}
|
|
3043
|
+
case "snapshotsession": {
|
|
3044
|
+
const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
|
|
3045
|
+
const layout = buildlayout(`session ${(/* @__PURE__ */ new Date()).toISOString()}`, tabs, windows, groups, scratch, Date.now());
|
|
3046
|
+
const snapshot2 = { id: randomid(), ...session ? { sessionid: session.id } : {}, layout, capturedat: Date.now() };
|
|
3047
|
+
await memory.addsnapshot(snapshot2);
|
|
3048
|
+
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);
|
|
3049
|
+
return { ok: true, summary: `Captured the session snapshot of ${layout.tabs.length} tabs and ${layout.windows.length} windows.`, details: { snapshot: snapshot2 } };
|
|
3050
|
+
}
|
|
3051
|
+
case "watchtab": {
|
|
3052
|
+
const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
|
|
3053
|
+
if (lifetime <= 0) throw new Error("A reviewed watch lifetime window in milliseconds is required in options.");
|
|
3054
|
+
const events = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
3055
|
+
const watchid = typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : randomid();
|
|
3056
|
+
const watch = { watchid, kind: "watchtab", stepid: step.id, sessionid: session?.id ?? plan.id, origin: session?.origin ?? plan.origin, scopes: [], events, startedat: Date.now(), lifetime };
|
|
3057
|
+
await memory.addwatch(watch);
|
|
3058
|
+
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);
|
|
3059
|
+
await new Promise((resolve) => setTimeout(resolve, lifetime));
|
|
3060
|
+
const observed = watchtabdispatch(tabwatchbuffers.get(watchid) ?? [], watchid, events);
|
|
3061
|
+
await memory.closewatch(watchid, Date.now());
|
|
3062
|
+
tabwatchbuffers.delete(watchid);
|
|
3063
|
+
await audit("watch", `Watchtab ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds with ${observed.length} observed event${observed.length === 1 ? "" : "s"}.`, extra);
|
|
3064
|
+
return { ok: true, summary: `Observed ${observed.length} tab event${observed.length === 1 ? "" : "s"} inside the reviewed lifetime.`, details: { watchid, events: observed } };
|
|
3065
|
+
}
|
|
3066
|
+
default:
|
|
3067
|
+
return { ok: false, summary: "Unsupported tabs and windows command." };
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
async function enforcewindowreview(step, session, plan) {
|
|
3071
|
+
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
3072
|
+
const progress = plan ? await memory.getprogress() : void 0;
|
|
3073
|
+
const tasktabids = plan ? trackedtasktabs(progress, plan.id) : [];
|
|
3074
|
+
const count = tasktabsinwindow(await livetabs(), windowid, tasktabids);
|
|
3075
|
+
const gate = windowclosegate(count, stepoptions2(step).reviewed === true);
|
|
3076
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The window close needs explicit review.");
|
|
3077
|
+
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 });
|
|
3078
|
+
}
|
|
3079
|
+
async function updatetaskbadges(plan, progress) {
|
|
3080
|
+
if (!plan || !progress || progress.planid !== plan.id) return;
|
|
3081
|
+
const state = badgefromprogress(progress.completedsteps.length, plan.steps.length);
|
|
3082
|
+
for (const tabid2 of tasktabs(progress, plan.id)) {
|
|
3083
|
+
await memory.setbadge({ tabid: tabid2, taskid: plan.id, label: state.label, setat: Date.now() });
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
async function togglecontroltab(enabled) {
|
|
3087
|
+
const current = await memory.getcontroltab();
|
|
3088
|
+
if (current && current.tabid) await chrome.tabs.remove(current.tabid).catch(() => void 0);
|
|
3089
|
+
if (!enabled) {
|
|
3090
|
+
const closed = { tabid: 0, enabled: false, updatedat: Date.now() };
|
|
3091
|
+
await memory.setcontroltab(closed);
|
|
3092
|
+
await audit("tab", "The pinned control tab was closed.");
|
|
3093
|
+
return closed;
|
|
3094
|
+
}
|
|
3095
|
+
const created = await chrome.tabs.create({ url: chrome.runtime.getURL("sidepanel.html"), pinned: true, active: false });
|
|
3096
|
+
const state = { tabid: created?.id ?? 0, enabled: true, updatedat: Date.now() };
|
|
3097
|
+
await memory.setcontroltab(state);
|
|
3098
|
+
await audit("tab", `The pinned control tab ${state.tabid} was opened with the live task feed.`);
|
|
3099
|
+
return state;
|
|
3100
|
+
}
|
|
2354
3101
|
async function refreshbadge() {
|
|
2355
3102
|
const queues = await memory.getnavqueues();
|
|
2356
|
-
const
|
|
3103
|
+
const badges = await memory.getbadges();
|
|
3104
|
+
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
3105
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2;
|
|
2357
3106
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
2358
3107
|
});
|
|
2359
3108
|
}
|
|
@@ -2372,7 +3121,12 @@ async function executestep(stepid) {
|
|
|
2372
3121
|
}
|
|
2373
3122
|
let output;
|
|
2374
3123
|
let watchwindow;
|
|
2375
|
-
if (
|
|
3124
|
+
if (step.kind === "windowclose") {
|
|
3125
|
+
await enforcewindowreview(step, session, plan);
|
|
3126
|
+
}
|
|
3127
|
+
if (istabscommandkind(step.kind)) {
|
|
3128
|
+
output = await executetabscommand(step, session, plan, tab.id);
|
|
3129
|
+
} else if (isbrowserkind(step.kind)) {
|
|
2376
3130
|
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
2377
3131
|
} else if (step.kind === "keyhold") {
|
|
2378
3132
|
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
@@ -2404,6 +3158,9 @@ async function executestep(stepid) {
|
|
|
2404
3158
|
}
|
|
2405
3159
|
if (["navigate", "back", "forward"].includes(step.kind)) await recordnavigation(step, session, tab.id);
|
|
2406
3160
|
await recordevidence(step, output, session, plan, origin);
|
|
3161
|
+
if (output?.ok && plan && typeof output.details?.tabid === "number") {
|
|
3162
|
+
await memory.setprogress(assigntasktab(await memory.getprogress(), plan.id, output.details.tabid, Date.now()));
|
|
3163
|
+
}
|
|
2407
3164
|
const summary = output?.summary ?? "The page action returned no result.";
|
|
2408
3165
|
const resolved = output?.details?.resolvedtarget;
|
|
2409
3166
|
if (resolved) {
|
|
@@ -2418,6 +3175,8 @@ async function executestep(stepid) {
|
|
|
2418
3175
|
const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
|
|
2419
3176
|
const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
|
|
2420
3177
|
await memory.setprogress(tracked);
|
|
3178
|
+
await updatetaskbadges(plan, tracked);
|
|
3179
|
+
await refreshbadge();
|
|
2421
3180
|
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
2422
3181
|
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
2423
3182
|
await memory.setplan(done);
|
|
@@ -2506,10 +3265,23 @@ async function handlerequest(message, sender) {
|
|
|
2506
3265
|
const navcontrol = await memory.getnavcontrol();
|
|
2507
3266
|
const navqueues = await memory.getnavqueues();
|
|
2508
3267
|
const artifacts = await memory.getartifacts();
|
|
3268
|
+
const tabs = await livetabs().catch(() => []);
|
|
3269
|
+
const windows = await livewindows().catch(() => []);
|
|
3270
|
+
const layouts = await memory.getlayouts();
|
|
3271
|
+
const tabgroups = await memory.gettabgroups();
|
|
3272
|
+
const tabmetas = await memory.gettabmetas();
|
|
3273
|
+
const badges = await memory.getbadges();
|
|
3274
|
+
const snapshots = await memory.getsnapshots();
|
|
3275
|
+
const closedtabs = await memory.getclosedtabs();
|
|
3276
|
+
const controltab = await memory.getcontroltab();
|
|
3277
|
+
const tabwatchevents = await memory.gettabwatchevents();
|
|
3278
|
+
const clones = clonetabs(tabs);
|
|
3279
|
+
const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
|
|
3280
|
+
const report = await buildtabreport(tabs);
|
|
2509
3281
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
2510
3282
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
2511
3283
|
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 };
|
|
3284
|
+
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 };
|
|
2513
3285
|
}
|
|
2514
3286
|
case "capabilities":
|
|
2515
3287
|
return refreshcapabilities();
|
|
@@ -2607,6 +3379,104 @@ async function handlerequest(message, sender) {
|
|
|
2607
3379
|
if (!plan) throw new Error("No plan is available for a safety envelope.");
|
|
2608
3380
|
return JSON.parse(safetyresponse({ verdicts: await memory.getsafeties(), plan }));
|
|
2609
3381
|
}
|
|
3382
|
+
case "jumptotab": {
|
|
3383
|
+
const inputtab = message;
|
|
3384
|
+
if (typeof inputtab.tabid !== "number") throw new Error("A numeric tab id is required to jump.");
|
|
3385
|
+
await chrome.tabs.update(inputtab.tabid, { active: true }).catch(() => {
|
|
3386
|
+
throw new Error("The tab to jump to is no longer open.");
|
|
3387
|
+
});
|
|
3388
|
+
await audit("tab", `The review panel jumped to tab ${inputtab.tabid}.`);
|
|
3389
|
+
return { tabid: inputtab.tabid };
|
|
3390
|
+
}
|
|
3391
|
+
case "tabsearch": {
|
|
3392
|
+
const inputsearch = message;
|
|
3393
|
+
const granted = await chrome.permissions.contains({ permissions: ["tabs"] });
|
|
3394
|
+
if (!granted) throw new Error("The tabs capability has not been granted; request it from the review panel.");
|
|
3395
|
+
const matches = searchtabmatches(await livetabs(), inputsearch.text ?? "");
|
|
3396
|
+
await audit("tab", `The review panel searched the open tabs for "${inputsearch.text ?? ""}" and matched ${matches.length} tab${matches.length === 1 ? "" : "s"}.`);
|
|
3397
|
+
return { matches };
|
|
3398
|
+
}
|
|
3399
|
+
case "savelayout": {
|
|
3400
|
+
const session = await memory.getsession();
|
|
3401
|
+
const gate = layoutmutationgranted(session, Date.now());
|
|
3402
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
3403
|
+
const inputlayout = message;
|
|
3404
|
+
if (!inputlayout.name?.trim()) throw new Error("A layout name is required.");
|
|
3405
|
+
const [tabs, windows, groups, scratch] = await Promise.all([livetabs(), livewindows(), memory.gettabgroups(), memory.getscratchwindows()]);
|
|
3406
|
+
const layout = buildlayout(inputlayout.name.trim(), tabs, windows, groups, scratch, Date.now());
|
|
3407
|
+
await memory.setlayout(layout);
|
|
3408
|
+
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 } : {} });
|
|
3409
|
+
return layout;
|
|
3410
|
+
}
|
|
3411
|
+
case "restorelayout": {
|
|
3412
|
+
const session = await memory.getsession();
|
|
3413
|
+
const gate = layoutmutationgranted(session, Date.now());
|
|
3414
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
3415
|
+
const inputlayout = message;
|
|
3416
|
+
const layout = await memory.getlayout(inputlayout.name ?? "");
|
|
3417
|
+
if (!layout) throw new Error(`No tab layout named ${inputlayout.name ?? ""} is stored yet.`);
|
|
3418
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
3419
|
+
const urls = layoutrestoreplan(layout, open);
|
|
3420
|
+
const opened = [];
|
|
3421
|
+
for (const url of urls) {
|
|
3422
|
+
const created = await chrome.tabs.create({ url, active: opened.length === 0 });
|
|
3423
|
+
opened.push(created?.id ?? 0);
|
|
3424
|
+
}
|
|
3425
|
+
await audit("layout", `The review panel restored the tab layout ${layout.name}: ${opened.length} tab${opened.length === 1 ? "" : "s"} reopened.`, { ...session ? { sessionid: session.id } : {} });
|
|
3426
|
+
return { name: layout.name, reopened: opened.length };
|
|
3427
|
+
}
|
|
3428
|
+
case "restoresnapshot": {
|
|
3429
|
+
const session = await memory.getsession();
|
|
3430
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Snapshot restore stays behind the consent gate of an active session.");
|
|
3431
|
+
const inputsnapshot = message;
|
|
3432
|
+
const snapshot2 = (await memory.getsnapshots()).find((item) => item.id === inputsnapshot.id);
|
|
3433
|
+
if (!snapshot2) throw new Error("No stored session snapshot matches the requested id.");
|
|
3434
|
+
const open = (await livetabs()).map((tab) => tab.url).filter(Boolean);
|
|
3435
|
+
const urls = layoutrestoreplan(snapshot2.layout, open);
|
|
3436
|
+
const opened = [];
|
|
3437
|
+
for (const url of urls) {
|
|
3438
|
+
const created = await chrome.tabs.create({ url, active: opened.length === 0 });
|
|
3439
|
+
opened.push(created?.id ?? 0);
|
|
3440
|
+
}
|
|
3441
|
+
await audit("layout", `The review panel restored the session snapshot ${snapshot2.id}: ${opened.length} tab${opened.length === 1 ? "" : "s"} reopened.`, { sessionid: session.id });
|
|
3442
|
+
return { id: snapshot2.id, reopened: opened.length };
|
|
3443
|
+
}
|
|
3444
|
+
case "controltab": {
|
|
3445
|
+
const inputcontrol = message;
|
|
3446
|
+
const state = await togglecontroltab(inputcontrol.enabled === true);
|
|
3447
|
+
const settings = await memory.getsettings();
|
|
3448
|
+
await memory.setsettings({ ...settings, controltab: state.enabled });
|
|
3449
|
+
return state;
|
|
3450
|
+
}
|
|
3451
|
+
case "settasktabceiling": {
|
|
3452
|
+
const inputceiling = message;
|
|
3453
|
+
const settings = await memory.getsettings();
|
|
3454
|
+
const ceiling = typeof inputceiling.ceiling === "number" && Number.isFinite(inputceiling.ceiling) && inputceiling.ceiling >= 0 ? inputceiling.ceiling : void 0;
|
|
3455
|
+
await memory.setsettings({ ...settings, ...ceiling !== void 0 ? { tasktabceiling: ceiling } : {} });
|
|
3456
|
+
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.`);
|
|
3457
|
+
return { tasktabceiling: ceiling };
|
|
3458
|
+
}
|
|
3459
|
+
case "windowstate": {
|
|
3460
|
+
const inputwindow = message;
|
|
3461
|
+
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.");
|
|
3462
|
+
await chrome.windows.update(inputwindow.windowid, { state: inputwindow.state });
|
|
3463
|
+
await audit("window", `The popup set window ${inputwindow.windowid} to the ${inputwindow.state} state.`);
|
|
3464
|
+
return { windowid: inputwindow.windowid, state: inputwindow.state };
|
|
3465
|
+
}
|
|
3466
|
+
case "closewindow": {
|
|
3467
|
+
const inputclose = message;
|
|
3468
|
+
if (typeof inputclose.windowid !== "number") throw new Error("A numeric window id is required.");
|
|
3469
|
+
const session = await memory.getsession();
|
|
3470
|
+
const plan = await memory.getplan();
|
|
3471
|
+
const progress = plan ? await memory.getprogress() : void 0;
|
|
3472
|
+
const tasktabids = plan && progress ? trackedtasktabs(progress, plan.id) : [];
|
|
3473
|
+
const count = tasktabsinwindow(await livetabs(), inputclose.windowid, tasktabids);
|
|
3474
|
+
const gate = windowclosegate(count, inputclose.reviewed === true);
|
|
3475
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
3476
|
+
await chrome.windows.remove(inputclose.windowid);
|
|
3477
|
+
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 } : {} });
|
|
3478
|
+
return { windowid: inputclose.windowid, closed: true };
|
|
3479
|
+
}
|
|
2610
3480
|
case "stop": {
|
|
2611
3481
|
const session = await memory.getsession();
|
|
2612
3482
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|