@wenathlan/extension 1.1.34 → 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.
@@ -1,3 +1,22 @@
1
+ // policy.ts
2
+ 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"]);
3
+ var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
4
+ 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"]);
5
+ var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
6
+
7
+ // extension/tabscommand.ts
8
+ function switcherlist(tabs, recency, filter) {
9
+ const needle = filter.trim().toLowerCase();
10
+ const matches = needle ? tabs.filter((tab) => tab.title.toLowerCase().includes(needle) || tab.url.toLowerCase().includes(needle)) : [...tabs];
11
+ const lastrun = new Map(recency.map((entry) => [entry.tabid, entry.at]));
12
+ return [...matches].sort((left, right) => {
13
+ const leftat = lastrun.get(left.tabid) ?? -1;
14
+ const rightat = lastrun.get(right.tabid) ?? -1;
15
+ if (leftat !== rightat) return rightat - leftat;
16
+ return left.index - right.index;
17
+ });
18
+ }
19
+
1
20
  // extension/sidepanel.ts
2
21
  var objective = document.querySelector("#objective");
3
22
  var localbutton = document.querySelector("#localplan");
@@ -14,6 +33,9 @@ var streamroot = document.querySelector("#stream");
14
33
  var diffsroot = document.querySelector("#diffs");
15
34
  var bannersroot = document.querySelector("#banners");
16
35
  var selectorsroot = document.querySelector("#selectors");
36
+ var trailroot = document.querySelector("#trail");
37
+ var navigationroot = document.querySelector("#navigation");
38
+ var tabswindowsroot = document.querySelector("#tabswindows");
17
39
  var statusnode = document.querySelector("#status");
18
40
  var progressnode = document.querySelector("#planprogress");
19
41
  var capabilitiestext = document.querySelector("#capabilitiestext");
@@ -58,7 +80,9 @@ var topictags = [
58
80
  { topic: "reads", kinds: ["mapclicks", "verifyvisible", "verifyenabled", "resolvexpath"] },
59
81
  { topic: "observation", kinds: ["a11ytree", "readvisible", "readertree", "readoutline", "readselection", "readopengraph", "readlang", "detectlanguage", "listshadow", "listframes", "readscrollpos"] },
60
82
  { topic: "detection", kinds: ["detectlists", "detecttables", "detectinfinitescroll", "detectvirtual", "detectlazy", "detectsticky", "detectscrolllock", "countpages", "classifypage", "fingerprintsection"] },
61
- { topic: "watch", kinds: ["watchmutate", "watchbanner", "watchfocus", "waitquiet", "readjson", "diffsnapshots", "deriveselector"] }
83
+ { topic: "watch", kinds: ["watchmutate", "watchbanner", "watchfocus", "waitquiet", "readjson", "diffsnapshots", "deriveselector"] },
84
+ { topic: "navigation", kinds: ["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"] },
85
+ { topic: "tabs", kinds: ["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"] }
62
86
  ];
63
87
  function steptopic(kind) {
64
88
  return topictags.find((tag) => tag.kinds.includes(kind))?.topic;
@@ -113,6 +137,29 @@ function quietdetail(step, outcomes) {
113
137
  node.textContent = `network quiet: ${samples.length} sample${samples.length === 1 ? "" : "s"} \xB7 quiet for ${Math.round(last?.quietfor ?? 0)} ms \xB7 idle threshold ${idle} ms \xB7 ${latest.ok ? "quiet reached" : "still busy"}`;
114
138
  return node;
115
139
  }
140
+ function navlistdetail(step, progress, outcomes) {
141
+ if (step.kind !== "navlist") return null;
142
+ const entries = (progress?.outcomes ?? []).filter((outcome) => outcome.stepid === step.id && outcome.details?.naventry !== void 0).map((outcome) => outcome.details?.naventry);
143
+ const total = entries.length > 0 ? Math.max(...entries.map((entry) => entry.index)) + 1 : 0;
144
+ const latestoutcome = [...outcomes].reverse().find((outcome) => outcome.stepid === step.id);
145
+ const remaining = typeof latestoutcome?.details?.remaining === "number" ? latestoutcome.details?.remaining : 0;
146
+ const current = entries[entries.length - 1];
147
+ const node = document.createElement("p");
148
+ node.className = "timeline";
149
+ node.textContent = entries.length === 0 ? "navigation list: no entry completed yet" : `navigation list: ${entries.filter((entry) => entry.ok).length} of ${total} entries completed \xB7 current url ${current?.url ?? ""} \xB7 ${remaining} remaining`;
150
+ return node;
151
+ }
152
+ function redirectdetail(step, outcomes) {
153
+ if (!["navigate", "followlink", "spanav", "navlist", "openlink", "reloadcache"].includes(step.kind)) return null;
154
+ const latest = [...outcomes].reverse().find((outcome) => outcome.stepid === step.id && outcome.details?.hops !== void 0);
155
+ if (!latest) return null;
156
+ const hops = typeof latest.details?.hops === "number" ? latest.details?.hops : 0;
157
+ const final = typeof latest.details?.finalurl === "string" ? latest.details?.finalurl : "";
158
+ const node = document.createElement("p");
159
+ node.className = "timeline";
160
+ node.textContent = `redirects: ${Math.max(0, hops - 1)} hop${hops - 1 === 1 ? "" : "s"} \xB7 final url ${final || "unknown"}`;
161
+ return node;
162
+ }
116
163
  function renderpreview(step) {
117
164
  const preview = previews.get(step.id);
118
165
  if (!preview) return null;
@@ -138,7 +185,7 @@ function renderpreview(step) {
138
185
  }
139
186
  return node;
140
187
  }
141
- function stepitem(plan, step, completed, outcomes, retries) {
188
+ function stepitem(plan, step, completed, outcomes, retries, progress) {
142
189
  const item = document.createElement("li");
143
190
  const done = completed.includes(step.id);
144
191
  item.textContent = `${done ? "\u2713" : ""} ${step.summary}${holddetail(step)}`;
@@ -146,6 +193,10 @@ function stepitem(plan, step, completed, outcomes, retries) {
146
193
  if (outcome) item.append(outcome);
147
194
  const timeline = step.kind === "retryaction" ? retrydetail(step, retries) : quietdetail(step, outcomes);
148
195
  if (timeline) item.append(timeline);
196
+ const navlist = navlistdetail(step, progress, outcomes);
197
+ if (navlist) item.append(navlist);
198
+ const redirects = redirectdetail(step, outcomes);
199
+ if (redirects) item.append(redirects);
149
200
  const preview = renderpreview(step);
150
201
  if (preview) item.append(preview);
151
202
  const hastarget = Boolean(step.target) || options(step).targetref !== void 0;
@@ -162,7 +213,7 @@ function stepitem(plan, step, completed, outcomes, retries) {
162
213
  }));
163
214
  return item;
164
215
  }
165
- function steplist(plan, steps, completed, outcomes, retries, risk) {
216
+ function steplist(plan, steps, completed, outcomes, retries, risk, progress) {
166
217
  const group = steps.filter((step) => step.risk === risk);
167
218
  if (group.length === 0) return null;
168
219
  const section = document.createElement("section");
@@ -172,7 +223,7 @@ function steplist(plan, steps, completed, outcomes, retries, risk) {
172
223
  const general = group.filter((step) => steptopic(step.kind) === void 0);
173
224
  if (general.length > 0) {
174
225
  const list = document.createElement("ol");
175
- for (const step of general) list.append(stepitem(plan, step, completed, outcomes, retries));
226
+ for (const step of general) list.append(stepitem(plan, step, completed, outcomes, retries, progress));
176
227
  section.append(list);
177
228
  }
178
229
  for (const tag of topictags) {
@@ -182,7 +233,7 @@ function steplist(plan, steps, completed, outcomes, retries, risk) {
182
233
  row.textContent = `${tag.topic} steps`;
183
234
  section.append(row);
184
235
  const list = document.createElement("ol");
185
- for (const step of tagged) list.append(stepitem(plan, step, completed, outcomes, retries));
236
+ for (const step of tagged) list.append(stepitem(plan, step, completed, outcomes, retries, progress));
186
237
  section.append(list);
187
238
  }
188
239
  return section;
@@ -200,9 +251,9 @@ function renderplan(plan, progress, outcomes = [], retries = []) {
200
251
  planroot.append(title);
201
252
  const completed = progress?.planid === plan.id ? progress.completedsteps : [];
202
253
  renderprogress(plan, completed);
203
- const sensitive = steplist(plan, plan.steps, completed, outcomes, retries, "sensitive");
204
- const interaction = steplist(plan, plan.steps, completed, outcomes, retries, "interaction");
205
- const read = steplist(plan, plan.steps, completed, outcomes, retries, "read");
254
+ const sensitive = steplist(plan, plan.steps, completed, outcomes, retries, "sensitive", progress);
255
+ const interaction = steplist(plan, plan.steps, completed, outcomes, retries, "interaction", progress);
256
+ const read = steplist(plan, plan.steps, completed, outcomes, retries, "read", progress);
206
257
  for (const group of [sensitive, interaction, read]) if (group) planroot.append(group);
207
258
  if (plan.state === "pending") {
208
259
  planroot.append(button("Approve reviewed plan", async () => {
@@ -400,6 +451,340 @@ function renderselectors(selectors) {
400
451
  selectorsroot.append(item);
401
452
  }
402
453
  }
454
+ function rendertrail(trail) {
455
+ if (!trailroot) return;
456
+ trailroot.replaceChildren();
457
+ if (trail.length === 0) {
458
+ trailroot.textContent = "No page has been visited inside a reviewed navigation step yet.";
459
+ return;
460
+ }
461
+ for (const entry of [...trail].reverse().slice(0, 12)) {
462
+ const item = document.createElement("li");
463
+ item.textContent = `${new Date(entry.at).toLocaleTimeString()} \xB7 ${entry.url}${entry.title ? ` \xB7 ${entry.title}` : ""}${entry.stepid ? ` \xB7 step ${entry.stepid}` : ""}`;
464
+ trailroot.append(item);
465
+ }
466
+ }
467
+ function rendernavigation(context) {
468
+ if (!navigationroot) return;
469
+ navigationroot.replaceChildren();
470
+ const paused = document.createElement("p");
471
+ if (context.navcontrol?.pausedat) {
472
+ paused.className = "bannercard";
473
+ paused.textContent = `Navigation is paused while ${context.navcontrol.reason ?? "a consent prompt is open"}; reviewed navigation steps are blocked until it resumes.`;
474
+ } else {
475
+ paused.textContent = "Navigation is live; no consent prompt holds it.";
476
+ }
477
+ navigationroot.append(paused);
478
+ const rates = context.ratestates ?? [];
479
+ if (rates.length > 0) {
480
+ const heading = document.createElement("p");
481
+ heading.textContent = "rate limit windows per domain:";
482
+ navigationroot.append(heading);
483
+ const list = document.createElement("ul");
484
+ for (const state of rates.slice(0, 6)) {
485
+ const item = document.createElement("li");
486
+ item.textContent = `${state.domain}: ${state.count} of ${state.limit.ceiling} navigations inside the reviewed window of ${state.limit.window} ms`;
487
+ list.append(item);
488
+ }
489
+ navigationroot.append(list);
490
+ }
491
+ const records = context.navrecords ?? [];
492
+ const record = records[0];
493
+ if (record) {
494
+ const chain = document.createElement("p");
495
+ chain.textContent = `latest navigation: ${Math.max(0, record.chain.hops.length - 1)} redirect${record.chain.hops.length - 1 === 1 ? "" : "s"} \xB7 final url ${record.finalurl}`;
496
+ navigationroot.append(chain);
497
+ const hops = document.createElement("ul");
498
+ for (const hop of record.chain.hops.slice(0, 6)) {
499
+ const item = document.createElement("li");
500
+ let path = hop.url;
501
+ try {
502
+ path = new URL(hop.url).pathname;
503
+ } catch {
504
+ path = hop.url;
505
+ }
506
+ item.textContent = `hop ${path} \xB7 status ${hop.status} \xB7 ${new Date(hop.at).toLocaleTimeString()}`;
507
+ hops.append(item);
508
+ }
509
+ navigationroot.append(hops);
510
+ }
511
+ const curatedlists = context.curated ?? [];
512
+ if (curatedlists.length > 0) {
513
+ const heading = document.createElement("p");
514
+ heading.textContent = "curated link lists with per url safety states:";
515
+ navigationroot.append(heading);
516
+ for (const list of curatedlists.slice(0, 3)) {
517
+ const card = document.createElement("div");
518
+ card.className = "bannercard";
519
+ const title = document.createElement("p");
520
+ title.textContent = `${list.links.length} curated url${list.links.length === 1 ? "" : "s"}${list.reviewedat ? " \xB7 opened after review" : " \xB7 waiting for review"}`;
521
+ card.append(title);
522
+ for (const link of list.links.slice(0, 8)) {
523
+ const line = document.createElement("p");
524
+ line.textContent = `${link.verdict === "safe" ? "\u2713" : "\u2717"} ${link.url}${link.reasons.length > 0 ? ` \xB7 ${link.reasons.join("; ")}` : ""}`;
525
+ card.append(line);
526
+ }
527
+ navigationroot.append(card);
528
+ }
529
+ }
530
+ const safeties = context.safeties ?? [];
531
+ const runner = document.createElement("p");
532
+ const checkinput = document.createElement("input");
533
+ checkinput.type = "url";
534
+ checkinput.placeholder = "https://external.example/link";
535
+ checkinput.setAttribute("aria-label", "url to verify with checksafe");
536
+ const checkbutton = button("Run checksafe", async () => {
537
+ const verdict = await request({ kind: "checksafe", url: checkinput.value });
538
+ status(verdict.safe ? `${verdict.url} passed every safety check.` : `${verdict.url} is unsafe: ${verdict.reasons.join("; ")}.`);
539
+ await refresh();
540
+ });
541
+ runner.append(checkinput, " ", checkbutton);
542
+ navigationroot.append(runner);
543
+ if (safeties.length > 0) {
544
+ const list = document.createElement("ul");
545
+ for (const verdict of safeties.slice(0, 6)) {
546
+ const item = document.createElement("li");
547
+ item.textContent = `${verdict.safe ? "safe" : "unsafe"} \xB7 ${verdict.url}${verdict.reasons.length > 0 ? ` \xB7 ${verdict.reasons.join("; ")}` : ""}`;
548
+ list.append(item);
549
+ }
550
+ navigationroot.append(list);
551
+ }
552
+ const active = context.session && !context.session.stoppedat && context.session.expiresat > Date.now();
553
+ const authcard = document.createElement("div");
554
+ authcard.className = "bannercard";
555
+ const authtitle = document.createElement("p");
556
+ authtitle.textContent = active ? "basic auth credentials (stored only after your explicit review):" : "basic auth credentials need an active session before they can be reviewed.";
557
+ authcard.append(authtitle);
558
+ if (active) {
559
+ const origininput = document.createElement("input");
560
+ origininput.type = "url";
561
+ origininput.placeholder = context.session?.origin ?? "https://example.com";
562
+ origininput.setAttribute("aria-label", "auth origin");
563
+ const userinput = document.createElement("input");
564
+ userinput.type = "text";
565
+ userinput.placeholder = "username";
566
+ userinput.setAttribute("aria-label", "auth username");
567
+ const passinput = document.createElement("input");
568
+ passinput.type = "password";
569
+ passinput.placeholder = "password";
570
+ passinput.setAttribute("aria-label", "auth password");
571
+ const storebutton = button("Store reviewed credentials", async () => {
572
+ const stored = await request({ kind: "storeauth", origin: origininput.value, username: userinput.value, password: passinput.value });
573
+ status(`Reviewed basic auth credentials stored for ${stored.origin}.`);
574
+ await refresh();
575
+ });
576
+ authcard.append(origininput, " ", userinput, " ", passinput, " ", storebutton);
577
+ }
578
+ navigationroot.append(authcard);
579
+ const auths = context.auths ?? [];
580
+ if (auths.length > 0) {
581
+ const list = document.createElement("ul");
582
+ for (const record2 of auths.slice(0, 4)) {
583
+ const item = document.createElement("li");
584
+ item.textContent = `basic auth for ${record2.origin} as ${record2.username}, reviewed ${new Date(record2.reviewedat).toLocaleString()}`;
585
+ list.append(item);
586
+ }
587
+ navigationroot.append(list);
588
+ }
589
+ const artifacts = context.artifacts ?? [];
590
+ if (artifacts.length > 0) {
591
+ const heading = document.createElement("p");
592
+ heading.textContent = "task artifacts:";
593
+ navigationroot.append(heading);
594
+ const list = document.createElement("ul");
595
+ for (const artifact of artifacts.slice(0, 6)) {
596
+ const item = document.createElement("li");
597
+ item.textContent = `${artifact.kind}: ${artifact.name} \xB7 step ${artifact.stepid}`;
598
+ list.append(item);
599
+ }
600
+ navigationroot.append(list);
601
+ }
602
+ }
603
+ function rendertabswindows(context) {
604
+ if (!tabswindowsroot) return;
605
+ tabswindowsroot.replaceChildren();
606
+ const tabs = context.tabs ?? [];
607
+ const badges = context.badges ?? [];
608
+ const metas = context.tabmetas ?? [];
609
+ const active = context.session && !context.session.stoppedat && context.session.expiresat > Date.now();
610
+ const gauge = context.tasktabgauge ?? { used: 0, ceiling: void 0, over: false };
611
+ const budget = document.createElement("p");
612
+ budget.className = gauge.over ? "bannercard" : "";
613
+ budget.textContent = `task tab budget: ${gauge.used} tab${gauge.used === 1 ? "" : "s"} with active tasks${gauge.ceiling !== void 0 ? ` of the user configured ceiling ${gauge.ceiling}` : " with no user ceiling configured"}${gauge.over ? " \u2014 over the reviewed budget" : ""}`;
614
+ tabswindowsroot.append(budget);
615
+ const ceilinginput = document.createElement("input");
616
+ ceilinginput.type = "number";
617
+ ceilinginput.min = "0";
618
+ ceilinginput.placeholder = gauge.ceiling !== void 0 ? String(gauge.ceiling) : "no ceiling";
619
+ ceilinginput.setAttribute("aria-label", "concurrent task tab ceiling");
620
+ const ceilingbutton = button("Save task tab ceiling", async () => {
621
+ await request({ kind: "settasktabceiling", ceiling: ceilinginput.value === "" ? void 0 : Number(ceilinginput.value) });
622
+ status(`Task tab ceiling saved as ${ceilinginput.value === "" ? "no ceiling" : ceilinginput.value}; the value stays a user choice.`);
623
+ await refresh();
624
+ });
625
+ tabswindowsroot.append(ceilinginput, " ", ceilingbutton);
626
+ const switcherheading = document.createElement("p");
627
+ switcherheading.textContent = "quick switcher (ordered by recency, filter by title or url):";
628
+ tabswindowsroot.append(switcherheading);
629
+ const filterinput = document.createElement("input");
630
+ filterinput.type = "search";
631
+ filterinput.placeholder = "filter open tabs";
632
+ filterinput.setAttribute("aria-label", "quick switcher filter");
633
+ const switchlist = document.createElement("ul");
634
+ const renderswitchlist = () => {
635
+ switchlist.replaceChildren();
636
+ const ordered = switcherlist(tabs, [], filterinput.value).slice(0, 10);
637
+ for (const tab of ordered) {
638
+ const item = document.createElement("li");
639
+ const jump = document.createElement("button");
640
+ jump.type = "button";
641
+ const badge = badges.find((entry) => entry.tabid === tab.tabid);
642
+ const meta = metas.find((entry) => entry.tabid === tab.tabid);
643
+ jump.textContent = `${tab.title || tab.url}${tab.pinned ? " \u{1F4CC}" : ""}${tab.audible || tab.muted ? ` ${tab.muted ? "\u{1F507}" : "\u{1F50A}"}` : ""}${badge ? ` [${badge.label}]` : ""}${meta && meta.labels.length > 0 ? ` (${meta.labels.join(", ")})` : ""}`;
644
+ jump.addEventListener("click", () => request({ kind: "jumptotab", tabid: tab.tabid }).then(() => status(`Jumped to tab ${tab.tabid}.`)).catch((error) => status(error instanceof Error ? error.message : String(error), true)));
645
+ item.append(jump);
646
+ switchlist.append(item);
647
+ }
648
+ if (ordered.length === 0) {
649
+ const empty = document.createElement("li");
650
+ empty.textContent = "no open tab matches the filter";
651
+ switchlist.append(empty);
652
+ }
653
+ };
654
+ filterinput.addEventListener("input", renderswitchlist);
655
+ tabswindowsroot.append(filterinput, switchlist);
656
+ renderswitchlist();
657
+ const searchinput = document.createElement("input");
658
+ searchinput.type = "search";
659
+ searchinput.placeholder = "search across open tabs by title and url";
660
+ searchinput.setAttribute("aria-label", "searchtabs text");
661
+ const searchresults = document.createElement("ul");
662
+ const searchbutton = button("Run searchtabs", async () => {
663
+ const result = await request({ kind: "tabsearch", text: searchinput.value });
664
+ searchresults.replaceChildren();
665
+ for (const tab of result.matches.slice(0, 10)) {
666
+ const item = document.createElement("li");
667
+ const jump = document.createElement("button");
668
+ jump.type = "button";
669
+ jump.textContent = `${tab.title || tab.url} \xB7 ${tab.url}`;
670
+ jump.addEventListener("click", () => request({ kind: "jumptotab", tabid: tab.tabid }).then(() => status(`Jumped to tab ${tab.tabid}.`)).catch((error) => status(error instanceof Error ? error.message : String(error), true)));
671
+ item.append(jump);
672
+ searchresults.append(item);
673
+ }
674
+ status(`searchtabs matched ${result.matches.length} open tab${result.matches.length === 1 ? "" : "s"}.`);
675
+ });
676
+ tabswindowsroot.append(searchinput, " ", searchbutton, searchresults);
677
+ const clones = context.clones ?? [];
678
+ for (const clone of clones.slice(0, 3)) {
679
+ const warning = document.createElement("p");
680
+ warning.className = "bannercard";
681
+ warning.textContent = `duplicate tab warning: ${clone.tabids.length} open tabs share the url ${clone.url} (tabs ${clone.tabids.join(", ")})`;
682
+ tabswindowsroot.append(warning);
683
+ }
684
+ const groups = context.tabgroups ?? [];
685
+ if (groups.length > 0) {
686
+ const groupsheading = document.createElement("p");
687
+ groupsheading.textContent = "tab groups with colors and collapse states:";
688
+ tabswindowsroot.append(groupsheading);
689
+ const grouplist = document.createElement("ul");
690
+ for (const group of groups.slice(0, 6)) {
691
+ const item = document.createElement("li");
692
+ item.textContent = `${group.name} \xB7 ${group.color} \xB7 ${group.collapsed ? "collapsed" : "expanded"} \xB7 ${group.tabids.length} member tab${group.tabids.length === 1 ? "" : "s"}`;
693
+ grouplist.append(item);
694
+ }
695
+ tabswindowsroot.append(grouplist);
696
+ }
697
+ const windows = context.windows ?? [];
698
+ if (windows.length > 0) {
699
+ const windowsheading = document.createElement("p");
700
+ windowsheading.textContent = "windows with layouts and bounds:";
701
+ tabswindowsroot.append(windowsheading);
702
+ const windowlist = document.createElement("ul");
703
+ for (const item of windows.slice(0, 6)) {
704
+ const entry = document.createElement("li");
705
+ entry.textContent = `window ${item.windowid} \xB7 ${item.state} \xB7 bounds ${item.left}\xD7${item.top} ${item.width}\xD7${item.height}${item.incognito ? " \xB7 incognito, grants not inherited" : ""}${item.focused ? " \xB7 focused" : ""}`;
706
+ const closebutton = button("Close window", async () => {
707
+ const tasktabids = context.progress?.tasktabs ?? [];
708
+ const tabsoftask = (context.tabs ?? []).filter((tab) => tab.windowid === item.windowid && tasktabids.includes(tab.tabid));
709
+ const reviewed = tabsoftask.length > 1 ? window.confirm(`This window holds ${tabsoftask.length} task tabs. Close it anyway under explicit review?`) : true;
710
+ await request({ kind: "closewindow", windowid: item.windowid, reviewed });
711
+ status(`Closed window ${item.windowid}.`);
712
+ await refresh();
713
+ });
714
+ entry.append(" ", closebutton);
715
+ windowlist.append(entry);
716
+ }
717
+ tabswindowsroot.append(windowlist);
718
+ }
719
+ const layoutcontrols = document.createElement("p");
720
+ const layoutname = document.createElement("input");
721
+ layoutname.type = "text";
722
+ layoutname.placeholder = "layout name";
723
+ layoutname.setAttribute("aria-label", "layout name");
724
+ const savebutton = button("Save layout", async () => {
725
+ if (!active) {
726
+ status("Layout save stays inside an active session.", true);
727
+ return;
728
+ }
729
+ await request({ kind: "savelayout", name: layoutname.value });
730
+ status(`Saved the tab layout ${layoutname.value}.`);
731
+ await refresh();
732
+ });
733
+ const restorebutton = button("Restore layout", async () => {
734
+ if (!active) {
735
+ status("Layout restore stays inside an active session.", true);
736
+ return;
737
+ }
738
+ const result = await request({ kind: "restorelayout", name: layoutname.value });
739
+ status(`Restored the tab layout ${layoutname.value}: ${result.reopened} tab${result.reopened === 1 ? "" : "s"} reopened.`);
740
+ await refresh();
741
+ });
742
+ layoutcontrols.append(layoutname, " ", savebutton, " ", restorebutton);
743
+ tabswindowsroot.append(layoutcontrols);
744
+ const layouts = context.layouts ?? [];
745
+ if (layouts.length > 0) {
746
+ const layoutlist = document.createElement("ul");
747
+ for (const layout of layouts.slice(0, 4)) {
748
+ const item = document.createElement("li");
749
+ item.textContent = `${layout.name} \xB7 ${layout.tabs.length} tab${layout.tabs.length === 1 ? "" : "s"} \xB7 ${layout.groups.length} group${layout.groups.length === 1 ? "" : "s"} \xB7 ${layout.windows.length} window bound${layout.windows.length === 1 ? "" : "s"} \xB7 saved ${new Date(layout.savedat).toLocaleString()}`;
750
+ layoutlist.append(item);
751
+ }
752
+ tabswindowsroot.append(layoutlist);
753
+ }
754
+ const snapshots = context.snapshots ?? [];
755
+ if (snapshots.length > 0) {
756
+ const snapcard = document.createElement("div");
757
+ snapcard.className = "bannercard";
758
+ const snaptitle = document.createElement("p");
759
+ snaptitle.textContent = `session snapshot card: ${snapshots.length} snapshot${snapshots.length === 1 ? "" : "s"} stored`;
760
+ snapcard.append(snaptitle);
761
+ for (const snapshot of snapshots.slice(0, 3)) {
762
+ const row = document.createElement("p");
763
+ row.textContent = `${snapshot.layout.tabs.length} tabs \xB7 captured ${new Date(snapshot.capturedat).toLocaleString()}`;
764
+ const restore = button("Restore snapshot", async () => {
765
+ const result = await request({ kind: "restoresnapshot", id: snapshot.id });
766
+ status(`Restored the session snapshot: ${result.reopened} tab${result.reopened === 1 ? "" : "s"} reopened.`);
767
+ await refresh();
768
+ });
769
+ row.append(" ", restore);
770
+ snapcard.append(row);
771
+ }
772
+ tabswindowsroot.append(snapcard);
773
+ }
774
+ const controlcard = document.createElement("div");
775
+ controlcard.className = "bannercard";
776
+ const controlstate = context.controltab;
777
+ const completed = context.progress?.completedsteps.length ?? 0;
778
+ const total = context.plan?.steps.length ?? 0;
779
+ controlcard.textContent = `pinned control tab feed: ${controlstate?.enabled ? `open as tab ${controlstate.tabid} with the live task status ${completed} of ${total} reviewed steps executed` : "disabled"}${context.plan ? ` \xB7 ${context.plan.state}` : " \xB7 no plan"}`;
780
+ const controlbutton = button(controlstate?.enabled ? "Close pinned control tab" : "Open pinned control tab", async () => {
781
+ await request({ kind: "controltab", enabled: !controlstate?.enabled });
782
+ status(controlstate?.enabled ? "The pinned control tab was closed." : "The pinned control tab was opened with the live task feed.");
783
+ await refresh();
784
+ });
785
+ controlcard.append(" ", controlbutton);
786
+ tabswindowsroot.append(controlcard);
787
+ }
403
788
  function rendercapabilities(report) {
404
789
  if (!capabilitiestext) return;
405
790
  if (!report) {
@@ -443,6 +828,9 @@ async function refresh() {
443
828
  renderdiffs(context.diffs ?? []);
444
829
  renderbanners(context.banners ?? []);
445
830
  renderselectors(context.selectors ?? []);
831
+ rendertrail(context.trail?.trail ?? []);
832
+ rendernavigation(context);
833
+ rendertabswindows(context);
446
834
  renderaudit(context.audit);
447
835
  rendercapabilities(context.capabilities);
448
836
  if (context.session?.pausedat) status("Session paused. Reviewed actions are blocked until resume.");