@quanta-intellect/vessel-browser 0.1.12 → 0.1.14

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/out/main/index.js CHANGED
@@ -710,7 +710,8 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
710
710
  if (text) {
711
711
  return wc.executeJavaScript(`
712
712
  (function() {
713
- var searchText = ${JSON.stringify(text)};
713
+ var searchText = (${JSON.stringify(text)} || '').trim();
714
+ var foldedSearchText = searchText.toLowerCase();
714
715
  var solidColor = ${JSON.stringify(c.solid)};
715
716
  var bgColor = ${JSON.stringify(c.bg)};
716
717
  var labelBg = ${JSON.stringify(c.label)};
@@ -748,7 +749,11 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
748
749
  });
749
750
  var n;
750
751
  while ((n = w.nextNode())) {
751
- var idx = n.textContent.indexOf(searchText);
752
+ var haystack = n.textContent || '';
753
+ var idx = haystack.indexOf(searchText);
754
+ if (idx === -1 && foldedSearchText) {
755
+ idx = haystack.toLowerCase().indexOf(foldedSearchText);
756
+ }
752
757
  if (idx !== -1) {
753
758
  matches.push({ node: n, idx: idx });
754
759
  if (matches.length >= limit) break;
@@ -2004,6 +2009,7 @@ const Channels = {
2004
2009
  BOOKMARKS_UPDATE: "bookmarks:update",
2005
2010
  BOOKMARK_SAVE: "bookmarks:save",
2006
2011
  BOOKMARK_REMOVE: "bookmarks:remove",
2012
+ BOOKMARK_ADD_CONTEXT_TO_CHAT: "bookmarks:add-context-to-chat",
2007
2013
  FOLDER_CREATE: "bookmarks:folder-create",
2008
2014
  FOLDER_REMOVE: "bookmarks:folder-remove",
2009
2015
  FOLDER_RENAME: "bookmarks:folder-rename",
@@ -2062,6 +2068,10 @@ async function getSidebarContextTarget(sidebarView, x, y) {
2062
2068
  return {
2063
2069
  inHighlightNav: !!nav,
2064
2070
  canRemoveCurrent: /\\d+\\s*\\/\\s*\\d+/.test(label),
2071
+ bookmarkId:
2072
+ el && typeof el.closest === "function"
2073
+ ? el.closest("[data-bookmark-id]")?.getAttribute("data-bookmark-id") || undefined
2074
+ : undefined,
2065
2075
  };
2066
2076
  })()`,
2067
2077
  true
@@ -2095,6 +2105,20 @@ async function showSidebarContextMenu(mainWindow, sidebarView, params) {
2095
2105
  })
2096
2106
  );
2097
2107
  }
2108
+ if (target.bookmarkId) {
2109
+ if (menu.items.length > 0) {
2110
+ menu.append(new electron.MenuItem({ type: "separator" }));
2111
+ }
2112
+ menu.append(
2113
+ new electron.MenuItem({
2114
+ label: "Add Context to Chat",
2115
+ click: () => sidebarView.webContents.send(
2116
+ Channels.BOOKMARK_ADD_CONTEXT_TO_CHAT,
2117
+ target.bookmarkId
2118
+ )
2119
+ })
2120
+ );
2121
+ }
2098
2122
  if (params.isEditable) {
2099
2123
  if (menu.items.length > 0) {
2100
2124
  menu.append(new electron.MenuItem({ type: "separator" }));
@@ -2226,7 +2250,14 @@ function createMainWindow(onTabStateChange) {
2226
2250
  return state2;
2227
2251
  }
2228
2252
  function layoutViews(state2) {
2229
- const { mainWindow, chromeView, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
2253
+ const {
2254
+ mainWindow,
2255
+ chromeView,
2256
+ sidebarView,
2257
+ devtoolsPanelView,
2258
+ tabManager,
2259
+ uiState
2260
+ } = state2;
2230
2261
  const [width, height] = mainWindow.getContentSize();
2231
2262
  const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
2232
2263
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
@@ -2971,11 +3002,25 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
2971
3002
  }
2972
3003
 
2973
3004
  function viewportWidth() {
2974
- return window.innerWidth || document.documentElement?.clientWidth || 0;
3005
+ return Math.max(
3006
+ window.innerWidth || 0,
3007
+ window.visualViewport?.width || 0,
3008
+ document.documentElement?.clientWidth || 0,
3009
+ document.scrollingElement?.clientWidth || 0,
3010
+ document.body?.clientWidth || 0,
3011
+ window.screen?.availWidth || 0,
3012
+ );
2975
3013
  }
2976
3014
 
2977
3015
  function viewportHeight() {
2978
- return window.innerHeight || document.documentElement?.clientHeight || 0;
3016
+ return Math.max(
3017
+ window.innerHeight || 0,
3018
+ window.visualViewport?.height || 0,
3019
+ document.documentElement?.clientHeight || 0,
3020
+ document.scrollingElement?.clientHeight || 0,
3021
+ document.body?.clientHeight || 0,
3022
+ window.screen?.availHeight || 0,
3023
+ );
2979
3024
  }
2980
3025
 
2981
3026
  function scrollingElement() {
@@ -3032,6 +3077,60 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3032
3077
  rect.bottom >= centerY;
3033
3078
  }
3034
3079
 
3080
+ function touchesViewportEdge(rect) {
3081
+ const edgePadding = 24;
3082
+ return rect.left <= edgePadding ||
3083
+ rect.top <= edgePadding ||
3084
+ rect.right >= viewportWidth() - edgePadding ||
3085
+ rect.bottom >= viewportHeight() - edgePadding;
3086
+ }
3087
+
3088
+ function hasFixedAncestor(el) {
3089
+ var cur = el.parentElement;
3090
+ while (cur && cur !== document.body) {
3091
+ var ps = getComputedStyle(cur).position;
3092
+ if (ps === "fixed" || ps === "sticky") return true;
3093
+ cur = cur.parentElement;
3094
+ }
3095
+ return false;
3096
+ }
3097
+
3098
+ function isPositioned(style) {
3099
+ return style.position === "fixed" || style.position === "sticky" ||
3100
+ style.position === "absolute";
3101
+ }
3102
+
3103
+ function effectiveZIndex(style, el) {
3104
+ var z = parseZIndex(style);
3105
+ if (z > 0) return z;
3106
+ var cur = el.parentElement;
3107
+ while (cur && cur !== document.body) {
3108
+ var pz = parseZIndex(getComputedStyle(cur));
3109
+ if (pz > 0) return pz;
3110
+ cur = cur.parentElement;
3111
+ }
3112
+ return 0;
3113
+ }
3114
+
3115
+ function looksLikeDrawer(style, rect, areaRatio, el) {
3116
+ if (rect.width < 220 || rect.height < 160 || areaRatio < 0.08) return false;
3117
+ if (!touchesViewportEdge(rect)) return false;
3118
+ if (style.position === "fixed" || style.position === "sticky") {
3119
+ return effectiveZIndex(style, el) >= 5;
3120
+ }
3121
+ if (style.position === "absolute" && hasFixedAncestor(el)) {
3122
+ return effectiveZIndex(style, el) >= 5;
3123
+ }
3124
+ return false;
3125
+ }
3126
+
3127
+ function looksLikeCartConfirmation(node) {
3128
+ var t = (node.textContent || "").slice(0, 500).toLowerCase();
3129
+ var signals = ["added to cart", "added to bag", "added to basket",
3130
+ "added to your cart", "added to your bag", "added to your basket"];
3131
+ return signals.some(function(s) { return t.indexOf(s) !== -1; });
3132
+ }
3133
+
3035
3134
  function detectOverlays() {
3036
3135
  if (!document.body) return [];
3037
3136
  const viewportArea = Math.max(1, viewportWidth() * viewportHeight());
@@ -3070,7 +3169,13 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3070
3169
  var type = overlayType(node);
3071
3170
  var dialogLike = type === "dialog" || type === "modal";
3072
3171
  var areaRatio = (rect.width * rect.height) / viewportArea;
3172
+ var drawerLike = looksLikeDrawer(style, rect, areaRatio, node);
3173
+ var cartConfirm = !dialogLike && !drawerLike && isPositioned(style) &&
3174
+ rect.width >= 160 && rect.height >= 100 &&
3175
+ looksLikeCartConfirmation(node);
3073
3176
  var blocksInteraction = dialogLike ||
3177
+ drawerLike ||
3178
+ cartConfirm ||
3074
3179
  ((style.position === "fixed" || style.position === "sticky") &&
3075
3180
  parseZIndex(style) >= 10 &&
3076
3181
  areaRatio >= 0.3 &&
@@ -3188,6 +3293,11 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3188
3293
  }
3189
3294
 
3190
3295
  function contextOf(el) {
3296
+ const overlayRoot = overlays.find(
3297
+ (overlay) => overlay.element === el || overlay.element.contains(el),
3298
+ );
3299
+ if (overlayRoot) return "dialog";
3300
+
3191
3301
  let current = el.parentElement;
3192
3302
  while (current) {
3193
3303
  const tag = current.tagName.toLowerCase();
@@ -4466,6 +4576,158 @@ function createProvider(config) {
4466
4576
  }
4467
4577
  return new OpenAICompatProvider(normalized);
4468
4578
  }
4579
+ const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
4580
+ const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
4581
+ function elementLabel(el) {
4582
+ return el.text?.trim() || el.label?.trim() || el.value?.trim() || el.placeholder?.trim() || void 0;
4583
+ }
4584
+ function isOverlayAction(el) {
4585
+ if (el.type === "button" || el.type === "link") return true;
4586
+ if (el.type !== "input") return false;
4587
+ return ["button", "submit", "radio", "checkbox"].includes(
4588
+ (el.inputType || "").toLowerCase()
4589
+ );
4590
+ }
4591
+ function isRadioOption(el) {
4592
+ return el.role === "radio" || el.type === "input" && (el.inputType || "").toLowerCase() === "radio";
4593
+ }
4594
+ function normalizeAction(el) {
4595
+ return {
4596
+ index: el.index,
4597
+ label: elementLabel(el),
4598
+ selector: el.selector,
4599
+ role: el.role,
4600
+ labelSource: el.labelSource,
4601
+ looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel(el))
4602
+ };
4603
+ }
4604
+ function normalizeStoredAction(action) {
4605
+ return {
4606
+ label: action.label,
4607
+ selector: action.selector,
4608
+ role: action.kind === "radio" ? "radio" : void 0
4609
+ };
4610
+ }
4611
+ function normalizeStoredRadioOption(option) {
4612
+ return {
4613
+ label: option.label,
4614
+ selector: option.selector,
4615
+ role: "radio",
4616
+ labelSource: option.labelSource,
4617
+ looksCorrect: option.looksCorrect !== void 0 ? option.looksCorrect : looksLikeCorrectOption(option.label)
4618
+ };
4619
+ }
4620
+ function dedupeCandidates(actions) {
4621
+ const seen = /* @__PURE__ */ new Set();
4622
+ return actions.filter((action) => {
4623
+ const key = [
4624
+ action.selector || "",
4625
+ action.label || "",
4626
+ action.role || "",
4627
+ action.labelSource || ""
4628
+ ].join("::");
4629
+ if (seen.has(key)) return false;
4630
+ seen.add(key);
4631
+ return true;
4632
+ });
4633
+ }
4634
+ function classifyOverlayKind(overlay, radioOptions) {
4635
+ if (overlay.kind) {
4636
+ return overlay.kind;
4637
+ }
4638
+ const haystack = [overlay.label, overlay.text, overlay.role].filter(Boolean).join(" ").toLowerCase();
4639
+ if (/cookie|consent|privacy|gdpr|ccpa|onetrust|trustarc|cookiebot/.test(
4640
+ haystack
4641
+ )) {
4642
+ return "cookie_consent";
4643
+ }
4644
+ if (radioOptions.length > 0) return "selection_modal";
4645
+ if (overlay.role === "alertdialog" || /\b(alert|warning|error)\b/.test(haystack)) {
4646
+ return "alert";
4647
+ }
4648
+ if (overlay.type === "dialog") return "dialog";
4649
+ if (overlay.type === "modal") return "modal";
4650
+ return "overlay";
4651
+ }
4652
+ function findAction(actions, matcher) {
4653
+ return actions.find(
4654
+ (action) => matcher.test((action.label || "").toLowerCase())
4655
+ );
4656
+ }
4657
+ function looksLikeCorrectOption(label) {
4658
+ const text = label?.trim();
4659
+ if (!text) return void 0;
4660
+ if (CORRECT_HINT_RE.test(text)) return true;
4661
+ if (WRONG_HINT_RE.test(text)) return false;
4662
+ return void 0;
4663
+ }
4664
+ function getBlockingOverlaySignature(overlays) {
4665
+ return overlays.filter((overlay) => overlay.blocksInteraction).map(
4666
+ (overlay) => [
4667
+ overlay.kind,
4668
+ overlay.selector || "",
4669
+ overlay.label || "",
4670
+ overlay.text || "",
4671
+ overlay.actions.map((action) => `${action.selector || ""}:${action.label || ""}`).join("|"),
4672
+ overlay.radioOptions.map((option) => `${option.selector || ""}:${option.label || ""}`).join("|")
4673
+ ].join("::")
4674
+ ).join("||");
4675
+ }
4676
+ function buildOverlayInventory(page) {
4677
+ if (page.overlays.length === 0) return [];
4678
+ return page.overlays.map((overlay) => {
4679
+ const controls = dedupeCandidates([
4680
+ ...page.interactiveElements.filter((el) => {
4681
+ if (overlay.selector && el.parentOverlay === overlay.selector) {
4682
+ return true;
4683
+ }
4684
+ return page.overlays.length === 1 && el.context === "dialog";
4685
+ }).filter(isOverlayAction).map(normalizeAction),
4686
+ ...(overlay.actions || []).map(normalizeStoredAction)
4687
+ ]).filter((action) => action.label || action.selector);
4688
+ const radioOptions = dedupeCandidates([
4689
+ ...page.interactiveElements.filter((el) => {
4690
+ if (!isRadioOption(el)) return false;
4691
+ if (overlay.selector && el.parentOverlay === overlay.selector) {
4692
+ return true;
4693
+ }
4694
+ return page.overlays.length === 1 && el.context === "dialog";
4695
+ }).map(normalizeAction),
4696
+ ...(overlay.radioOptions || []).map(normalizeStoredRadioOption)
4697
+ ]).filter((action) => action.label || action.selector);
4698
+ const kind = classifyOverlayKind(overlay, radioOptions);
4699
+ const dismissAction = findAction(
4700
+ controls,
4701
+ /\b(close|dismiss|skip|cancel|reject|decline|no thanks|not now|maybe later|continue without)\b/
4702
+ );
4703
+ const acceptAction = findAction(
4704
+ controls,
4705
+ /\b(accept|allow|agree|got it|ok|okay|consent)\b/
4706
+ );
4707
+ const submitAction = findAction(
4708
+ controls,
4709
+ /\b(submit|continue|confirm|done|next|save|apply|finish)\b/
4710
+ );
4711
+ const correctOption = radioOptions.find(
4712
+ (option) => option.looksCorrect === true
4713
+ );
4714
+ return {
4715
+ type: overlay.type,
4716
+ kind,
4717
+ role: overlay.role,
4718
+ label: overlay.label,
4719
+ selector: overlay.selector,
4720
+ text: overlay.text,
4721
+ blocksInteraction: overlay.blocksInteraction,
4722
+ actions: controls,
4723
+ radioOptions,
4724
+ dismissAction,
4725
+ acceptAction,
4726
+ submitAction,
4727
+ correctOption
4728
+ };
4729
+ });
4730
+ }
4469
4731
  const MAX_CONTENT_LENGTH = 6e4;
4470
4732
  const MAX_STRUCTURED_ITEMS = 100;
4471
4733
  const LARGE_PAGE_HINT_THRESHOLD = 12e3;
@@ -4533,10 +4795,18 @@ function formatElementMeta(el) {
4533
4795
  if (el.pattern) {
4534
4796
  meta.push(`pattern="${el.pattern}"`);
4535
4797
  }
4798
+ if (el.labelSource) {
4799
+ meta.push(`source=${el.labelSource}`);
4800
+ }
4801
+ if (el.looksCorrect === true) {
4802
+ meta.push("likely-correct");
4803
+ } else if (el.looksCorrect === false) {
4804
+ meta.push("likely-wrong");
4805
+ }
4536
4806
  if (el.description) {
4537
4807
  meta.push(`desc="${el.description.slice(0, 80)}"`);
4538
4808
  }
4539
- if (el.value) {
4809
+ if (el.value !== void 0 && el.value !== null && el.value !== "") {
4540
4810
  meta.push(`value="${el.value.slice(0, 60)}"`);
4541
4811
  }
4542
4812
  if (el.selector) {
@@ -4545,14 +4815,207 @@ function formatElementMeta(el) {
4545
4815
  }
4546
4816
  return meta;
4547
4817
  }
4818
+ function summarizeElementValue(el) {
4819
+ const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
4820
+ if (!value) return null;
4821
+ if (el.type === "select") {
4822
+ return { label: "selected", value: value.slice(0, 60) };
4823
+ }
4824
+ if (el.type === "textarea") {
4825
+ return { label: "current", value: value.slice(0, 60) };
4826
+ }
4827
+ if (el.type === "input") {
4828
+ return { label: "current", value: value.slice(0, 60) };
4829
+ }
4830
+ return null;
4831
+ }
4832
+ function isQuantityLike(el) {
4833
+ const text = [
4834
+ el.label,
4835
+ el.name,
4836
+ el.placeholder,
4837
+ el.text,
4838
+ el.description,
4839
+ el.selector
4840
+ ].filter(Boolean).join(" ").toLowerCase();
4841
+ return /\b(qty|quantity|count|items?)\b/.test(text) || el.inputType === "number" && (/\b(quantity|qty|count|items?)\b/.test(text) || el.name === "quantity" || el.name === "qty");
4842
+ }
4843
+ function getQuantityElements(page) {
4844
+ const seen = /* @__PURE__ */ new Set();
4845
+ const elements = [
4846
+ ...page.interactiveElements,
4847
+ ...page.forms.flatMap((form) => form.fields)
4848
+ ];
4849
+ return elements.filter((el) => {
4850
+ if (!isQuantityLike(el)) return false;
4851
+ const key = String(
4852
+ el.index ?? el.selector ?? `${el.type}|${el.name || ""}|${el.label || ""}|${el.value || ""}`
4853
+ );
4854
+ if (seen.has(key)) return false;
4855
+ seen.add(key);
4856
+ return true;
4857
+ });
4858
+ }
4859
+ function formatQuantityElements(elements) {
4860
+ if (elements.length === 0) return "None detected";
4861
+ return limitItems(elements, 12).map((el) => {
4862
+ const prefix = el.index ? `[#${el.index}]` : "-";
4863
+ const name = el.label || el.name || el.placeholder || "Quantity";
4864
+ const summary = summarizeElementValue(el);
4865
+ const parts = [prefix, `[${name}]`, el.type];
4866
+ if (summary) {
4867
+ parts.push(`${summary.label}="${summary.value}"`);
4868
+ }
4869
+ const meta = formatElementMeta({
4870
+ ...el,
4871
+ value: void 0
4872
+ });
4873
+ if (meta.length > 0) {
4874
+ parts.push(`(${meta.join(", ")})`);
4875
+ }
4876
+ return parts.join(" ");
4877
+ }).join("\n");
4878
+ }
4879
+ function isCartLikePage(page) {
4880
+ const url = page.url.toLowerCase();
4881
+ const text = `${page.title}
4882
+ ${page.content}`.toLowerCase();
4883
+ return url.includes("cart") || url.includes("checkout") || url.includes("basket") || url.includes("bag") || getQuantityElements(page).length > 0 || /\b(subtotal|order total|cart total|checkout|shopping cart)\b/.test(text);
4884
+ }
4885
+ function getCartItemLinks(page) {
4886
+ const blockedText = /\b(remove|delete|wishlist|save for later|move to|checkout|view cart|continue shopping|edit|details?)\b/i;
4887
+ const blockedHref = /\/(cart|checkout|wishlist|account|login|signin|remove|delete)(\/|$)|[?&](remove|delete|wishlist)=/i;
4888
+ const seen = /* @__PURE__ */ new Set();
4889
+ return page.interactiveElements.filter((el) => el.type === "link").filter((el) => {
4890
+ const text = (el.text || "").trim();
4891
+ const href = (el.href || "").trim();
4892
+ if (!text || text.length < 3 || !href) return false;
4893
+ if (el.context === "nav" || el.context === "footer" || el.context === "sidebar") {
4894
+ return false;
4895
+ }
4896
+ if (blockedText.test(text) || blockedHref.test(href)) return false;
4897
+ const key = `${normalizeComparable(text)}|${normalizeUrlForMatch(href) || href}`;
4898
+ if (seen.has(key)) return false;
4899
+ seen.add(key);
4900
+ return true;
4901
+ }).slice(0, 12);
4902
+ }
4903
+ function extractCartTotals(content) {
4904
+ const lines = content.split(/\n+/).map((line) => line.trim()).filter(Boolean);
4905
+ const totalLines = [];
4906
+ const seen = /* @__PURE__ */ new Set();
4907
+ const keyword = /\b(subtotal|order total|estimated total|total|tax|shipping|discount|savings?)\b/i;
4908
+ const money = /([$€£]\s?\d[\d,]*(?:\.\d{2})?|\d[\d,]*(?:\.\d{2})?\s?(?:usd|eur|gbp))/i;
4909
+ for (const line of lines) {
4910
+ if (!keyword.test(line)) continue;
4911
+ if (!money.test(line) && line.length > 90) continue;
4912
+ const cleaned = line.replace(/\s+/g, " ").trim();
4913
+ if (seen.has(cleaned.toLowerCase())) continue;
4914
+ seen.add(cleaned.toLowerCase());
4915
+ totalLines.push(cleaned);
4916
+ if (totalLines.length >= 6) break;
4917
+ }
4918
+ return totalLines;
4919
+ }
4920
+ function formatCartSnapshot(page) {
4921
+ if (!isCartLikePage(page)) return null;
4922
+ const itemLinks = getCartItemLinks(page);
4923
+ const quantityElements = getQuantityElements(page);
4924
+ const quantityValues = quantityElements.map((el) => summarizeElementValue(el)?.value || "").filter(Boolean);
4925
+ const numericQuantities = quantityValues.map((value) => Number.parseFloat(value)).filter((value) => Number.isFinite(value) && value >= 0);
4926
+ const totalLines = extractCartTotals(page.content);
4927
+ const lines = [];
4928
+ if (itemLinks.length > 0) {
4929
+ lines.push(`Distinct items: ${itemLinks.length}`);
4930
+ lines.push(
4931
+ `Items: ${itemLinks.slice(0, 8).map((item) => item.text || item.label || "Untitled item").join(" | ")}`
4932
+ );
4933
+ }
4934
+ if (quantityElements.length > 0) {
4935
+ if (numericQuantities.length === quantityElements.length && numericQuantities.length > 0) {
4936
+ const unique = Array.from(new Set(numericQuantities));
4937
+ const totalUnits = numericQuantities.reduce(
4938
+ (sum, value) => sum + value,
4939
+ 0
4940
+ );
4941
+ lines.push(
4942
+ unique.length === 1 ? `Quantity controls: ${quantityElements.length} (all set to ${unique[0]})` : `Quantity controls: ${quantityElements.length} (${numericQuantities.join(", ")})`
4943
+ );
4944
+ lines.push(`Total units inferred: ${totalUnits}`);
4945
+ if (itemLinks.length > 0 && totalUnits > itemLinks.length) {
4946
+ lines.push(
4947
+ `Attention: ${itemLinks.length} distinct items but ${totalUnits} total units. Check for duplicate quantities.`
4948
+ );
4949
+ }
4950
+ } else {
4951
+ lines.push(
4952
+ `Quantity controls: ${quantityElements.length}${quantityValues.length > 0 ? ` (${quantityValues.join(", ")})` : ""}`
4953
+ );
4954
+ }
4955
+ }
4956
+ if (totalLines.length > 0) {
4957
+ lines.push("Totals:");
4958
+ totalLines.forEach((line) => lines.push(`- ${line}`));
4959
+ }
4960
+ if (lines.length === 0) return null;
4961
+ return lines.join("\n");
4962
+ }
4548
4963
  function isVisibleToUser(el) {
4549
4964
  return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
4550
4965
  }
4966
+ function getDialogFocusedElements(page) {
4967
+ return page.interactiveElements.filter(
4968
+ (el) => isVisibleToUser(el) && el.context === "dialog"
4969
+ );
4970
+ }
4971
+ function normalizeOverlayText(value) {
4972
+ return (value || "").trim().toLowerCase();
4973
+ }
4974
+ function isCartConfirmationLike(page) {
4975
+ const overlayText = page.overlays.map(
4976
+ (overlay) => normalizeOverlayText(
4977
+ [overlay.label, overlay.text].filter(Boolean).join(" ")
4978
+ )
4979
+ ).join(" ");
4980
+ const dialogText = getDialogFocusedElements(page).map((el) => normalizeOverlayText(el.text || el.label || el.description)).join(" ");
4981
+ const haystack = `${overlayText} ${dialogText}`.trim();
4982
+ if (!haystack) return false;
4983
+ const cartSignals = [
4984
+ "added to cart",
4985
+ "added to bag",
4986
+ "added to basket",
4987
+ "shopping cart",
4988
+ "view cart",
4989
+ "go to cart",
4990
+ "continue shopping",
4991
+ "keep shopping",
4992
+ "checkout"
4993
+ ];
4994
+ return cartSignals.some((signal) => haystack.includes(signal));
4995
+ }
4996
+ function formatDialogFocus(page) {
4997
+ const dialogElements = getDialogFocusedElements(page);
4998
+ if (dialogElements.length === 0) return null;
4999
+ const lines = [];
5000
+ lines.push(
5001
+ "A live dialog/modal is open. Prioritize its controls before acting on the page behind it."
5002
+ );
5003
+ if (isCartConfirmationLike(page)) {
5004
+ lines.push(
5005
+ "Cart confirmation detected: choose a dialog action such as Continue Shopping, View Cart, or Checkout. Do not click background Add to Cart again."
5006
+ );
5007
+ }
5008
+ lines.push("");
5009
+ lines.push("Visible dialog controls:");
5010
+ lines.push(formatInteractiveElements(dialogElements));
5011
+ return lines.join("\n");
5012
+ }
4551
5013
  function formatInteractiveElements(elements) {
4552
5014
  if (elements.length === 0) return "None";
4553
5015
  const sorted = [...elements].sort((a, b) => {
4554
5016
  const scoreEl = (el) => {
4555
5017
  let s = 0;
5018
+ if (el.context === "dialog") s -= 40;
4556
5019
  if (el.visible === false) s += 100;
4557
5020
  if (el.inViewport === false) s += 50;
4558
5021
  if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
@@ -4569,7 +5032,7 @@ function formatInteractiveElements(elements) {
4569
5032
  const parts = [prefix];
4570
5033
  if (el.type === "button") {
4571
5034
  parts.push(`[${el.text || "Button"}]`);
4572
- parts.push("button");
5035
+ parts.push(el.role === "radio" ? "radio" : "button");
4573
5036
  } else if (el.type === "link") {
4574
5037
  parts.push(`[${el.text || "Link"}]`);
4575
5038
  parts.push("link");
@@ -4578,10 +5041,14 @@ function formatInteractiveElements(elements) {
4578
5041
  parts.push(`[${el.label || el.placeholder || "Input"}]`);
4579
5042
  parts.push(el.inputType || "text");
4580
5043
  parts.push("input");
5044
+ const summary = summarizeElementValue(el);
5045
+ if (summary) parts.push(`${summary.label}="${summary.value}"`);
4581
5046
  if (el.required) parts.push("(required)");
4582
5047
  } else if (el.type === "select") {
4583
5048
  parts.push(`[${el.label || "Select"}]`);
4584
5049
  parts.push("dropdown");
5050
+ const summary = summarizeElementValue(el);
5051
+ if (summary) parts.push(`${summary.label}="${summary.value}"`);
4585
5052
  if (el.options?.length) {
4586
5053
  parts.push(
4587
5054
  `options=${el.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
@@ -4590,6 +5057,8 @@ function formatInteractiveElements(elements) {
4590
5057
  } else if (el.type === "textarea") {
4591
5058
  parts.push(`[${el.label || "Text Area"}]`);
4592
5059
  parts.push("textarea");
5060
+ const summary = summarizeElementValue(el);
5061
+ if (summary) parts.push(`${summary.label}="${summary.value}"`);
4593
5062
  }
4594
5063
  const meta = formatElementMeta(el);
4595
5064
  if (meta.length > 0) parts.push(`(${meta.join(", ")})`);
@@ -4628,14 +5097,18 @@ function formatForms(forms) {
4628
5097
  ];
4629
5098
  if (field.type === "button") {
4630
5099
  fieldParts.push(`[${field.text || "Submit"}]`);
4631
- fieldParts.push("button");
5100
+ fieldParts.push(field.role === "radio" ? "radio" : "button");
4632
5101
  } else if (field.type === "input") {
4633
5102
  fieldParts.push(`[${field.label || field.placeholder || "Input"}]`);
4634
5103
  fieldParts.push(field.inputType || "text");
5104
+ const summary = summarizeElementValue(field);
5105
+ if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
4635
5106
  if (field.required) fieldParts.push("(required)");
4636
5107
  } else if (field.type === "select") {
4637
5108
  fieldParts.push(`[${field.label || "Select"}]`);
4638
5109
  fieldParts.push("dropdown");
5110
+ const summary = summarizeElementValue(field);
5111
+ if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
4639
5112
  if (field.options?.length) {
4640
5113
  fieldParts.push(
4641
5114
  `options=${field.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
@@ -4644,6 +5117,8 @@ function formatForms(forms) {
4644
5117
  } else if (field.type === "textarea") {
4645
5118
  fieldParts.push(`[${field.label || "Text"}]`);
4646
5119
  fieldParts.push("textarea");
5120
+ const summary = summarizeElementValue(field);
5121
+ if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
4647
5122
  }
4648
5123
  const meta = formatElementMeta(field);
4649
5124
  if (meta.length > 0) fieldParts.push(`(${meta.join(", ")})`);
@@ -4669,18 +5144,51 @@ function formatLandmarks(landmarks) {
4669
5144
  function formatViewport(page) {
4670
5145
  return `${page.viewport.width}x${page.viewport.height} at scroll (${page.viewport.scrollX}, ${page.viewport.scrollY})`;
4671
5146
  }
4672
- function formatOverlays(overlays) {
4673
- if (overlays.length === 0) return "None detected";
4674
- const items = limitItems(overlays, 10);
5147
+ function formatOverlays(page) {
5148
+ if (page.overlays.length === 0) return "None detected";
5149
+ const items = limitItems(buildOverlayInventory(page), 10);
4675
5150
  return items.map((overlay) => {
4676
- const parts = [`- ${overlay.type}`];
4677
- if (overlay.role) parts.push(`role=${overlay.role}`);
4678
- if (overlay.blocksInteraction) parts.push("blocking");
4679
- if (overlay.label) parts.push(`label="${overlay.label.slice(0, 80)}"`);
4680
- if (overlay.text) parts.push(`text="${overlay.text.slice(0, 100)}"`);
4681
- return parts.join(" ");
5151
+ const lines = [
5152
+ [
5153
+ `- ${overlay.kind}`,
5154
+ overlay.role ? `role=${overlay.role}` : "",
5155
+ overlay.blocksInteraction ? "blocking" : "",
5156
+ overlay.label ? `label="${overlay.label.slice(0, 80)}"` : "",
5157
+ overlay.text ? `text="${overlay.text.slice(0, 100)}"` : ""
5158
+ ].filter(Boolean).join(" ")
5159
+ ];
5160
+ if (overlay.radioOptions.length > 0) {
5161
+ const options = overlay.radioOptions.slice(0, 4).map((option) => {
5162
+ const tags = [];
5163
+ if (option.labelSource) tags.push(`source=${option.labelSource}`);
5164
+ if (option.looksCorrect === true) tags.push("likely-correct");
5165
+ if (option.looksCorrect === false) tags.push("likely-wrong");
5166
+ const suffix = tags.length > 0 ? ` (${tags.join(", ")})` : "";
5167
+ return `${option.label || option.selector || "radio"}${suffix}`;
5168
+ }).join(" | ");
5169
+ lines.push(` options: ${options}`);
5170
+ }
5171
+ const actionLabels = [
5172
+ overlay.dismissAction?.label ? `dismiss="${overlay.dismissAction.label}"` : "",
5173
+ overlay.acceptAction?.label ? `accept="${overlay.acceptAction.label}"` : "",
5174
+ overlay.submitAction?.label ? `submit="${overlay.submitAction.label}"` : ""
5175
+ ].filter(Boolean);
5176
+ if (actionLabels.length > 0) {
5177
+ lines.push(` actions: ${actionLabels.join(" ")}`);
5178
+ }
5179
+ return lines.join("\n");
4682
5180
  }).join("\n");
4683
5181
  }
5182
+ function getScrollHints(page) {
5183
+ const candidates = page.interactiveElements.filter(
5184
+ (el) => el.visible !== false && el.inViewport === false && el.context !== "nav" && el.context !== "footer" && el.context !== "sidebar" && el.blockedByOverlay !== true && (el.type === "input" || el.type === "textarea" || el.type === "select" || el.type === "button")
5185
+ );
5186
+ if (candidates.length === 0) return [];
5187
+ const labels = limitItems(candidates, 3).map((el) => el.text || el.label || el.placeholder || el.type).filter(Boolean);
5188
+ return [
5189
+ `Scroll to reveal offscreen controls: ${labels.join(", ")}${candidates.length > labels.length ? ", ..." : ""}`
5190
+ ];
5191
+ }
4684
5192
  function formatDormantOverlays(overlays) {
4685
5193
  if (overlays.length === 0) return "None detected";
4686
5194
  const items = limitItems(overlays, 10);
@@ -5009,6 +5517,7 @@ function buildScopedContext(page, mode) {
5009
5517
  switch (mode) {
5010
5518
  case "summary": {
5011
5519
  const sections = [];
5520
+ const cartSnapshot = formatCartSnapshot(page);
5012
5521
  sections.push(`**URL:** ${page.url}`);
5013
5522
  sections.push(`**Title:** ${page.title}`);
5014
5523
  sections.push(`**Viewport:** ${formatViewport(page)}`);
@@ -5016,12 +5525,21 @@ function buildScopedContext(page, mode) {
5016
5525
  if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
5017
5526
  const largePageHint = formatLargePageHint(page);
5018
5527
  if (largePageHint) sections.push(`**Reading Hint:** ${largePageHint}`);
5528
+ const scrollHints = getScrollHints(page);
5529
+ if (scrollHints.length > 0) {
5530
+ sections.push(`**Scroll Hint:** ${scrollHints[0]}`);
5531
+ }
5019
5532
  sections.push("");
5020
5533
  const summaryIntent = analyzePageIntent(page);
5021
5534
  if (summaryIntent) {
5022
5535
  sections.push(summaryIntent);
5023
5536
  sections.push("");
5024
5537
  }
5538
+ if (cartSnapshot) {
5539
+ sections.push("### Cart Snapshot");
5540
+ sections.push(cartSnapshot);
5541
+ sections.push("");
5542
+ }
5025
5543
  if ((page.pageIssues?.length ?? 0) > 0) {
5026
5544
  sections.push("### Page Access Warnings");
5027
5545
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5073,9 +5591,16 @@ function buildScopedContext(page, mode) {
5073
5591
  }
5074
5592
  case "interactives_only": {
5075
5593
  const sections = [];
5594
+ const quantityElements = getQuantityElements(page);
5595
+ const cartSnapshot = formatCartSnapshot(page);
5596
+ const dialogFocus = formatDialogFocus(page);
5076
5597
  sections.push(`**URL:** ${page.url}`);
5077
5598
  sections.push(`**Title:** ${page.title}`);
5078
5599
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5600
+ const interactivesScrollHints = getScrollHints(page);
5601
+ if (interactivesScrollHints.length > 0) {
5602
+ sections.push(`**Scroll Hint:** ${interactivesScrollHints[0]}`);
5603
+ }
5079
5604
  sections.push("");
5080
5605
  const interactivesIntent = analyzePageIntent(page);
5081
5606
  if (interactivesIntent) {
@@ -5088,6 +5613,11 @@ function buildScopedContext(page, mode) {
5088
5613
  sections.push(formatHighlights(interactivesHighlights));
5089
5614
  sections.push("");
5090
5615
  }
5616
+ if (cartSnapshot) {
5617
+ sections.push("### Cart Snapshot");
5618
+ sections.push(cartSnapshot);
5619
+ sections.push("");
5620
+ }
5091
5621
  if ((page.pageIssues?.length ?? 0) > 0) {
5092
5622
  sections.push("### Page Access Warnings");
5093
5623
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5095,7 +5625,12 @@ function buildScopedContext(page, mode) {
5095
5625
  }
5096
5626
  if (page.overlays.length > 0) {
5097
5627
  sections.push("### Active Overlays");
5098
- sections.push(formatOverlays(page.overlays));
5628
+ sections.push(formatOverlays(page));
5629
+ sections.push("");
5630
+ }
5631
+ if (dialogFocus) {
5632
+ sections.push("### Immediate Overlay Actions");
5633
+ sections.push(dialogFocus);
5099
5634
  sections.push("");
5100
5635
  }
5101
5636
  if (page.dormantOverlays.length > 0) {
@@ -5108,6 +5643,11 @@ function buildScopedContext(page, mode) {
5108
5643
  sections.push(formatNavigation(page.navigation));
5109
5644
  sections.push("");
5110
5645
  }
5646
+ if (quantityElements.length > 0) {
5647
+ sections.push("### Quantity / Count Controls");
5648
+ sections.push(formatQuantityElements(quantityElements));
5649
+ sections.push("");
5650
+ }
5111
5651
  if (page.interactiveElements.length > 0) {
5112
5652
  sections.push(
5113
5653
  `### Interactive Elements (${page.interactiveElements.length})`
@@ -5118,9 +5658,15 @@ function buildScopedContext(page, mode) {
5118
5658
  }
5119
5659
  case "forms_only": {
5120
5660
  const sections = [];
5661
+ const quantityElements = getQuantityElements(page);
5662
+ const cartSnapshot = formatCartSnapshot(page);
5121
5663
  sections.push(`**URL:** ${page.url}`);
5122
5664
  sections.push(`**Title:** ${page.title}`);
5123
5665
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5666
+ const visibleScrollHints = getScrollHints(page);
5667
+ if (visibleScrollHints.length > 0) {
5668
+ sections.push(`**Scroll Hint:** ${visibleScrollHints[0]}`);
5669
+ }
5124
5670
  sections.push("");
5125
5671
  const formsHighlights = getHighlightsForPage(page.url);
5126
5672
  if (formsHighlights.length > 0) {
@@ -5128,6 +5674,11 @@ function buildScopedContext(page, mode) {
5128
5674
  sections.push(formatHighlights(formsHighlights));
5129
5675
  sections.push("");
5130
5676
  }
5677
+ if (cartSnapshot) {
5678
+ sections.push("### Cart Snapshot");
5679
+ sections.push(cartSnapshot);
5680
+ sections.push("");
5681
+ }
5131
5682
  if ((page.pageIssues?.length ?? 0) > 0) {
5132
5683
  sections.push("### Page Access Warnings");
5133
5684
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5135,7 +5686,7 @@ function buildScopedContext(page, mode) {
5135
5686
  }
5136
5687
  if (page.overlays.length > 0) {
5137
5688
  sections.push("### Active Overlays");
5138
- sections.push(formatOverlays(page.overlays));
5689
+ sections.push(formatOverlays(page));
5139
5690
  sections.push("");
5140
5691
  }
5141
5692
  if (page.dormantOverlays.length > 0) {
@@ -5143,6 +5694,11 @@ function buildScopedContext(page, mode) {
5143
5694
  sections.push(formatDormantOverlays(page.dormantOverlays));
5144
5695
  sections.push("");
5145
5696
  }
5697
+ if (quantityElements.length > 0) {
5698
+ sections.push("### Quantity / Count Controls");
5699
+ sections.push(formatQuantityElements(quantityElements));
5700
+ sections.push("");
5701
+ }
5146
5702
  if (page.forms.length > 0) {
5147
5703
  sections.push(`### Forms (${page.forms.length})`);
5148
5704
  sections.push(formatForms(page.forms));
@@ -5175,10 +5731,21 @@ function buildScopedContext(page, mode) {
5175
5731
  case "visible_only": {
5176
5732
  const visibleElements = page.interactiveElements.filter(isVisibleToUser);
5177
5733
  const visibleNav = page.navigation.filter(isVisibleToUser);
5178
- const visibleForms = page.forms.map((form) => ({
5179
- ...form,
5180
- fields: form.fields.filter(isVisibleToUser)
5181
- })).filter((form) => form.fields.length > 0);
5734
+ const dialogFocusedElements = getDialogFocusedElements(page);
5735
+ const visiblePage = {
5736
+ ...page,
5737
+ interactiveElements: dialogFocusedElements.length > 0 ? dialogFocusedElements : visibleElements,
5738
+ forms: page.forms.map((form) => ({
5739
+ ...form,
5740
+ fields: form.fields.filter(
5741
+ (field) => isVisibleToUser(field) && (dialogFocusedElements.length === 0 || field.context === "dialog")
5742
+ )
5743
+ })).filter((form) => form.fields.length > 0)
5744
+ };
5745
+ const quantityElements = getQuantityElements(visiblePage);
5746
+ const cartSnapshot = formatCartSnapshot(visiblePage);
5747
+ const visibleForms = visiblePage.forms;
5748
+ const dialogFocus = formatDialogFocus(page);
5182
5749
  const sections = [];
5183
5750
  sections.push(`**URL:** ${page.url}`);
5184
5751
  sections.push(`**Title:** ${page.title}`);
@@ -5190,6 +5757,11 @@ function buildScopedContext(page, mode) {
5190
5757
  sections.push(formatHighlights(visibleHighlights));
5191
5758
  sections.push("");
5192
5759
  }
5760
+ if (cartSnapshot) {
5761
+ sections.push("### Cart Snapshot");
5762
+ sections.push(cartSnapshot);
5763
+ sections.push("");
5764
+ }
5193
5765
  if ((page.pageIssues?.length ?? 0) > 0) {
5194
5766
  sections.push("### Page Access Warnings");
5195
5767
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5197,7 +5769,18 @@ function buildScopedContext(page, mode) {
5197
5769
  }
5198
5770
  if (page.overlays.length > 0) {
5199
5771
  sections.push("### Active Overlays");
5200
- sections.push(formatOverlays(page.overlays));
5772
+ sections.push(formatOverlays(page));
5773
+ sections.push("");
5774
+ }
5775
+ if (dialogFocus) {
5776
+ sections.push("### Immediate Overlay Actions");
5777
+ sections.push(dialogFocus);
5778
+ if (visibleElements.length > dialogFocusedElements.length) {
5779
+ sections.push("");
5780
+ sections.push(
5781
+ `Background controls hidden while the dialog is active: ${visibleElements.length - dialogFocusedElements.length}`
5782
+ );
5783
+ }
5201
5784
  sections.push("");
5202
5785
  }
5203
5786
  if (page.dormantOverlays.length > 0) {
@@ -5210,11 +5793,18 @@ function buildScopedContext(page, mode) {
5210
5793
  sections.push(formatNavigation(visibleNav));
5211
5794
  sections.push("");
5212
5795
  }
5213
- if (visibleElements.length > 0) {
5796
+ if (quantityElements.length > 0) {
5797
+ sections.push("### Quantity / Count Controls");
5798
+ sections.push(formatQuantityElements(quantityElements));
5799
+ sections.push("");
5800
+ }
5801
+ if (visiblePage.interactiveElements.length > 0) {
5802
+ sections.push(
5803
+ `### Visible In-Viewport Interactive Elements (${visiblePage.interactiveElements.length})`
5804
+ );
5214
5805
  sections.push(
5215
- `### Visible In-Viewport Interactive Elements (${visibleElements.length})`
5806
+ formatInteractiveElements(visiblePage.interactiveElements)
5216
5807
  );
5217
- sections.push(formatInteractiveElements(visibleElements));
5218
5808
  sections.push("");
5219
5809
  }
5220
5810
  if (visibleForms.length > 0) {
@@ -5364,6 +5954,10 @@ function buildStructuredContext(page) {
5364
5954
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5365
5955
  if (page.byline) sections.push(`**Author:** ${page.byline}`);
5366
5956
  if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
5957
+ const structuredScrollHints = getScrollHints(page);
5958
+ if (structuredScrollHints.length > 0) {
5959
+ sections.push(`**Scroll Hint:** ${structuredScrollHints[0]}`);
5960
+ }
5367
5961
  sections.push("");
5368
5962
  const pageIntent = analyzePageIntent(page);
5369
5963
  if (pageIntent) {
@@ -5402,7 +5996,7 @@ function buildStructuredContext(page) {
5402
5996
  sections.push(formatLandmarks(page.landmarks));
5403
5997
  sections.push("");
5404
5998
  sections.push("### Active Overlays / Modals");
5405
- sections.push(formatOverlays(page.overlays));
5999
+ sections.push(formatOverlays(page));
5406
6000
  sections.push("");
5407
6001
  sections.push("### Dormant Consent / Modal UI");
5408
6002
  sections.push(formatDormantOverlays(page.dormantOverlays));
@@ -5481,6 +6075,83 @@ function buildGeneralPrompt(query) {
5481
6075
  user: query
5482
6076
  };
5483
6077
  }
6078
+ const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
6079
+ function stripWrappingQuotes(value) {
6080
+ const trimmed = value.trim();
6081
+ if (trimmed.length < 2) return trimmed;
6082
+ const first = trimmed[0];
6083
+ const last = trimmed[trimmed.length - 1];
6084
+ if (first === last && WRAPPING_QUOTES.has(first)) {
6085
+ return trimmed.slice(1, -1).trim();
6086
+ }
6087
+ return trimmed;
6088
+ }
6089
+ function normalizeArrayItem(value) {
6090
+ if (typeof value === "string") {
6091
+ return stripWrappingQuotes(value).trim();
6092
+ }
6093
+ return String(value).trim();
6094
+ }
6095
+ function normalizeLooseString(value) {
6096
+ if (typeof value !== "string") return void 0;
6097
+ const normalized = stripWrappingQuotes(value);
6098
+ return normalized ? normalized : void 0;
6099
+ }
6100
+ function coerceOptionalNumber(value) {
6101
+ if (typeof value === "number" && Number.isFinite(value)) {
6102
+ return value;
6103
+ }
6104
+ const normalized = normalizeLooseString(value);
6105
+ if (!normalized) return void 0;
6106
+ const parsed = Number(normalized);
6107
+ return Number.isFinite(parsed) ? parsed : void 0;
6108
+ }
6109
+ function coerceStringArray(value) {
6110
+ if (Array.isArray(value)) {
6111
+ return value.map(normalizeArrayItem).filter(Boolean);
6112
+ }
6113
+ const normalized = normalizeLooseString(value);
6114
+ if (!normalized) return [];
6115
+ try {
6116
+ const parsed = JSON.parse(normalized);
6117
+ if (Array.isArray(parsed)) {
6118
+ return parsed.map(normalizeArrayItem).filter(Boolean);
6119
+ }
6120
+ } catch {
6121
+ }
6122
+ const lines = normalized.split(/\r?\n/).map((line) => line.replace(/^\s*[-*]\s+/, "").trim()).filter(Boolean);
6123
+ if (lines.length > 1) {
6124
+ return lines;
6125
+ }
6126
+ return [normalized];
6127
+ }
6128
+ function optionalNumberLikeSchema() {
6129
+ return zod.z.preprocess(
6130
+ (value) => {
6131
+ if (value == null) return void 0;
6132
+ return coerceOptionalNumber(value) ?? value;
6133
+ },
6134
+ zod.z.number().finite().optional()
6135
+ );
6136
+ }
6137
+ function normalizedOptionalStringSchema() {
6138
+ return zod.z.preprocess(
6139
+ (value) => {
6140
+ if (value == null) return void 0;
6141
+ return normalizeLooseString(value) ?? value;
6142
+ },
6143
+ zod.z.string().optional()
6144
+ );
6145
+ }
6146
+ function stringArrayLikeSchema() {
6147
+ return zod.z.preprocess(
6148
+ (value) => {
6149
+ if (value == null) return value;
6150
+ return coerceStringArray(value) ?? value;
6151
+ },
6152
+ zod.z.array(zod.z.string().min(1)).min(1)
6153
+ );
6154
+ }
5484
6155
  const TOOL_DEFINITIONS = [
5485
6156
  // --- Tab Management ---
5486
6157
  {
@@ -5609,7 +6280,9 @@ const TOOL_DEFINITIONS = [
5609
6280
  description: "Scroll the page up or down.",
5610
6281
  inputSchema: {
5611
6282
  direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
5612
- amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
6283
+ amount: optionalNumberLikeSchema().describe(
6284
+ "Pixels to scroll (default 500)"
6285
+ )
5613
6286
  },
5614
6287
  tier: 0,
5615
6288
  relevance: ["ARTICLE", "SEARCH_RESULTS", "PAGINATED_LIST"]
@@ -5654,6 +6327,29 @@ const TOOL_DEFINITIONS = [
5654
6327
  description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions.",
5655
6328
  tier: 1
5656
6329
  },
6330
+ {
6331
+ name: "clear_overlays",
6332
+ title: "Clear Overlays",
6333
+ description: "Work through blocking overlays and modals until the page is unblocked, using overlay-specific heuristics for consent banners and radio-selection dialogs.",
6334
+ inputSchema: {
6335
+ strategy: zod.z.enum(["auto", "interactive"]).optional().describe(
6336
+ 'How aggressively to clear overlays. "auto" uses heuristics; "interactive" stops earlier when human judgment may be needed.'
6337
+ )
6338
+ },
6339
+ tier: 1
6340
+ },
6341
+ {
6342
+ name: "inspect_element",
6343
+ title: "Inspect Element",
6344
+ description: "Inspect one element and its nearest local UI region such as a product card, result row, form section, or modal. Use this instead of reading the whole page when you only need local context.",
6345
+ inputSchema: {
6346
+ index: zod.z.number().optional().describe("Element index to inspect"),
6347
+ selector: zod.z.string().optional().describe("CSS selector to inspect"),
6348
+ limit: zod.z.number().optional().describe("Maximum nearby controls to include (default 8)")
6349
+ },
6350
+ tier: 1,
6351
+ relevance: ["SEARCH_RESULTS", "SHOPPING", "FORM"]
6352
+ },
5657
6353
  {
5658
6354
  name: "read_page",
5659
6355
  title: "Read Page",
@@ -5841,7 +6537,9 @@ const TOOL_DEFINITIONS = [
5841
6537
  inputSchema: {
5842
6538
  index: zod.z.number().optional().describe("Element index from page content to highlight"),
5843
6539
  selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
5844
- text: zod.z.string().optional().describe("Text to find and highlight on the page (all occurrences)"),
6540
+ text: normalizedOptionalStringSchema().describe(
6541
+ "Text to find and highlight on the page (all occurrences)"
6542
+ ),
5845
6543
  label: zod.z.string().optional().describe("Annotation label to display near the highlight"),
5846
6544
  durationMs: zod.z.number().optional().describe(
5847
6545
  "Auto-clear after this many milliseconds (omit for permanent)"
@@ -5866,11 +6564,12 @@ const TOOL_DEFINITIONS = [
5866
6564
  goal: zod.z.string().describe(
5867
6565
  "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
5868
6566
  ),
5869
- steps: zod.z.array(zod.z.string()).describe(
6567
+ steps: stringArrayLikeSchema().describe(
5870
6568
  "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
5871
6569
  )
5872
6570
  },
5873
- tier: 1
6571
+ tier: 1,
6572
+ hiddenByDefault: true
5874
6573
  },
5875
6574
  {
5876
6575
  name: "flow_advance",
@@ -5879,26 +6578,30 @@ const TOOL_DEFINITIONS = [
5879
6578
  inputSchema: {
5880
6579
  detail: zod.z.string().optional().describe("Brief note about what was accomplished")
5881
6580
  },
5882
- tier: 1
6581
+ tier: 1,
6582
+ hiddenByDefault: true
5883
6583
  },
5884
6584
  {
5885
6585
  name: "flow_status",
5886
6586
  title: "Workflow Status",
5887
6587
  description: "Check the current workflow progress.",
5888
- tier: 2
6588
+ tier: 2,
6589
+ hiddenByDefault: true
5889
6590
  },
5890
6591
  {
5891
6592
  name: "flow_end",
5892
6593
  title: "End Workflow",
5893
6594
  description: "Clear the active workflow tracker.",
5894
- tier: 2
6595
+ tier: 2,
6596
+ hiddenByDefault: true
5895
6597
  },
5896
6598
  // --- Speedee System: Suggestion Engine ---
5897
6599
  {
5898
6600
  name: "suggest",
5899
6601
  title: "What Should I Do?",
5900
6602
  description: "Analyze the current page and return the most relevant tools and suggested next actions. Call this when unsure what to do.",
5901
- tier: 1
6603
+ tier: 1,
6604
+ hiddenByDefault: true
5902
6605
  },
5903
6606
  // --- Speedee System: Composable Macros ---
5904
6607
  {
@@ -5910,9 +6613,14 @@ const TOOL_DEFINITIONS = [
5910
6613
  zod.z.object({
5911
6614
  index: zod.z.number().optional().describe("Element index from page content"),
5912
6615
  selector: zod.z.string().optional().describe("CSS selector fallback"),
6616
+ name: zod.z.string().optional().describe("Field name or id, such as custname"),
6617
+ label: zod.z.string().optional().describe("Visible label or aria-label text"),
6618
+ placeholder: zod.z.string().optional().describe("Placeholder text shown in the field"),
5913
6619
  value: zod.z.string().describe("Value to enter")
5914
6620
  })
5915
- ).describe("Fields to fill"),
6621
+ ).describe(
6622
+ "Fields to fill, matched by index, selector, name, label, or placeholder"
6623
+ ),
5916
6624
  submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
5917
6625
  },
5918
6626
  tier: 1,
@@ -5998,14 +6706,16 @@ const TOOL_DEFINITIONS = [
5998
6706
  inputSchema: {
5999
6707
  timeoutMs: zod.z.number().optional().describe("Maximum time to wait in milliseconds (default 10000)")
6000
6708
  },
6001
- tier: 1
6709
+ tier: 1,
6710
+ hiddenByDefault: true
6002
6711
  },
6003
6712
  // --- Speedee System: Metrics ---
6004
6713
  {
6005
6714
  name: "metrics",
6006
6715
  title: "Session Metrics",
6007
6716
  description: "Show performance metrics for this session: total tool calls, average duration, per-tool breakdown, and error rates.",
6008
- tier: 2
6717
+ tier: 2,
6718
+ hiddenByDefault: true
6009
6719
  }
6010
6720
  ];
6011
6721
  function toAnthropicTools(defs) {
@@ -6047,16 +6757,19 @@ const CONTEXT_HINTS = {
6047
6757
  SEARCH_RESULTS: {
6048
6758
  paginate: "⚡ PAGINATION DETECTED — ",
6049
6759
  search: "⚡ Refine search — ",
6760
+ inspect_element: "⚡ Inspect one result card without reading the whole page — ",
6050
6761
  highlight: "💡 Mark interesting results — "
6051
6762
  },
6052
6763
  SHOPPING: {
6053
6764
  fill_form: "⚡ CHECKOUT FIELDS DETECTED — ",
6054
- select_option: "⚡ Payment/shipping options available — "
6765
+ select_option: "⚡ Payment/shipping options available — ",
6766
+ inspect_element: "⚡ Inspect the current product card or option group — "
6055
6767
  },
6056
6768
  FORM: {
6057
6769
  fill_form: "⚡ FORM DETECTED — ",
6058
6770
  select_option: "⚡ Dropdown fields on page — ",
6059
- submit_form: "⚡ Form ready to submit — "
6771
+ submit_form: "⚡ Form ready to submit — ",
6772
+ inspect_element: "⚡ Inspect just this form section — "
6060
6773
  },
6061
6774
  PAGINATED_LIST: {
6062
6775
  paginate: "⚡ PAGINATION DETECTED — ",
@@ -6079,23 +6792,122 @@ function scoreForContext(toolName, pageType) {
6079
6792
  if (tier === 2 && isRelevant) return 20;
6080
6793
  return 40;
6081
6794
  }
6082
- function pruneToolsForContext(tools, pageType) {
6083
- const ctx = pageType ?? "GENERAL";
6084
- const hints = CONTEXT_HINTS[ctx] ?? {};
6085
- const scored = tools.map((tool) => ({
6086
- tool,
6087
- score: scoreForContext(tool.name, ctx)
6088
- }));
6089
- scored.sort((a, b) => a.score - b.score);
6090
- return scored.map(({ tool, score }) => {
6091
- const hint = hints[tool.name];
6092
- if (hint && score <= 20) {
6093
- return {
6094
- ...tool,
6095
- description: hint + tool.description
6096
- };
6097
- }
6098
- return tool;
6795
+ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
6796
+ "current_tab",
6797
+ "navigate",
6798
+ "click",
6799
+ "type_text",
6800
+ "press_key",
6801
+ "search",
6802
+ "scroll",
6803
+ "dismiss_popup",
6804
+ "clear_overlays",
6805
+ "accept_cookies",
6806
+ "wait_for",
6807
+ "read_page",
6808
+ "inspect_element"
6809
+ ]);
6810
+ function inferIntent(query) {
6811
+ const lowered = query.toLowerCase();
6812
+ const intents = /* @__PURE__ */ new Set();
6813
+ if (/\b(tab|tabs|window|windows)\b/.test(lowered)) intents.add("tabs");
6814
+ if (/\b(bookmark|bookmarks|save this|folder)\b/.test(lowered)) {
6815
+ intents.add("bookmarks");
6816
+ }
6817
+ if (/\b(session|cookies|log in|login|sign in|sign-in|resume)\b/.test(lowered)) {
6818
+ intents.add("sessions");
6819
+ }
6820
+ if (/\b(flow|workflow|checkpoint|step|progress|plan)\b/.test(lowered)) {
6821
+ intents.add("workflow");
6822
+ }
6823
+ if (/\b(metric|metrics|performance|slow|latency)\b/.test(lowered)) {
6824
+ intents.add("metrics");
6825
+ }
6826
+ if (/\b(highlight|mark|annotate)\b/.test(lowered)) intents.add("highlight");
6827
+ if (/\b(table|csv|rows|columns)\b/.test(lowered)) intents.add("table");
6828
+ if (/\b(overlay|modal|popup|consent|cookie|blocking ui)\b/.test(lowered)) {
6829
+ intents.add("debug");
6830
+ }
6831
+ if (/\b(debug|diagnose|what should i do|stuck|inspect)\b/.test(lowered)) {
6832
+ intents.add("debug");
6833
+ }
6834
+ return intents;
6835
+ }
6836
+ function shouldIncludeTool(toolName, pageType, intents) {
6837
+ if (ALWAYS_FAST_TOOL_NAMES.has(toolName)) return true;
6838
+ switch (toolName) {
6839
+ case "select_option":
6840
+ case "submit_form":
6841
+ case "fill_form":
6842
+ return pageType === "FORM" || pageType === "SHOPPING" || pageType === "LOGIN";
6843
+ case "paginate":
6844
+ return pageType === "SEARCH_RESULTS" || pageType === "PAGINATED_LIST";
6845
+ case "login":
6846
+ return pageType === "LOGIN" || intents.has("sessions");
6847
+ case "focus":
6848
+ return pageType === "FORM" || pageType === "LOGIN" || pageType === "SEARCH_READY";
6849
+ case "scroll_to_element":
6850
+ return pageType === "SEARCH_RESULTS" || pageType === "SHOPPING" || intents.has("debug");
6851
+ case "go_back":
6852
+ return true;
6853
+ case "go_forward":
6854
+ case "reload":
6855
+ case "hover":
6856
+ return intents.has("debug");
6857
+ case "highlight":
6858
+ case "clear_highlights":
6859
+ return intents.has("highlight");
6860
+ case "list_tabs":
6861
+ case "switch_tab":
6862
+ case "create_tab":
6863
+ case "set_ad_blocking":
6864
+ return intents.has("tabs") || intents.has("debug");
6865
+ case "save_session":
6866
+ case "load_session":
6867
+ case "list_sessions":
6868
+ case "delete_session":
6869
+ return intents.has("sessions");
6870
+ case "list_bookmarks":
6871
+ case "search_bookmarks":
6872
+ case "create_bookmark_folder":
6873
+ case "save_bookmark":
6874
+ case "organize_bookmark":
6875
+ case "archive_bookmark":
6876
+ case "open_bookmark":
6877
+ return intents.has("bookmarks");
6878
+ case "flow_start":
6879
+ case "flow_advance":
6880
+ case "flow_status":
6881
+ case "flow_end":
6882
+ return intents.has("workflow");
6883
+ case "suggest":
6884
+ case "wait_for_navigation":
6885
+ case "metrics":
6886
+ return intents.has("debug") || intents.has("metrics");
6887
+ case "extract_table":
6888
+ return intents.has("table");
6889
+ default:
6890
+ return !defByName[toolName]?.hiddenByDefault;
6891
+ }
6892
+ }
6893
+ function pruneToolsForContext(tools, pageType, query = "") {
6894
+ const ctx = pageType ?? "GENERAL";
6895
+ const hints = CONTEXT_HINTS[ctx] ?? {};
6896
+ const intents = inferIntent(query);
6897
+ const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
6898
+ tool,
6899
+ score: scoreForContext(tool.name, ctx)
6900
+ }));
6901
+ scored.sort((a, b) => a.score - b.score);
6902
+ return scored.map(({ tool, score }) => {
6903
+ const hint = hints[tool.name];
6904
+ if (hint && score <= 20) {
6905
+ return {
6906
+ ...tool,
6907
+ description: hint + tool.description
6908
+ };
6909
+ }
6910
+ return tool;
6099
6911
  });
6100
6912
  }
6101
6913
  function trimText(value) {
@@ -6682,7 +7494,7 @@ async function validateLinkDestination(url, timeoutMs = 3500) {
6682
7494
  function formatDeadLinkMessage(label, result) {
6683
7495
  const destination = result.finalUrl || result.checkedUrl;
6684
7496
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
6685
- return `Skipped stale link "${label}" because ${destination} returned ${status}.`;
7497
+ return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
6686
7498
  }
6687
7499
  const SESSION_VERSION = 1;
6688
7500
  function getSessionsDir() {
@@ -7320,10 +8132,297 @@ async function describeElementForClick$1(wc, selector) {
7320
8132
  href: "href" in result && typeof result.href === "string" ? result.href : void 0
7321
8133
  };
7322
8134
  }
8135
+ async function inspectElement(wc, selector, limit = 8) {
8136
+ const result = await executePageScript(
8137
+ wc,
8138
+ `
8139
+ (function() {
8140
+ function text(value) {
8141
+ const trimmed = value == null ? "" : String(value).trim();
8142
+ return trimmed || undefined;
8143
+ }
8144
+
8145
+ function escapeSelectorValue(value) {
8146
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
8147
+ return CSS.escape(value);
8148
+ }
8149
+ return String(value).replace(/["\\\\]/g, "\\\\$&");
8150
+ }
8151
+
8152
+ function uniqueSelector(candidate) {
8153
+ if (!candidate) return null;
8154
+ try {
8155
+ return document.querySelectorAll(candidate).length === 1 ? candidate : null;
8156
+ } catch {
8157
+ return null;
8158
+ }
8159
+ }
8160
+
8161
+ function uniqueAttributeSelector(el, attribute) {
8162
+ const value = text(el.getAttribute && el.getAttribute(attribute));
8163
+ if (!value) return null;
8164
+ const candidate = el.tagName.toLowerCase() + "[" + attribute + "=\\"" + escapeSelectorValue(value) + "\\"]";
8165
+ return uniqueSelector(candidate);
8166
+ }
8167
+
8168
+ function selectorFor(el) {
8169
+ if (!el) return null;
8170
+ if (el.id) return "#" + escapeSelectorValue(el.id);
8171
+ for (const attribute of ["data-testid", "name", "form", "aria-label", "title"]) {
8172
+ const candidate = uniqueAttributeSelector(el, attribute);
8173
+ if (candidate) return candidate;
8174
+ }
8175
+ const parts = [];
8176
+ let current = el;
8177
+ while (current) {
8178
+ if (current.id) {
8179
+ parts.unshift("#" + escapeSelectorValue(current.id));
8180
+ break;
8181
+ }
8182
+ const tag = current.tagName.toLowerCase();
8183
+ const parent = current.parentElement;
8184
+ if (!parent) { parts.unshift(tag); break; }
8185
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
8186
+ const index = siblings.indexOf(current) + 1;
8187
+ parts.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
8188
+ current = parent;
8189
+ }
8190
+ const selector = parts.join(" > ");
8191
+ return uniqueSelector(selector) || selector;
8192
+ }
8193
+
8194
+ function isVisible(el) {
8195
+ if (!(el instanceof HTMLElement)) return true;
8196
+ const style = window.getComputedStyle(el);
8197
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
8198
+ return false;
8199
+ }
8200
+ const rect = el.getBoundingClientRect();
8201
+ return rect.width > 0 && rect.height > 0;
8202
+ }
8203
+
8204
+ function labelFor(el) {
8205
+ return text(
8206
+ el.getAttribute("aria-label") ||
8207
+ el.getAttribute("title") ||
8208
+ el.getAttribute("name") ||
8209
+ el.getAttribute("placeholder") ||
8210
+ el.textContent ||
8211
+ el.getAttribute("value") ||
8212
+ el.tagName
8213
+ ) || "element";
8214
+ }
8215
+
8216
+ function chooseRegion(target) {
8217
+ const preferred = target.closest(
8218
+ "[data-testid], article, [role='article'], [role='listitem'], li, tr, form, section, aside, dialog, [role='dialog']"
8219
+ );
8220
+ if (preferred) return preferred;
8221
+ let current = target.parentElement;
8222
+ let depth = 0;
8223
+ while (current && depth < 5) {
8224
+ const count = current.querySelectorAll("a[href], button, input, select, textarea").length;
8225
+ if (count >= 2 && count <= 16) return current;
8226
+ current = current.parentElement;
8227
+ depth += 1;
8228
+ }
8229
+ return target.parentElement || target;
8230
+ }
8231
+
8232
+ const target = document.querySelector(${JSON.stringify(selector)});
8233
+ if (!target) return { error: "Element not found" };
8234
+ if (target instanceof HTMLElement) {
8235
+ target.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
8236
+ }
8237
+
8238
+ const region = chooseRegion(target);
8239
+ const nearby = [];
8240
+ const seen = new Set();
8241
+ region.querySelectorAll("a[href], button, input:not([type='hidden']), select, textarea").forEach((el) => {
8242
+ if (!(el instanceof HTMLElement) || !isVisible(el)) return;
8243
+ const candidateSelector = selectorFor(el);
8244
+ if (!candidateSelector || seen.has(candidateSelector)) return;
8245
+ seen.add(candidateSelector);
8246
+ nearby.push({
8247
+ label: labelFor(el).slice(0, 100),
8248
+ type: el.tagName.toLowerCase(),
8249
+ selector: candidateSelector,
8250
+ href: el instanceof HTMLAnchorElement ? text(el.href) : undefined,
8251
+ });
8252
+ });
8253
+
8254
+ return {
8255
+ target: {
8256
+ label: labelFor(target).slice(0, 120),
8257
+ tag: target.tagName.toLowerCase(),
8258
+ text: text(target.textContent)?.slice(0, 240),
8259
+ href: target instanceof HTMLAnchorElement ? text(target.href) : undefined,
8260
+ value: target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement
8261
+ ? text(target.value)?.slice(0, 120)
8262
+ : undefined,
8263
+ },
8264
+ region: {
8265
+ tag: region.tagName.toLowerCase(),
8266
+ label: labelFor(region).slice(0, 120),
8267
+ text: text(region.textContent)?.slice(0, 400),
8268
+ },
8269
+ nearby: nearby.slice(0, ${Math.max(1, Math.min(20, limit))}),
8270
+ };
8271
+ })()
8272
+ `,
8273
+ {
8274
+ timeoutMs: 2e3,
8275
+ label: "inspect element"
8276
+ }
8277
+ );
8278
+ if (result === PAGE_SCRIPT_TIMEOUT) {
8279
+ return pageBusyError("inspect_element");
8280
+ }
8281
+ if (!result || typeof result !== "object") {
8282
+ return "Error: Could not inspect element";
8283
+ }
8284
+ if ("error" in result && typeof result.error === "string") {
8285
+ return `Error: ${result.error}`;
8286
+ }
8287
+ const lines = [];
8288
+ if (result.target) {
8289
+ lines.push(`Target: ${result.target.label} <${result.target.tag}>`);
8290
+ if (result.target.text) lines.push(`Target text: ${result.target.text}`);
8291
+ if (result.target.href) lines.push(`Target href: ${result.target.href}`);
8292
+ if (result.target.value) lines.push(`Target value: ${result.target.value}`);
8293
+ }
8294
+ if (result.region) {
8295
+ lines.push(`Region: ${result.region.label} <${result.region.tag}>`);
8296
+ if (result.region.text) lines.push(`Region text: ${result.region.text}`);
8297
+ }
8298
+ if (Array.isArray(result.nearby) && result.nearby.length > 0) {
8299
+ lines.push("Nearby controls:");
8300
+ for (const item of result.nearby) {
8301
+ const hrefSuffix = item.href ? ` -> ${item.href}` : "";
8302
+ lines.push(
8303
+ `- ${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
8304
+ );
8305
+ }
8306
+ }
8307
+ return lines.join("\n");
8308
+ }
8309
+ async function getLocaleSnapshot(wc) {
8310
+ const snapshot = await executePageScript(
8311
+ wc,
8312
+ `
8313
+ (function() {
8314
+ return {
8315
+ lang:
8316
+ document.documentElement?.lang ||
8317
+ document.body?.lang ||
8318
+ navigator.language ||
8319
+ "",
8320
+ url: window.location.href || "",
8321
+ title: document.title || "",
8322
+ };
8323
+ })()
8324
+ `,
8325
+ {
8326
+ label: "locale snapshot"
8327
+ }
8328
+ );
8329
+ if (!snapshot || snapshot === PAGE_SCRIPT_TIMEOUT || typeof snapshot !== "object") {
8330
+ return null;
8331
+ }
8332
+ return {
8333
+ lang: typeof snapshot.lang === "string" ? snapshot.lang.trim() : "",
8334
+ url: typeof snapshot.url === "string" ? snapshot.url : wc.getURL(),
8335
+ title: typeof snapshot.title === "string" ? snapshot.title : wc.getTitle()
8336
+ };
8337
+ }
8338
+ function primaryLanguageTag(value) {
8339
+ return value.trim().toLowerCase().split(/[-_]/)[0] || "";
8340
+ }
8341
+ function localeChanged(before, after) {
8342
+ if (!before || !after) return false;
8343
+ const beforeLang = primaryLanguageTag(before.lang);
8344
+ const afterLang = primaryLanguageTag(after.lang);
8345
+ if (beforeLang && afterLang && beforeLang !== afterLang) {
8346
+ return true;
8347
+ }
8348
+ const localeHint = /[?&](lang|locale|language|hl)=|\/(ja|jp|en|fr|de|es|it|ko|zh)(\/|$)/i;
8349
+ return before.url !== after.url && localeHint.test(after.url);
8350
+ }
8351
+ async function restoreLocaleSnapshot(wc, snapshot) {
8352
+ if (!snapshot || wc.isDestroyed()) return;
8353
+ try {
8354
+ if (typeof wc.canGoBack === "function" && wc.canGoBack()) {
8355
+ wc.goBack();
8356
+ await waitForLoad$1(wc, 3e3);
8357
+ const reverted = await getLocaleSnapshot(wc);
8358
+ if (!localeChanged(snapshot, reverted)) {
8359
+ return;
8360
+ }
8361
+ }
8362
+ } catch {
8363
+ }
8364
+ if (snapshot.url && snapshot.url !== wc.getURL()) {
8365
+ try {
8366
+ await wc.loadURL(snapshot.url);
8367
+ await waitForLoad$1(wc, 3e3);
8368
+ return;
8369
+ } catch {
8370
+ }
8371
+ }
8372
+ if (snapshot.url) {
8373
+ try {
8374
+ await wc.reload();
8375
+ await waitForLoad$1(wc, 3e3);
8376
+ } catch {
8377
+ }
8378
+ }
8379
+ }
8380
+ const ADD_TO_CART_PATTERNS = [
8381
+ "add to cart",
8382
+ "add to bag",
8383
+ "add to basket",
8384
+ "add to my cart",
8385
+ "add to my bag",
8386
+ "add to my basket",
8387
+ "add item to cart",
8388
+ "add item to bag",
8389
+ "add item to basket"
8390
+ ];
8391
+ const recentCartClicks = /* @__PURE__ */ new Map();
8392
+ const CART_CLICK_COOLDOWN_MS = 15e3;
8393
+ function isAddToCartText(text) {
8394
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
8395
+ return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
8396
+ }
8397
+ function recordCartClick(url, text) {
8398
+ recentCartClicks.set(url, { text, ts: Date.now() });
8399
+ for (const [key, entry] of recentCartClicks) {
8400
+ if (Date.now() - entry.ts > CART_CLICK_COOLDOWN_MS) {
8401
+ recentCartClicks.delete(key);
8402
+ }
8403
+ }
8404
+ }
8405
+ function isDuplicateCartClick(url, text) {
8406
+ const recent = recentCartClicks.get(url);
8407
+ if (!recent) return false;
8408
+ if (Date.now() - recent.ts > CART_CLICK_COOLDOWN_MS) {
8409
+ recentCartClicks.delete(url);
8410
+ return false;
8411
+ }
8412
+ return isAddToCartText(text);
8413
+ }
7323
8414
  async function clickResolvedSelector$1(wc, selector) {
7324
8415
  if (selector.startsWith("__vessel_idx:")) {
7325
8416
  const idx = Number(selector.slice("__vessel_idx:".length));
7326
8417
  const beforeUrl2 = wc.getURL();
8418
+ const idxLabel = await executePageScript(
8419
+ wc,
8420
+ `window.__vessel?.getElementText?.(${idx}) || ""`,
8421
+ { label: "shadow element text" }
8422
+ );
8423
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
8424
+ return `Blocked: "${idxLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
8425
+ }
7327
8426
  const result = await executePageScript(
7328
8427
  wc,
7329
8428
  `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
@@ -7333,12 +8432,29 @@ async function clickResolvedSelector$1(wc, selector) {
7333
8432
  );
7334
8433
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
7335
8434
  if (typeof result === "string" && result.startsWith("Error")) return result;
8435
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
8436
+ recordCartClick(beforeUrl2, idxLabel);
8437
+ }
7336
8438
  await waitForPotentialNavigation$1(wc, beforeUrl2);
7337
8439
  const afterUrl2 = wc.getURL();
7338
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
8440
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
8441
+ const idxOverlay = await detectPostClickOverlay$1(wc);
8442
+ return idxOverlay ? `${result}
8443
+ ${idxOverlay}` : result;
7339
8444
  }
7340
8445
  if (selector.includes(" >>> ")) {
7341
8446
  const beforeUrl2 = wc.getURL();
8447
+ const shadowLabel = await executePageScript(
8448
+ wc,
8449
+ `(function() {
8450
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
8451
+ return el ? (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || "") : "";
8452
+ })()`,
8453
+ { label: "shadow element text" }
8454
+ );
8455
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
8456
+ return `Blocked: "${shadowLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
8457
+ }
7342
8458
  const result = await executePageScript(
7343
8459
  wc,
7344
8460
  `
@@ -7355,19 +8471,48 @@ async function clickResolvedSelector$1(wc, selector) {
7355
8471
  );
7356
8472
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
7357
8473
  if (typeof result === "string" && result.startsWith("Error")) return result;
8474
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
8475
+ recordCartClick(beforeUrl2, shadowLabel);
8476
+ }
7358
8477
  await waitForPotentialNavigation$1(wc, beforeUrl2);
7359
8478
  const afterUrl2 = wc.getURL();
7360
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
8479
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
8480
+ const shadowOverlay = await detectPostClickOverlay$1(wc);
8481
+ return shadowOverlay ? `${result}
8482
+ ${shadowOverlay}` : result;
7361
8483
  }
7362
8484
  const beforeUrl = wc.getURL();
7363
8485
  const elInfo = await describeElementForClick$1(wc, selector);
7364
8486
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
8487
+ const cartMatch = isAddToCartText(elInfo.text);
8488
+ console.log(
8489
+ `[Vessel cart-guard] text="${elInfo.text}" cartMatch=${cartMatch} url=${beforeUrl} hasPrior=${recentCartClicks.has(beforeUrl)}`
8490
+ );
8491
+ if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
8492
+ console.log(`[Vessel cart-guard] BLOCKED duplicate add-to-cart click`);
8493
+ return `Blocked: "${elInfo.text}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
8494
+ }
8495
+ if (!cartMatch && recentCartClicks.has(beforeUrl)) {
8496
+ const dialogActions = await getCartDialogActions$1(wc);
8497
+ if (dialogActions) {
8498
+ console.log(
8499
+ `[Vessel cart-guard] BLOCKED background click while cart dialog is open`
8500
+ );
8501
+ return `Blocked: a cart confirmation dialog is open. Do not click background elements.
8502
+ ${dialogActions}
8503
+ Click one of these dialog actions instead.`;
8504
+ }
8505
+ }
7365
8506
  if (elInfo.href) {
7366
8507
  const validation = await validateLinkDestination(elInfo.href);
7367
8508
  if (validation.status === "dead") {
7368
8509
  return formatDeadLinkMessage(elInfo.text, validation);
7369
8510
  }
7370
8511
  }
8512
+ if (cartMatch) {
8513
+ console.log(`[Vessel cart-guard] RECORDED cart click for url=${beforeUrl}`);
8514
+ recordCartClick(beforeUrl, elInfo.text);
8515
+ }
7371
8516
  const clickText = `Clicked: ${elInfo.text}`;
7372
8517
  const clickResult = await clickElement$1(wc, selector);
7373
8518
  if (clickResult.startsWith("Error:")) return clickResult;
@@ -7376,6 +8521,18 @@ async function clickResolvedSelector$1(wc, selector) {
7376
8521
  if (afterUrl !== beforeUrl) {
7377
8522
  return `${clickText} -> ${afterUrl}`;
7378
8523
  }
8524
+ const overlayHint = await detectPostClickOverlay$1(wc);
8525
+ if (overlayHint) {
8526
+ const dialogActions = cartMatch ? await getCartDialogActions$1(wc) : null;
8527
+ const actionsSuffix = dialogActions ? `
8528
+ ${dialogActions}
8529
+ Click one of these dialog actions. Do NOT click any other element.` : "";
8530
+ return `${clickText} (${clickResult})
8531
+ ${overlayHint}${actionsSuffix}`;
8532
+ }
8533
+ if (cartMatch) {
8534
+ return `${clickText} (${clickResult})`;
8535
+ }
7379
8536
  const activationResult = await activateElement$1(wc, selector);
7380
8537
  if (!activationResult.startsWith("Error:")) {
7381
8538
  await waitForPotentialNavigation$1(wc, beforeUrl);
@@ -7384,14 +8541,226 @@ async function clickResolvedSelector$1(wc, selector) {
7384
8541
  return `${clickText} -> ${fallbackUrl} (recovered via DOM activation)`;
7385
8542
  }
7386
8543
  }
8544
+ const postActivationOverlayHint = await detectPostClickOverlay$1(wc);
8545
+ if (postActivationOverlayHint) {
8546
+ return `${clickText} (${clickResult})
8547
+ ${postActivationOverlayHint}`;
8548
+ }
7387
8549
  return `${clickText} (${clickResult})`;
7388
8550
  }
8551
+ async function getCartDialogActions$1(wc) {
8552
+ const result = await executePageScript(
8553
+ wc,
8554
+ `
8555
+ (function() {
8556
+ var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
8557
+ if (!dialog) return { found: false, actions: [] };
8558
+ var cs = getComputedStyle(dialog);
8559
+ if (cs.display === 'none' || cs.visibility === 'hidden') return { found: false, actions: [] };
8560
+ var text = (dialog.textContent || '').slice(0, 500).toLowerCase();
8561
+ var cartSignals = ['added to cart','added to bag','added to basket',
8562
+ 'item added','your basket','your cart','your bag',
8563
+ 'view basket','view cart','continue shopping'];
8564
+ var isCart = cartSignals.some(function(s) { return text.indexOf(s) !== -1; });
8565
+ if (!isCart) return { found: false, actions: [] };
8566
+ var actions = [];
8567
+ dialog.querySelectorAll('button, a[href], [role="button"]').forEach(function(el) {
8568
+ var cs2 = getComputedStyle(el);
8569
+ if (cs2.display === 'none' || cs2.visibility === 'hidden') return;
8570
+ var r = el.getBoundingClientRect();
8571
+ if (r.width < 20 || r.height < 10) return;
8572
+ var label = (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 80);
8573
+ if (!label || label.length < 2) return;
8574
+ var href = el.getAttribute('href') || '';
8575
+ var sel = el.id ? '#' + el.id
8576
+ : el.getAttribute('data-test') ? '[data-test="' + el.getAttribute('data-test') + '"]'
8577
+ : el.getAttribute('aria-label') ? '[aria-label="' + el.getAttribute('aria-label') + '"]'
8578
+ : null;
8579
+ if (sel) actions.push({ label: label, selector: sel, href: href });
8580
+ });
8581
+ return {
8582
+ found: true,
8583
+ actions: actions.map(function(a) {
8584
+ return '- "' + a.label + '"' + (a.href ? ' → ' + a.href : '') + (a.selector ? ' (selector: ' + a.selector + ')' : '');
8585
+ }),
8586
+ };
8587
+ })()
8588
+ `,
8589
+ { timeoutMs: 800, label: "get cart dialog actions" }
8590
+ );
8591
+ if (!result || result === PAGE_SCRIPT_TIMEOUT || !result.found) return null;
8592
+ if (result.actions.length === 0) return null;
8593
+ return `Available dialog actions:
8594
+ ${result.actions.join("\n")}`;
8595
+ }
8596
+ async function detectPostClickOverlay$1(wc) {
8597
+ const result = await executePageScript(
8598
+ wc,
8599
+ `
8600
+ (function() {
8601
+ var vw = window.innerWidth || document.documentElement.clientWidth;
8602
+ var vh = window.innerHeight || document.documentElement.clientHeight;
8603
+ var vpArea = Math.max(1, vw * vh);
8604
+
8605
+ function isVis(el) {
8606
+ var cs = getComputedStyle(el);
8607
+ return cs.display !== 'none' && cs.visibility !== 'hidden' &&
8608
+ el.getBoundingClientRect().width > 0;
8609
+ }
8610
+
8611
+ function hasFixedAncestor(el) {
8612
+ var cur = el.parentElement;
8613
+ while (cur && cur !== document.body) {
8614
+ var ps = getComputedStyle(cur).position;
8615
+ if (ps === 'fixed' || ps === 'sticky') return true;
8616
+ cur = cur.parentElement;
8617
+ }
8618
+ return false;
8619
+ }
8620
+
8621
+ function effectiveZ(el) {
8622
+ var cur = el;
8623
+ while (cur && cur !== document.body) {
8624
+ var z = parseInt(getComputedStyle(cur).zIndex, 10);
8625
+ if (z > 0) return z;
8626
+ cur = cur.parentElement;
8627
+ }
8628
+ return 0;
8629
+ }
8630
+
8631
+ function edgePad(r) {
8632
+ return r.left <= 24 || r.top <= 24 ||
8633
+ r.right >= vw - 24 || r.bottom >= vh - 24;
8634
+ }
8635
+
8636
+ var cartPhrases = ['added to cart','added to bag','added to basket',
8637
+ 'added to your cart','added to your bag','added to your basket'];
8638
+ var cartActions = ['view cart','go to cart','continue shopping',
8639
+ 'keep shopping','checkout','view basket','go to basket'];
8640
+
8641
+ // Phase 1: semantic dialog elements
8642
+ var selectors = 'dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"]';
8643
+ var candidates = document.querySelectorAll(selectors);
8644
+ var hit = null;
8645
+ for (var j = 0; j < candidates.length; j++) {
8646
+ if (isVis(candidates[j])) { hit = candidates[j]; break; }
8647
+ }
8648
+
8649
+ // Phase 2: positioned drawer-like elements
8650
+ if (!hit) {
8651
+ var els = document.querySelectorAll('*');
8652
+ for (var i = 0; i < els.length; i++) {
8653
+ var s = getComputedStyle(els[i]);
8654
+ if (s.display === 'none' || s.visibility === 'hidden') continue;
8655
+ var pos = s.position;
8656
+ var isFixed = pos === 'fixed' || pos === 'sticky';
8657
+ var isAbs = pos === 'absolute';
8658
+ if (!isFixed && !isAbs) continue;
8659
+ if (isAbs && !hasFixedAncestor(els[i])) continue;
8660
+ if (effectiveZ(els[i]) < 5) continue;
8661
+ var r = els[i].getBoundingClientRect();
8662
+ var area = (r.width * r.height) / vpArea;
8663
+ if (r.width >= 160 && r.height >= 100 && area >= 0.05 && edgePad(r)) {
8664
+ hit = els[i]; break;
8665
+ }
8666
+ }
8667
+ }
8668
+
8669
+ // Phase 3: text-based fallback — any positioned element with cart confirmation text
8670
+ if (!hit) {
8671
+ var els2 = document.querySelectorAll('*');
8672
+ for (var k = 0; k < els2.length; k++) {
8673
+ var s2 = getComputedStyle(els2[k]);
8674
+ if (s2.display === 'none' || s2.visibility === 'hidden') continue;
8675
+ var p2 = s2.position;
8676
+ if (p2 !== 'fixed' && p2 !== 'sticky' && p2 !== 'absolute') continue;
8677
+ var r2 = els2[k].getBoundingClientRect();
8678
+ if (r2.width < 120 || r2.height < 80) continue;
8679
+ var innerText = (els2[k].textContent || '').slice(0, 500).toLowerCase();
8680
+ var hasConfirm = cartPhrases.some(function(ph) { return innerText.indexOf(ph) !== -1; });
8681
+ if (hasConfirm) { hit = els2[k]; break; }
8682
+ }
8683
+ }
8684
+
8685
+ if (!hit) return { found: false, label: '', cartLike: false };
8686
+ var text = (hit.textContent || '').slice(0, 500).toLowerCase();
8687
+ var cartLike = cartPhrases.concat(cartActions).some(function(s) { return text.indexOf(s) !== -1; });
8688
+ var label = (hit.getAttribute('aria-label') || (hit.querySelector('h1,h2,h3,h4') || {}).textContent || '').trim().slice(0, 80);
8689
+ return { found: true, label: label, cartLike: cartLike };
8690
+ })()
8691
+ `,
8692
+ { timeoutMs: 800, label: "post-click overlay check" }
8693
+ );
8694
+ if (!result || result === PAGE_SCRIPT_TIMEOUT || !result.found) return null;
8695
+ if (result.cartLike) {
8696
+ const desc2 = result.label ? ` ("${result.label}")` : "";
8697
+ return `A cart confirmation dialog appeared${desc2}. Call read_page to see available actions — do not click Add to Cart again.`;
8698
+ }
8699
+ const desc = result.label ? ` ("${result.label}")` : "";
8700
+ return `A dialog or overlay appeared${desc}. Call read_page to see available actions.`;
8701
+ }
7389
8702
  async function dismissPopup$1(wc) {
7390
8703
  const before = await extractContent(wc);
7391
8704
  const initialBlocking = before.overlays.filter(
7392
8705
  (overlay) => overlay.blocksInteraction
7393
8706
  ).length;
8707
+ if (initialBlocking > 0) {
8708
+ const overlayText = before.overlays.map((o) => [o.label, o.text].filter(Boolean).join(" ")).join(" ").toLowerCase();
8709
+ const cartSignals = [
8710
+ "added to cart",
8711
+ "added to bag",
8712
+ "added to basket",
8713
+ "item added",
8714
+ "items in your basket",
8715
+ "items in your cart",
8716
+ "items in your bag",
8717
+ "your basket",
8718
+ "your cart",
8719
+ "your bag",
8720
+ "view basket",
8721
+ "view cart",
8722
+ "continue shopping"
8723
+ ];
8724
+ if (cartSignals.some((s) => overlayText.includes(s))) {
8725
+ const continueResult = await executePageScript(
8726
+ wc,
8727
+ `
8728
+ (function() {
8729
+ var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
8730
+ if (!dialog) return "Error: dialog not found";
8731
+ var buttons = dialog.querySelectorAll('button, a[href], [role="button"]');
8732
+ var continueBtn = null;
8733
+ var viewCartBtn = null;
8734
+ for (var i = 0; i < buttons.length; i++) {
8735
+ var label = (buttons[i].getAttribute('aria-label') || buttons[i].textContent || '').trim().toLowerCase();
8736
+ if (/continue shopping|keep shopping/.test(label)) { continueBtn = buttons[i]; break; }
8737
+ if (/view (basket|cart|bag)|checkout/.test(label) && !viewCartBtn) { viewCartBtn = buttons[i]; }
8738
+ }
8739
+ var target = continueBtn || viewCartBtn;
8740
+ if (!target) return "Error: no dialog action found";
8741
+ var actionLabel = (target.getAttribute('aria-label') || target.textContent || '').trim();
8742
+ if (target.tagName === 'A' && target.href) {
8743
+ window.location.href = target.href;
8744
+ return "Clicked: " + actionLabel + " -> " + target.href;
8745
+ }
8746
+ target.click();
8747
+ return "Clicked: " + actionLabel;
8748
+ })()
8749
+ `,
8750
+ { timeoutMs: 1500, label: "cart dialog continue shopping" }
8751
+ );
8752
+ if (continueResult && continueResult !== PAGE_SCRIPT_TIMEOUT && typeof continueResult === "string" && !continueResult.startsWith("Error")) {
8753
+ console.log(
8754
+ `[Vessel cart-guard] dismiss_popup auto-clicked dialog action: ${continueResult}`
8755
+ );
8756
+ return `Cart confirmation handled: ${continueResult}. Item was already added to your cart.`;
8757
+ }
8758
+ const dialogActions = await getCartDialogActions$1(wc);
8759
+ return `Cannot dismiss: this is a cart confirmation dialog. Item is in your cart.${dialogActions ? "\n" + dialogActions + "\nClick one of these instead." : " Use read_page to see dialog actions."}`;
8760
+ }
8761
+ }
7394
8762
  const initialDormant = before.dormantOverlays.length;
8763
+ const initialLocale = await getLocaleSnapshot(wc);
7395
8764
  const candidates = await executePageScript(
7396
8765
  wc,
7397
8766
  `
@@ -7503,7 +8872,8 @@ async function dismissPopup$1(wc) {
7503
8872
  ).toLowerCase();
7504
8873
  const classText = text(typeof el.className === "string" ? el.className : "").toLowerCase();
7505
8874
  const idText = text(el.id).toLowerCase();
7506
- const combined = classText + " " + idText;
8875
+ const hrefText = text(el.getAttribute && el.getAttribute("href")).toLowerCase();
8876
+ const combined = label + " " + classText + " " + idText + " " + hrefText;
7507
8877
  let score = rooted ? 30 : 0;
7508
8878
  if (/^x$|^×$/.test(label)) score += 120;
7509
8879
  if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you|reject|decline/.test(label)) score += 100;
@@ -7513,6 +8883,11 @@ async function dismissPopup$1(wc) {
7513
8883
  // OneTrust "Accept" is valid for dismissing the banner (user just wants it gone)
7514
8884
  if (/onetrust-accept|cookie.*accept|consent.*accept/.test(combined)) score += 80;
7515
8885
  if (el.getAttribute("aria-label")) score += 20;
8886
+ if (/(language|locale|region|country|currency)\b/.test(combined)) score -= 320;
8887
+ if (/\b(english|japanese|japan|francais|espanol|deutsch|italiano|portuguese|nihongo)\b/.test(label)) score -= 280;
8888
+ if (/日本語|中文|한국어/.test(label)) score -= 280;
8889
+ if (/[?&](lang|locale|language|hl)=/.test(hrefText)) score -= 260;
8890
+ if (//(ja|jp|en|fr|de|es|it|ko|zh)(/|$)/.test(hrefText)) score -= 220;
7516
8891
  // Penalize general accept/subscribe buttons that aren't consent-related
7517
8892
  if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label) && !/cookie|consent|onetrust/.test(combined)) score -= 80;
7518
8893
  const rect = el.getBoundingClientRect();
@@ -7590,6 +8965,11 @@ async function dismissPopup$1(wc) {
7590
8965
  const result = await clickElement$1(wc, candidate.selector);
7591
8966
  if (result.startsWith("Error:")) continue;
7592
8967
  await sleep$1(250);
8968
+ const postClickLocale = await getLocaleSnapshot(wc);
8969
+ if (localeChanged(initialLocale, postClickLocale)) {
8970
+ await restoreLocaleSnapshot(wc, initialLocale);
8971
+ continue;
8972
+ }
7593
8973
  const after = await extractContent(wc);
7594
8974
  const blocking = after.overlays.filter(
7595
8975
  (overlay) => overlay.blocksInteraction
@@ -7613,6 +8993,93 @@ async function dismissPopup$1(wc) {
7613
8993
  }
7614
8994
  return initialBlocking > 0 ? "Could not dismiss the blocking popup automatically" : initialDormant > 0 ? `No active blocking popup detected. Found ${initialDormant} dormant consent/modal surface(s) in the DOM, likely geo-gated or inactive in this session.` : "No blocking popup detected";
7615
8995
  }
8996
+ function describeOverlayState(page) {
8997
+ const inventory = buildOverlayInventory(page);
8998
+ return {
8999
+ inventory,
9000
+ blocking: inventory.filter((overlay) => overlay.blocksInteraction).length,
9001
+ total: inventory.length,
9002
+ signature: getBlockingOverlaySignature(inventory)
9003
+ };
9004
+ }
9005
+ async function clickOverlayCandidate(wc, action) {
9006
+ if (!action?.selector) return null;
9007
+ const result = await clickResolvedSelector$1(wc, action.selector);
9008
+ return `${action.label || action.selector}: ${result}`;
9009
+ }
9010
+ async function clearOverlays(wc, strategy = "auto") {
9011
+ const steps = [];
9012
+ let cleared = 0;
9013
+ const maxIterations = 8;
9014
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
9015
+ const before = await extractContent(wc);
9016
+ const beforeState = describeOverlayState(before);
9017
+ const blockingOverlays = beforeState.inventory.filter(
9018
+ (overlay2) => overlay2.blocksInteraction
9019
+ );
9020
+ if (blockingOverlays.length === 0) {
9021
+ if (cleared === 0) return "No blocking overlays detected";
9022
+ steps.push(`Overlays remaining: ${beforeState.total}`);
9023
+ steps.push("Page still blocked: false");
9024
+ return steps.join("\n");
9025
+ }
9026
+ const overlay = blockingOverlays[0];
9027
+ let actionMessage = null;
9028
+ if (overlay.kind === "cookie_consent") {
9029
+ actionMessage = await clickOverlayCandidate(
9030
+ wc,
9031
+ overlay.acceptAction || overlay.dismissAction || overlay.actions[0]
9032
+ );
9033
+ } else if (overlay.kind === "selection_modal") {
9034
+ if (!overlay.correctOption?.selector) {
9035
+ if (strategy === "interactive") {
9036
+ steps.push(
9037
+ "Stopped: selection modal needs human judgment because no likely-correct option was detected."
9038
+ );
9039
+ steps.push(`Overlays remaining: ${beforeState.total}`);
9040
+ steps.push("Page still blocked: true");
9041
+ return steps.join("\n");
9042
+ }
9043
+ } else {
9044
+ const optionResult = await clickOverlayCandidate(
9045
+ wc,
9046
+ overlay.correctOption
9047
+ );
9048
+ if (optionResult) {
9049
+ actionMessage = `Selected likely-correct option: ${optionResult}`;
9050
+ await sleep$1(120);
9051
+ const submitResult = await clickOverlayCandidate(
9052
+ wc,
9053
+ overlay.submitAction || overlay.acceptAction
9054
+ );
9055
+ if (submitResult) {
9056
+ actionMessage += `
9057
+ Submitted modal: ${submitResult}`;
9058
+ }
9059
+ }
9060
+ }
9061
+ }
9062
+ if (!actionMessage) {
9063
+ actionMessage = `Fallback popup handling: ${await dismissPopup$1(wc)}`;
9064
+ }
9065
+ steps.push(actionMessage);
9066
+ await sleep$1(250);
9067
+ const after = await extractContent(wc);
9068
+ const afterState = describeOverlayState(after);
9069
+ steps.push(`Overlays remaining: ${afterState.total}`);
9070
+ steps.push(`Page still blocked: ${afterState.blocking > 0}`);
9071
+ if (afterState.blocking === 0) {
9072
+ return steps.join("\n");
9073
+ }
9074
+ const progressMade = afterState.blocking < beforeState.blocking || afterState.total !== beforeState.total || afterState.signature !== beforeState.signature;
9075
+ if (progressMade) {
9076
+ cleared += 1;
9077
+ continue;
9078
+ }
9079
+ return steps.join("\n");
9080
+ }
9081
+ return steps.join("\n");
9082
+ }
7616
9083
  async function resolveSelector$1(wc, index, selector) {
7617
9084
  if (selector) return selector;
7618
9085
  if (index == null) return null;
@@ -7727,6 +9194,197 @@ async function resolveSelector$1(wc, index, selector) {
7727
9194
  if (extractedSelector) return extractedSelector;
7728
9195
  return null;
7729
9196
  }
9197
+ function normalizeFieldToken(value) {
9198
+ return typeof value === "string" ? value.trim() : "";
9199
+ }
9200
+ function describeFillField(field) {
9201
+ if (field.selector) return `selector=${field.selector}`;
9202
+ if (field.index != null) return `index=${field.index}`;
9203
+ if (field.name) return `name=${field.name}`;
9204
+ if (field.label) return `label=${field.label}`;
9205
+ if (field.placeholder) return `placeholder=${field.placeholder}`;
9206
+ return "field";
9207
+ }
9208
+ async function resolveFieldSelector(wc, field) {
9209
+ const directSelector = await resolveSelector$1(wc, field.index, field.selector);
9210
+ if (directSelector) return directSelector;
9211
+ const name = normalizeFieldToken(field.name);
9212
+ const label = normalizeFieldToken(field.label);
9213
+ const placeholder = normalizeFieldToken(field.placeholder);
9214
+ if (!name && !label && !placeholder) return null;
9215
+ const selector = await executePageScript(
9216
+ wc,
9217
+ `
9218
+ (function() {
9219
+ function normalize(value) {
9220
+ return value == null ? "" : String(value).trim().toLowerCase();
9221
+ }
9222
+
9223
+ function text(value) {
9224
+ return value == null ? "" : String(value).trim();
9225
+ }
9226
+
9227
+ function isVisible(el) {
9228
+ if (!(el instanceof HTMLElement)) return true;
9229
+ const style = window.getComputedStyle(el);
9230
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
9231
+ return false;
9232
+ }
9233
+ if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") {
9234
+ return false;
9235
+ }
9236
+ const rect = el.getBoundingClientRect();
9237
+ return rect.width > 0 && rect.height > 0;
9238
+ }
9239
+
9240
+ function escapeSelectorValue(value) {
9241
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
9242
+ return CSS.escape(value);
9243
+ }
9244
+ return String(value).replace(/["\\\\]/g, "\\\\$&");
9245
+ }
9246
+
9247
+ function uniqueSelector(candidate) {
9248
+ if (!candidate) return null;
9249
+ try {
9250
+ return document.querySelectorAll(candidate).length === 1 ? candidate : null;
9251
+ } catch {
9252
+ return null;
9253
+ }
9254
+ }
9255
+
9256
+ function uniqueAttributeSelector(el, attribute) {
9257
+ const value = text(el.getAttribute && el.getAttribute(attribute));
9258
+ if (!value) return null;
9259
+ const candidate = el.tagName.toLowerCase() + "[" + attribute + "=\\"" + escapeSelectorValue(value) + "\\"]";
9260
+ return uniqueSelector(candidate);
9261
+ }
9262
+
9263
+ function selectorFor(el) {
9264
+ if (!el) return null;
9265
+ if (el.id) return "#" + escapeSelectorValue(el.id);
9266
+ const attributes = ["data-testid", "name", "form", "aria-label", "placeholder"];
9267
+ for (const attribute of attributes) {
9268
+ const attributeCandidate = uniqueAttributeSelector(el, attribute);
9269
+ if (attributeCandidate) return attributeCandidate;
9270
+ }
9271
+ const parts = [];
9272
+ let current = el;
9273
+ while (current) {
9274
+ if (current.id) {
9275
+ parts.unshift("#" + escapeSelectorValue(current.id));
9276
+ break;
9277
+ }
9278
+ const tag = current.tagName.toLowerCase();
9279
+ const parent = current.parentElement;
9280
+ if (!parent) {
9281
+ parts.unshift(tag);
9282
+ break;
9283
+ }
9284
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
9285
+ const index = siblings.indexOf(current) + 1;
9286
+ parts.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
9287
+ current = parent;
9288
+ }
9289
+ const candidate = parts.join(" > ");
9290
+ return uniqueSelector(candidate) || candidate;
9291
+ }
9292
+
9293
+ function getLabelText(el) {
9294
+ const parts = [];
9295
+ if (el.labels) {
9296
+ Array.from(el.labels).forEach((labelEl) => {
9297
+ const value = text(labelEl.textContent);
9298
+ if (value) parts.push(value);
9299
+ });
9300
+ }
9301
+ const ariaLabel = text(el.getAttribute && el.getAttribute("aria-label"));
9302
+ if (ariaLabel) parts.push(ariaLabel);
9303
+ const labelledBy = text(el.getAttribute && el.getAttribute("aria-labelledby"));
9304
+ if (labelledBy) {
9305
+ labelledBy.split(/\\s+/).forEach((id) => {
9306
+ const ref = document.getElementById(id);
9307
+ const value = text(ref && ref.textContent);
9308
+ if (value) parts.push(value);
9309
+ });
9310
+ }
9311
+ return normalize(parts.join(" "));
9312
+ }
9313
+
9314
+ function scoreField(el) {
9315
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
9316
+ return -1;
9317
+ }
9318
+ if (!isVisible(el) || el.disabled || el.getAttribute("aria-disabled") === "true") {
9319
+ return -1;
9320
+ }
9321
+
9322
+ const normalizedName = normalize(el.getAttribute("name")) || normalize(el.id);
9323
+ const normalizedLabel = getLabelText(el);
9324
+ const normalizedPlaceholder = normalize(el.getAttribute("placeholder"));
9325
+ let score = 0;
9326
+
9327
+ if (${JSON.stringify(name)}) {
9328
+ if (normalizedName === ${JSON.stringify(name.toLowerCase())}) score += 120;
9329
+ else if (normalizedName.includes(${JSON.stringify(name.toLowerCase())})) score += 70;
9330
+ }
9331
+
9332
+ if (${JSON.stringify(label)}) {
9333
+ if (normalizedLabel === ${JSON.stringify(label.toLowerCase())}) score += 110;
9334
+ else if (normalizedLabel.includes(${JSON.stringify(label.toLowerCase())})) score += 65;
9335
+ }
9336
+
9337
+ if (${JSON.stringify(placeholder)}) {
9338
+ if (normalizedPlaceholder === ${JSON.stringify(placeholder.toLowerCase())}) score += 105;
9339
+ else if (normalizedPlaceholder.includes(${JSON.stringify(placeholder.toLowerCase())})) score += 60;
9340
+ }
9341
+
9342
+ if (score === 0) return -1;
9343
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) score += 5;
9344
+ return score;
9345
+ }
9346
+
9347
+ const candidates = Array.from(document.querySelectorAll("input, textarea, select"));
9348
+ let best = null;
9349
+ let bestScore = -1;
9350
+ for (const el of candidates) {
9351
+ const score = scoreField(el);
9352
+ if (score > bestScore) {
9353
+ best = el;
9354
+ bestScore = score;
9355
+ }
9356
+ }
9357
+
9358
+ return best ? selectorFor(best) : null;
9359
+ })()
9360
+ `,
9361
+ {
9362
+ label: "resolve form field"
9363
+ }
9364
+ );
9365
+ return typeof selector === "string" && selector ? selector : null;
9366
+ }
9367
+ async function fillFormFields(wc, fields) {
9368
+ const results = [];
9369
+ for (const field of fields) {
9370
+ const selector = await resolveFieldSelector(wc, field);
9371
+ if (!selector) {
9372
+ results.push({
9373
+ field,
9374
+ selector: null,
9375
+ result: `Skipped: no selector for ${describeFillField(field)}`
9376
+ });
9377
+ continue;
9378
+ }
9379
+ const result = await setElementValue$1(
9380
+ wc,
9381
+ selector,
9382
+ String(field.value || "")
9383
+ );
9384
+ results.push({ field, selector, result });
9385
+ }
9386
+ return results;
9387
+ }
7730
9388
  function getTabByMatch$1(tabManager, match) {
7731
9389
  if (!match) return null;
7732
9390
  const lowered = match.toLowerCase();
@@ -8327,7 +9985,9 @@ async function getPostActionState$1(ctx, name) {
8327
9985
  "select_option",
8328
9986
  "hover",
8329
9987
  "focus",
8330
- "fill_form"
9988
+ "fill_form",
9989
+ "inspect_element",
9990
+ "clear_overlays"
8331
9991
  ];
8332
9992
  const tabActions = [
8333
9993
  "create_tab",
@@ -8367,6 +10027,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
8367
10027
  "go_forward",
8368
10028
  "reload",
8369
10029
  "click",
10030
+ "inspect_element",
8370
10031
  "type_text",
8371
10032
  "select_option",
8372
10033
  "submit_form",
@@ -8376,6 +10037,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
8376
10037
  "focus",
8377
10038
  "set_ad_blocking",
8378
10039
  "dismiss_popup",
10040
+ "clear_overlays",
8379
10041
  "read_page",
8380
10042
  "wait_for",
8381
10043
  "create_checkpoint",
@@ -8508,6 +10170,10 @@ async function executeAction(name, args, ctx) {
8508
10170
  }
8509
10171
  case "navigate": {
8510
10172
  if (!wc || !tabId) return "Error: No active tab";
10173
+ const navValidation = await validateLinkDestination(args.url);
10174
+ if (navValidation.status === "dead") {
10175
+ return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
10176
+ }
8511
10177
  ctx.tabManager.navigateTab(tabId, args.url);
8512
10178
  await waitForLoad$1(wc);
8513
10179
  return `Navigated to ${wc.getURL()}${getPostNavSummary(wc)}`;
@@ -8546,6 +10212,16 @@ async function executeAction(name, args, ctx) {
8546
10212
  if (!selector) return "Error: No element index or selector provided";
8547
10213
  return clickResolvedSelector$1(wc, selector);
8548
10214
  }
10215
+ case "inspect_element": {
10216
+ if (!wc) return "Error: No active tab";
10217
+ const selector = await resolveSelector$1(wc, args.index, args.selector);
10218
+ if (!selector) return "Error: No element index or selector provided";
10219
+ return inspectElement(
10220
+ wc,
10221
+ selector,
10222
+ typeof args.limit === "number" ? args.limit : 8
10223
+ );
10224
+ }
8549
10225
  case "type_text": {
8550
10226
  if (!wc) return "Error: No active tab";
8551
10227
  const selector = await resolveSelector$1(wc, args.index, args.selector);
@@ -8589,7 +10265,7 @@ async function executeAction(name, args, ctx) {
8589
10265
  }
8590
10266
  case "scroll": {
8591
10267
  if (!wc) return "Error: No active tab";
8592
- const pixels = args.amount || 500;
10268
+ const pixels = coerceOptionalNumber(args.amount) ?? 500;
8593
10269
  const dir = args.direction === "up" ? -pixels : pixels;
8594
10270
  const result2 = await scrollPage$1(wc, dir);
8595
10271
  return `Scrolled ${args.direction} by ${pixels}px (moved ${Math.abs(result2.movedY)}px, now at y=${Math.round(result2.afterY)})`;
@@ -8634,6 +10310,11 @@ async function executeAction(name, args, ctx) {
8634
10310
  if (!wc) return "Error: No active tab";
8635
10311
  return dismissPopup$1(wc);
8636
10312
  }
10313
+ case "clear_overlays": {
10314
+ if (!wc) return "Error: No active tab";
10315
+ const strategy = args.strategy === "interactive" ? "interactive" : "auto";
10316
+ return clearOverlays(wc, strategy);
10317
+ }
8637
10318
  case "read_page": {
8638
10319
  if (!wc) return "Error: No active tab";
8639
10320
  console.log("[Vessel read_page] starting extraction with 6s timeout");
@@ -9000,9 +10681,9 @@ ${truncated}`;
9000
10681
  if (!wc) return "Error: No active tab";
9001
10682
  const selector = await resolveSelector$1(wc, args.index, args.selector);
9002
10683
  const highlightColor = args.color || "yellow";
10684
+ const highlightText = normalizeLooseString(args.text);
9003
10685
  const url = wc.getURL();
9004
10686
  if (url && url !== "about:blank") {
9005
- const highlightText = typeof args.text === "string" ? args.text : void 0;
9006
10687
  addHighlight(
9007
10688
  url,
9008
10689
  typeof selector === "string" ? selector : void 0,
@@ -9015,7 +10696,7 @@ ${truncated}`;
9015
10696
  return highlightOnPage(
9016
10697
  wc,
9017
10698
  selector,
9018
- args.text,
10699
+ highlightText,
9019
10700
  args.label,
9020
10701
  args.durationMs,
9021
10702
  highlightColor
@@ -9028,7 +10709,7 @@ ${truncated}`;
9028
10709
  // --- Speedee System ---
9029
10710
  case "flow_start": {
9030
10711
  const goal = typeof args.goal === "string" ? args.goal : "";
9031
- const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
10712
+ const steps = coerceStringArray(args.steps) ?? [];
9032
10713
  if (!goal || steps.length === 0)
9033
10714
  return "Error: goal and steps are required";
9034
10715
  const flow = ctx.runtime.startFlow(goal, steps, wc?.getURL());
@@ -9088,9 +10769,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
9088
10769
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
9089
10770
  if (hasOverlays) {
9090
10771
  suggestions.push("BLOCKING OVERLAY detected — dismiss it first:");
9091
- suggestions.push(
9092
- " → dismiss_popup or click on close/accept button"
9093
- );
10772
+ suggestions.push(" → clear_overlays for stacked modals");
10773
+ suggestions.push(" → or dismiss_popup for a single popup");
9094
10774
  suggestions.push("");
9095
10775
  }
9096
10776
  if (hasPasswordField) {
@@ -9108,6 +10788,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
9108
10788
  );
9109
10789
  } else if (hasSearchInput && linkCount >= 10) {
9110
10790
  suggestions.push("SEARCH RESULTS detected:");
10791
+ suggestions.push(
10792
+ " → inspect_element(index) to inspect one result card"
10793
+ );
9111
10794
  suggestions.push(" → click on a result link");
9112
10795
  if (hasPagination)
9113
10796
  suggestions.push(" → paginate('next') for more results");
@@ -9145,26 +10828,10 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
9145
10828
  if (!wc) return "Error: No active tab";
9146
10829
  const fields = Array.isArray(args.fields) ? args.fields : [];
9147
10830
  if (fields.length === 0) return "Error: No fields provided";
9148
- const results = [];
9149
- for (const field of fields) {
9150
- const sel = await resolveSelector$1(wc, field.index, field.selector);
9151
- if (!sel) {
9152
- results.push(`Skipped: no selector for index=${field.index}`);
9153
- continue;
9154
- }
9155
- const result2 = await setElementValue$1(
9156
- wc,
9157
- sel,
9158
- String(field.value || "")
9159
- );
9160
- results.push(result2);
9161
- }
10831
+ const fillResults = await fillFormFields(wc, fields);
10832
+ const results = fillResults.map((item) => item.result);
9162
10833
  if (args.submit) {
9163
- const firstSel = await resolveSelector$1(
9164
- wc,
9165
- fields[0]?.index,
9166
- fields[0]?.selector
9167
- );
10834
+ const firstSel = fillResults.find((item) => item.selector)?.selector ?? null;
9168
10835
  if (firstSel) {
9169
10836
  const beforeUrl = wc.getURL();
9170
10837
  const submitResult = await submitForm$1(wc, { selector: firstSel });
@@ -9264,6 +10931,36 @@ ${steps.join("\n")}`;
9264
10931
  if (!wc) return "Error: No active tab";
9265
10932
  const query = String(args.query || "");
9266
10933
  if (!query) return "Error: No search query provided.";
10934
+ const queryLower = query.toLowerCase().trim();
10935
+ const buttonLikePatterns = [
10936
+ "add to cart",
10937
+ "add to bag",
10938
+ "add to basket",
10939
+ "buy now",
10940
+ "buy it now",
10941
+ "purchase",
10942
+ "continue shopping",
10943
+ "keep shopping",
10944
+ "view cart",
10945
+ "view bag",
10946
+ "view basket",
10947
+ "go to cart",
10948
+ "go to checkout",
10949
+ "checkout",
10950
+ "check out",
10951
+ "proceed to checkout",
10952
+ "place order",
10953
+ "submit",
10954
+ "subscribe",
10955
+ "sign up",
10956
+ "sign in",
10957
+ "log in",
10958
+ "register",
10959
+ "continue"
10960
+ ];
10961
+ if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
10962
+ return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
10963
+ }
9267
10964
  const searchInfo = args.selector ? { selector: args.selector, formAction: null, formMethod: null } : await executePageScript(
9268
10965
  wc,
9269
10966
  `
@@ -9634,8 +11331,11 @@ Instructions:
9634
11331
  - Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.
9635
11332
  - After navigating to a new site, DO NOT call read_page immediately. Instead, act on what you already know: use the search tool to search the site, type_text to enter queries in search bars, or click on known navigation patterns. You know what major sites look like — use that knowledge. Only call read_page if you're genuinely stuck and need to discover unfamiliar page structure.
9636
11333
  - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
11334
+ - When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
9637
11335
  - Escalate page reads progressively: read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
9638
11336
  - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
11337
+ - VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
11338
+ - read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
9639
11339
  - After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
9640
11340
  - If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.
9641
11341
  - If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.
@@ -9652,7 +11352,11 @@ Instructions:
9652
11352
  - USE YOUR KNOWLEDGE: You have broad, practical knowledge about technology, products, cooking, travel, finance, and countless other domains. When the user asks for recommendations, GIVE them — don't deflect to Reddit, YouTubers, or other sources. You know enough to recommend PC parts, suggest restaurants, pick a good laptop, or advise on most consumer decisions. Make a clear recommendation, explain your reasoning briefly, and then execute. If there's genuine ambiguity (e.g. AMD vs Intel is preference-dependent), state your pick and why, then ask only the questions that would actually change your recommendation. Never refuse a recommendation by claiming you're "not an expert" — the user chose to ask you, so help them.
9653
11353
  - NEVER USE EMOJIS unless the user uses them first.`;
9654
11354
  const actionCtx = { tabManager, runtime };
9655
- const contextualTools = pruneToolsForContext(AGENT_TOOLS, pageType);
11355
+ const contextualTools = pruneToolsForContext(
11356
+ AGENT_TOOLS,
11357
+ pageType,
11358
+ query
11359
+ );
9656
11360
  await provider.streamAgentQuery(
9657
11361
  systemPrompt,
9658
11362
  query,
@@ -10777,16 +12481,42 @@ async function clickResolvedSelector(wc, selector) {
10777
12481
  if (selector.startsWith("__vessel_idx:")) {
10778
12482
  const idx = Number(selector.slice("__vessel_idx:".length));
10779
12483
  const beforeUrl2 = wc.getURL();
12484
+ const idxLabel = await wc.executeJavaScript(
12485
+ `window.__vessel?.getElementText?.(${idx}) || ""`
12486
+ );
12487
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
12488
+ return `Blocked: "${idxLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
12489
+ }
10780
12490
  const result = await wc.executeJavaScript(
10781
12491
  `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`
10782
12492
  );
10783
12493
  if (typeof result === "string" && result.startsWith("Error")) return result;
12494
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
12495
+ recordCartClick(beforeUrl2, idxLabel);
12496
+ }
10784
12497
  await waitForPotentialNavigation(wc, beforeUrl2);
10785
12498
  const afterUrl2 = wc.getURL();
10786
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
12499
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
12500
+ const overlayHint2 = await detectPostClickOverlay(wc);
12501
+ if (!overlayHint2) return result;
12502
+ const dialogActions = typeof idxLabel === "string" && isAddToCartText(idxLabel) ? await getCartDialogActions(wc) : null;
12503
+ const actionsSuffix = dialogActions ? `
12504
+ ${dialogActions}
12505
+ Click one of these dialog actions. Do NOT click any other element.` : "";
12506
+ return `${result}
12507
+ ${overlayHint2}${actionsSuffix}`;
10787
12508
  }
10788
12509
  if (selector.includes(" >>> ")) {
10789
12510
  const beforeUrl2 = wc.getURL();
12511
+ const shadowLabel = await wc.executeJavaScript(`
12512
+ (function() {
12513
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
12514
+ return el ? (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || "") : "";
12515
+ })()
12516
+ `);
12517
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
12518
+ return `Blocked: "${shadowLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
12519
+ }
10790
12520
  const result = await wc.executeJavaScript(`
10791
12521
  (function() {
10792
12522
  var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
@@ -10796,19 +12526,45 @@ async function clickResolvedSelector(wc, selector) {
10796
12526
  })()
10797
12527
  `);
10798
12528
  if (typeof result === "string" && result.startsWith("Error")) return result;
12529
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
12530
+ recordCartClick(beforeUrl2, shadowLabel);
12531
+ }
10799
12532
  await waitForPotentialNavigation(wc, beforeUrl2);
10800
12533
  const afterUrl2 = wc.getURL();
10801
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
12534
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
12535
+ const overlayHint2 = await detectPostClickOverlay(wc);
12536
+ if (!overlayHint2) return result;
12537
+ const dialogActions = typeof shadowLabel === "string" && isAddToCartText(shadowLabel) ? await getCartDialogActions(wc) : null;
12538
+ const actionsSuffix = dialogActions ? `
12539
+ ${dialogActions}
12540
+ Click one of these dialog actions. Do NOT click any other element.` : "";
12541
+ return `${result}
12542
+ ${overlayHint2}${actionsSuffix}`;
10802
12543
  }
10803
12544
  const beforeUrl = wc.getURL();
10804
12545
  const elInfo = await describeElementForClick(wc, selector);
10805
12546
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
12547
+ const cartMatch = isAddToCartText(elInfo.text);
12548
+ if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
12549
+ return `Blocked: "${elInfo.text}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
12550
+ }
12551
+ if (!cartMatch) {
12552
+ const dialogActions = await getCartDialogActions(wc);
12553
+ if (dialogActions) {
12554
+ return `Blocked: a cart confirmation dialog is open. Do not click background elements.
12555
+ ${dialogActions}
12556
+ Click one of these dialog actions instead.`;
12557
+ }
12558
+ }
10806
12559
  if (elInfo.href) {
10807
12560
  const validation = await validateLinkDestination(elInfo.href);
10808
12561
  if (validation.status === "dead") {
10809
12562
  return formatDeadLinkMessage(elInfo.text, validation);
10810
12563
  }
10811
12564
  }
12565
+ if (cartMatch) {
12566
+ recordCartClick(beforeUrl, elInfo.text);
12567
+ }
10812
12568
  const clickText = `Clicked: ${elInfo.text}`;
10813
12569
  const clickResult = await clickElement(wc, selector);
10814
12570
  if (clickResult.startsWith("Error:")) return clickResult;
@@ -10817,6 +12573,18 @@ async function clickResolvedSelector(wc, selector) {
10817
12573
  if (afterUrl !== beforeUrl) {
10818
12574
  return `${clickText} -> ${afterUrl}`;
10819
12575
  }
12576
+ const overlayHint = await detectPostClickOverlay(wc);
12577
+ if (overlayHint) {
12578
+ const dialogActions = cartMatch ? await getCartDialogActions(wc) : null;
12579
+ const actionsSuffix = dialogActions ? `
12580
+ ${dialogActions}
12581
+ Click one of these dialog actions. Do NOT click any other element.` : "";
12582
+ return `${clickText} (${clickResult})
12583
+ ${overlayHint}${actionsSuffix}`;
12584
+ }
12585
+ if (cartMatch) {
12586
+ return `${clickText} (${clickResult})`;
12587
+ }
10820
12588
  const activationResult = await activateElement(wc, selector);
10821
12589
  if (!activationResult.startsWith("Error:")) {
10822
12590
  await waitForPotentialNavigation(wc, beforeUrl);
@@ -10825,13 +12593,239 @@ async function clickResolvedSelector(wc, selector) {
10825
12593
  return `${clickText} -> ${fallbackUrl} (recovered via DOM activation)`;
10826
12594
  }
10827
12595
  }
12596
+ const postActivationOverlayHint = await detectPostClickOverlay(wc);
12597
+ if (postActivationOverlayHint) {
12598
+ return `${clickText} (${clickResult})
12599
+ ${postActivationOverlayHint}`;
12600
+ }
10828
12601
  return `${clickText} (${clickResult})`;
10829
12602
  }
12603
+ async function getCartDialogActions(wc) {
12604
+ const result = await wc.executeJavaScript(`
12605
+ (function() {
12606
+ function isVisible(el) {
12607
+ if (!(el instanceof HTMLElement)) return false;
12608
+ const style = getComputedStyle(el);
12609
+ if (style.display === "none" || style.visibility === "hidden") return false;
12610
+ const rect = el.getBoundingClientRect();
12611
+ return rect.width >= 20 && rect.height >= 10;
12612
+ }
12613
+
12614
+ function findDialogRoot() {
12615
+ const selectors = [
12616
+ '[data-test="basket-flyout"]',
12617
+ '[role="dialog"]',
12618
+ 'dialog[open]',
12619
+ '[role="alertdialog"]',
12620
+ '[aria-modal="true"]',
12621
+ ];
12622
+ for (const selector of selectors) {
12623
+ const nodes = document.querySelectorAll(selector);
12624
+ for (const node of nodes) {
12625
+ if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
12626
+ const text = (node.textContent || "").slice(0, 800).toLowerCase();
12627
+ const cartSignals = [
12628
+ "added to cart", "added to bag", "added to basket",
12629
+ "item added", "your basket", "your cart", "your bag",
12630
+ "view basket", "view cart", "continue shopping",
12631
+ ];
12632
+ if (cartSignals.some((signal) => text.includes(signal))) {
12633
+ return node;
12634
+ }
12635
+ }
12636
+ }
12637
+ return null;
12638
+ }
12639
+
12640
+ const dialog = findDialogRoot();
12641
+ if (!dialog) return { found: false, actions: [] };
12642
+
12643
+ const actions = [];
12644
+ dialog.querySelectorAll('button, a[href], [role="button"]').forEach((el) => {
12645
+ if (!(el instanceof HTMLElement) || !isVisible(el)) return;
12646
+ const label = (el.getAttribute("aria-label") || el.textContent || "").trim().slice(0, 80);
12647
+ if (!label || label.length < 2) return;
12648
+ const href = el.getAttribute("href") || "";
12649
+ const selector = el.id ? "#" + el.id
12650
+ : el.getAttribute("data-test") ? '[data-test="' + el.getAttribute("data-test") + '"]'
12651
+ : el.getAttribute("aria-label") ? '[aria-label="' + el.getAttribute("aria-label") + '"]'
12652
+ : null;
12653
+ if (selector) {
12654
+ actions.push({ label: label, href: href, selector: selector });
12655
+ }
12656
+ });
12657
+
12658
+ return {
12659
+ found: true,
12660
+ actions: actions.map((action) =>
12661
+ '- "' + action.label + '"' +
12662
+ (action.href ? ' -> ' + action.href : "") +
12663
+ (action.selector ? ' (selector: ' + action.selector + ')' : "")
12664
+ ),
12665
+ };
12666
+ })()
12667
+ `);
12668
+ if (!result || typeof result !== "object" || !("found" in result) || !result.found) {
12669
+ return null;
12670
+ }
12671
+ if (!("actions" in result) || !Array.isArray(result.actions) || result.actions.length === 0) {
12672
+ return null;
12673
+ }
12674
+ return `Available dialog actions:
12675
+ ${result.actions.join("\n")}`;
12676
+ }
12677
+ async function detectPostClickOverlay(wc) {
12678
+ const result = await wc.executeJavaScript(`
12679
+ (function() {
12680
+ var vw = window.innerWidth || document.documentElement.clientWidth;
12681
+ var vh = window.innerHeight || document.documentElement.clientHeight;
12682
+ var vpArea = Math.max(1, vw * vh);
12683
+
12684
+ function isVisible(el) {
12685
+ if (!(el instanceof HTMLElement)) return false;
12686
+ var style = getComputedStyle(el);
12687
+ if (style.display === "none" || style.visibility === "hidden") return false;
12688
+ return el.getBoundingClientRect().width > 0;
12689
+ }
12690
+
12691
+ function hasFixedAncestor(el) {
12692
+ var current = el.parentElement;
12693
+ while (current && current !== document.body) {
12694
+ var position = getComputedStyle(current).position;
12695
+ if (position === "fixed" || position === "sticky") return true;
12696
+ current = current.parentElement;
12697
+ }
12698
+ return false;
12699
+ }
12700
+
12701
+ function effectiveZ(el) {
12702
+ var current = el;
12703
+ while (current && current !== document.body) {
12704
+ var z = parseInt(getComputedStyle(current).zIndex, 10);
12705
+ if (z > 0) return z;
12706
+ current = current.parentElement;
12707
+ }
12708
+ return 0;
12709
+ }
12710
+
12711
+ function touchesViewportEdge(rect) {
12712
+ return rect.left <= 24 || rect.top <= 24 ||
12713
+ rect.right >= vw - 24 || rect.bottom >= vh - 24;
12714
+ }
12715
+
12716
+ var cartPhrases = [
12717
+ "added to cart", "added to bag", "added to basket",
12718
+ "added to your cart", "added to your bag", "added to your basket",
12719
+ ];
12720
+ var cartActions = [
12721
+ "view cart", "go to cart", "view basket", "go to basket",
12722
+ "continue shopping", "keep shopping", "checkout",
12723
+ ];
12724
+
12725
+ var selectors = 'dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"], [data-test="basket-flyout"]';
12726
+ var candidates = document.querySelectorAll(selectors);
12727
+ var hit = null;
12728
+ for (var i = 0; i < candidates.length; i++) {
12729
+ if (isVisible(candidates[i])) {
12730
+ hit = candidates[i];
12731
+ break;
12732
+ }
12733
+ }
12734
+
12735
+ if (!hit) {
12736
+ var elements = document.querySelectorAll("*");
12737
+ for (var j = 0; j < elements.length; j++) {
12738
+ var el = elements[j];
12739
+ if (!(el instanceof HTMLElement) || !isVisible(el)) continue;
12740
+ var style = getComputedStyle(el);
12741
+ var position = style.position;
12742
+ var isFixed = position === "fixed" || position === "sticky";
12743
+ var isAbsolute = position === "absolute";
12744
+ if (!isFixed && !isAbsolute) continue;
12745
+ if (isAbsolute && !hasFixedAncestor(el)) continue;
12746
+ if (effectiveZ(el) < 5) continue;
12747
+ var rect = el.getBoundingClientRect();
12748
+ var areaRatio = (rect.width * rect.height) / vpArea;
12749
+ if (rect.width >= 160 && rect.height >= 100 && areaRatio >= 0.05 && touchesViewportEdge(rect)) {
12750
+ hit = el;
12751
+ break;
12752
+ }
12753
+ }
12754
+ }
12755
+
12756
+ if (!hit) return { found: false, label: "", cartLike: false };
12757
+ var text = (hit.textContent || "").slice(0, 800).toLowerCase();
12758
+ var cartLike = cartPhrases.concat(cartActions).some(function(signal) {
12759
+ return text.indexOf(signal) !== -1;
12760
+ });
12761
+ var heading = hit.querySelector("h1,h2,h3,h4");
12762
+ var label = (hit.getAttribute("aria-label") || (heading && heading.textContent) || "").trim().slice(0, 80);
12763
+ return { found: true, label: label, cartLike: cartLike };
12764
+ })()
12765
+ `);
12766
+ if (!result || typeof result !== "object" || !("found" in result) || !result.found) {
12767
+ return null;
12768
+ }
12769
+ const label = typeof result.label === "string" && result.label ? ` ("${result.label}")` : "";
12770
+ if ("cartLike" in result && result.cartLike) {
12771
+ return `A cart confirmation dialog appeared${label}. Call read_page to see available actions — do not click Add to Cart again.`;
12772
+ }
12773
+ return `A dialog or overlay appeared${label}. Call read_page to see available actions.`;
12774
+ }
10830
12775
  async function dismissPopup(wc) {
10831
12776
  const before = await extractContent(wc);
10832
12777
  const initialBlocking = before.overlays.filter(
10833
12778
  (overlay) => overlay.blocksInteraction
10834
12779
  ).length;
12780
+ if (initialBlocking > 0) {
12781
+ const overlayText = before.overlays.map(
12782
+ (o) => [o.label, o.text].filter(Boolean).join(" ")
12783
+ ).join(" ").toLowerCase();
12784
+ const cartSignals = [
12785
+ "added to cart",
12786
+ "added to bag",
12787
+ "added to basket",
12788
+ "item added",
12789
+ "items in your basket",
12790
+ "items in your cart",
12791
+ "items in your bag",
12792
+ "your basket",
12793
+ "your cart",
12794
+ "your bag",
12795
+ "view basket",
12796
+ "view cart",
12797
+ "continue shopping"
12798
+ ];
12799
+ if (cartSignals.some((s) => overlayText.includes(s))) {
12800
+ const continueResult = await wc.executeJavaScript(`
12801
+ (function() {
12802
+ var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
12803
+ if (!dialog) return "Error: dialog not found";
12804
+ var buttons = dialog.querySelectorAll('button, a[href], [role="button"]');
12805
+ var continueBtn = null;
12806
+ var viewCartBtn = null;
12807
+ for (var i = 0; i < buttons.length; i++) {
12808
+ var label = (buttons[i].getAttribute('aria-label') || buttons[i].textContent || '').trim().toLowerCase();
12809
+ if (/continue shopping|keep shopping/.test(label)) { continueBtn = buttons[i]; break; }
12810
+ if (/view (basket|cart|bag)|checkout/.test(label) && !viewCartBtn) { viewCartBtn = buttons[i]; }
12811
+ }
12812
+ var target = continueBtn || viewCartBtn;
12813
+ if (!target) return "Error: no dialog action found";
12814
+ var actionLabel = (target.getAttribute('aria-label') || target.textContent || '').trim();
12815
+ if (target.tagName === 'A' && target.href) {
12816
+ window.location.href = target.href;
12817
+ return "Clicked: " + actionLabel + " -> " + target.href;
12818
+ }
12819
+ target.click();
12820
+ return "Clicked: " + actionLabel;
12821
+ })()
12822
+ `);
12823
+ if (typeof continueResult === "string" && !continueResult.startsWith("Error")) {
12824
+ return `Cart confirmation handled: ${continueResult}. Item was already added to your cart.`;
12825
+ }
12826
+ return "Cannot dismiss: this is a cart confirmation dialog. Item is in your cart. Use read_page to see dialog actions (e.g. View Basket, Continue Shopping) and click one of them instead.";
12827
+ }
12828
+ }
10835
12829
  const initialDormant = before.dormantOverlays.length;
10836
12830
  const candidates = await wc.executeJavaScript(`
10837
12831
  (function() {
@@ -11077,7 +13071,13 @@ async function getPostActionState(tabManager, name) {
11077
13071
  "reload",
11078
13072
  "press_key"
11079
13073
  ];
11080
- const interactActions = ["type", "type_text", "select_option", "hover", "focus"];
13074
+ const interactActions = [
13075
+ "type",
13076
+ "type_text",
13077
+ "select_option",
13078
+ "hover",
13079
+ "focus"
13080
+ ];
11081
13081
  const tabActions = ["create_tab", "switch_tab", "close_tab"];
11082
13082
  if (navActions.includes(name)) {
11083
13083
  let warning = "";
@@ -11260,7 +13260,8 @@ async function hoverElement(wc, selector) {
11260
13260
  if ("error" in pos && typeof pos.error === "string") return pos.error;
11261
13261
  const x = typeof pos.x === "number" ? pos.x : null;
11262
13262
  const y = typeof pos.y === "number" ? pos.y : null;
11263
- if (x == null || y == null) return "Error: Could not resolve hover coordinates";
13263
+ if (x == null || y == null)
13264
+ return "Error: Could not resolve hover coordinates";
11264
13265
  wc.sendInputEvent({ type: "mouseMove", x, y });
11265
13266
  const label = typeof pos.label === "string" ? pos.label : "element";
11266
13267
  return `Hovered: ${label}`;
@@ -11479,7 +13480,10 @@ async function waitForCondition(wc, text, selector, timeoutMs) {
11479
13480
  const expectedText = (text || "").trim();
11480
13481
  const expectedSelector = (selector || "").trim();
11481
13482
  if (!expectedText && !expectedSelector) {
11482
- return JSON.stringify({ matched: false, error: "wait_for requires text or selector" });
13483
+ return JSON.stringify({
13484
+ matched: false,
13485
+ error: "wait_for requires text or selector"
13486
+ });
11483
13487
  }
11484
13488
  if (wc.isLoading()) {
11485
13489
  await waitForLoad(wc, Math.min(effectiveTimeout, 5e3));
@@ -11503,13 +13507,26 @@ async function waitForCondition(wc, text, selector, timeoutMs) {
11503
13507
  `);
11504
13508
  const elapsedMs2 = Date.now() - startedAt;
11505
13509
  if (result === "selector") {
11506
- return JSON.stringify({ matched: true, type: "selector", value: expectedSelector, elapsed_ms: elapsedMs2 });
13510
+ return JSON.stringify({
13511
+ matched: true,
13512
+ type: "selector",
13513
+ value: expectedSelector,
13514
+ elapsed_ms: elapsedMs2
13515
+ });
11507
13516
  }
11508
13517
  if (result === "text") {
11509
- return JSON.stringify({ matched: true, type: "text", value: expectedText.slice(0, 80), elapsed_ms: elapsedMs2 });
13518
+ return JSON.stringify({
13519
+ matched: true,
13520
+ type: "text",
13521
+ value: expectedText.slice(0, 80),
13522
+ elapsed_ms: elapsedMs2
13523
+ });
11510
13524
  }
11511
13525
  if (typeof result === "string" && result.startsWith("invalid_selector:")) {
11512
- return JSON.stringify({ matched: false, error: `Invalid selector "${expectedSelector}" — ${result.slice(17)}` });
13526
+ return JSON.stringify({
13527
+ matched: false,
13528
+ error: `Invalid selector "${expectedSelector}" — ${result.slice(17)}`
13529
+ });
11513
13530
  }
11514
13531
  await new Promise((resolve) => setTimeout(resolve, 150));
11515
13532
  }
@@ -11653,7 +13670,12 @@ function registerTools(server, tabManager, runtime) {
11653
13670
  pageType,
11654
13671
  pageUrl,
11655
13672
  pageTitle,
11656
- recommended: scored.filter((t) => t.score <= 20).map(({ name, title, description, relevance }) => ({ name, title, description, relevance })),
13673
+ recommended: scored.filter((t) => t.score <= 20).map(({ name, title, description, relevance }) => ({
13674
+ name,
13675
+ title,
13676
+ description,
13677
+ relevance
13678
+ })),
11657
13679
  available: scored.filter((t) => t.score > 20).map(({ name, title, relevance }) => ({ name, title, relevance }))
11658
13680
  };
11659
13681
  return {
@@ -11685,8 +13707,12 @@ function registerTools(server, tabManager, runtime) {
11685
13707
  description: "Publish or stream agent reasoning/status text into Vessel's in-browser transcript monitor. Intended for external harnesses that want to mirror live thinking into the browser UI.",
11686
13708
  inputSchema: {
11687
13709
  text: zod.z.string().describe("Transcript text chunk to publish"),
11688
- stream_id: zod.z.string().optional().describe("Stable stream ID for incremental updates to the same entry"),
11689
- mode: zod.z.enum(["append", "replace", "final"]).optional().describe("append (default), replace current stream text, or mark the stream final"),
13710
+ stream_id: zod.z.string().optional().describe(
13711
+ "Stable stream ID for incremental updates to the same entry"
13712
+ ),
13713
+ mode: zod.z.enum(["append", "replace", "final"]).optional().describe(
13714
+ "append (default), replace current stream text, or mark the stream final"
13715
+ ),
11690
13716
  kind: zod.z.enum(["thinking", "message", "status"]).optional().describe("Visual style for the transcript entry"),
11691
13717
  title: zod.z.string().optional().describe("Optional short label such as Plan, Search, or Summary")
11692
13718
  }
@@ -11737,7 +13763,9 @@ function registerTools(server, tabManager, runtime) {
11737
13763
  ];
11738
13764
  async function buildExtractResponse(pageContent, mode, adBlockingEnabled, wc) {
11739
13765
  const adBlockLine = `**Ad Blocking:** ${adBlockingEnabled ? "On" : "Off"}`;
11740
- const savedHighlights = getHighlightsForUrl(pageContent.url);
13766
+ const savedHighlights = getHighlightsForUrl(
13767
+ pageContent.url
13768
+ );
11741
13769
  const liveSelectionSection = wc ? formatLiveSelectionSection(
11742
13770
  await captureLiveHighlightSnapshot(wc, savedHighlights)
11743
13771
  ) : null;
@@ -11854,10 +13882,18 @@ ${buildScopedContext(pageContent, mode)}`;
11854
13882
  async ({ url }) => {
11855
13883
  const tab = tabManager.getActiveTab();
11856
13884
  if (!tab) return asTextResponse("Error: No active tab");
13885
+ const preCheck = await validateLinkDestination(url);
13886
+ if (preCheck.status === "dead") {
13887
+ return asTextResponse(
13888
+ `Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
13889
+ );
13890
+ }
11857
13891
  return withAction(runtime, tabManager, "navigate", { url }, async () => {
11858
13892
  const id = tabManager.getActiveTabId();
11859
13893
  tabManager.navigateTab(id, url);
11860
- const { httpStatus } = await waitForLoadWithStatus(tab.view.webContents);
13894
+ const { httpStatus } = await waitForLoadWithStatus(
13895
+ tab.view.webContents
13896
+ );
11861
13897
  const finalUrl = tab.view.webContents.getURL();
11862
13898
  const statusNote = httpStatus !== null && httpStatus >= 400 ? ` [HTTP ${httpStatus} — page may be missing or unavailable, consider navigating back and trying a different link]` : "";
11863
13899
  return `Navigated to ${finalUrl}${statusNote}`;
@@ -11927,7 +13963,9 @@ ${buildScopedContext(pageContent, mode)}`;
11927
13963
  const pageContent = await extractContent(tab.view.webContents);
11928
13964
  const requestedType = typeof type === "string" && type.trim() ? type.trim().toLowerCase() : "";
11929
13965
  const entities = (pageContent.structuredData ?? []).filter(
11930
- (entity) => requestedType ? entity.types.some((entry) => entry.toLowerCase() === requestedType) : true
13966
+ (entity) => requestedType ? entity.types.some(
13967
+ (entry) => entry.toLowerCase() === requestedType
13968
+ ) : true
11931
13969
  );
11932
13970
  const sourceCounts = {
11933
13971
  json_ld: pageContent.jsonLd?.length ?? 0,
@@ -12166,7 +14204,9 @@ ${buildScopedContext(pageContent, mode)}`;
12166
14204
  })()
12167
14205
  `);
12168
14206
  if (!result || typeof result !== "object") {
12169
- return asTextResponse("Error: Element text extraction returned no result");
14207
+ return asTextResponse(
14208
+ "Error: Element text extraction returned no result"
14209
+ );
12170
14210
  }
12171
14211
  if ("error" in result && typeof result.error === "string") {
12172
14212
  return asTextResponse(`Error: ${result.error}`);
@@ -12216,11 +14256,7 @@ ${buildScopedContext(pageContent, mode)}`;
12216
14256
  return "Error: No index or selector provided";
12217
14257
  }
12218
14258
  if (mode === "keystroke") {
12219
- return typeKeystroke(
12220
- tab.view.webContents,
12221
- resolvedSelector,
12222
- text
12223
- );
14259
+ return typeKeystroke(tab.view.webContents, resolvedSelector, text);
12224
14260
  }
12225
14261
  return setElementValue(tab.view.webContents, resolvedSelector, text);
12226
14262
  }
@@ -12259,11 +14295,7 @@ ${buildScopedContext(pageContent, mode)}`;
12259
14295
  return "Error: No index or selector provided";
12260
14296
  }
12261
14297
  if (mode === "keystroke") {
12262
- return typeKeystroke(
12263
- tab.view.webContents,
12264
- resolvedSelector,
12265
- text
12266
- );
14298
+ return typeKeystroke(tab.view.webContents, resolvedSelector, text);
12267
14299
  }
12268
14300
  return setElementValue(tab.view.webContents, resolvedSelector, text);
12269
14301
  }
@@ -12368,7 +14400,9 @@ ${buildScopedContext(pageContent, mode)}`;
12368
14400
  description: "Scroll the page up or down.",
12369
14401
  inputSchema: {
12370
14402
  direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
12371
- amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
14403
+ amount: optionalNumberLikeSchema().describe(
14404
+ "Pixels to scroll (default 500)"
14405
+ )
12372
14406
  }
12373
14407
  },
12374
14408
  async ({ direction, amount }) => {
@@ -12380,7 +14414,7 @@ ${buildScopedContext(pageContent, mode)}`;
12380
14414
  "scroll",
12381
14415
  { direction, amount },
12382
14416
  async () => {
12383
- const pixels = amount || 500;
14417
+ const pixels = coerceOptionalNumber(amount) ?? 500;
12384
14418
  const dir = direction === "up" ? -pixels : pixels;
12385
14419
  const result = await scrollPage(tab.view.webContents, dir);
12386
14420
  return `Scrolled ${direction} by ${pixels}px (moved ${Math.abs(result.movedY)}px, now at y=${Math.round(result.afterY)})`;
@@ -12406,6 +14440,32 @@ ${buildScopedContext(pageContent, mode)}`;
12406
14440
  );
12407
14441
  }
12408
14442
  );
14443
+ server.registerTool(
14444
+ "vessel_clear_overlays",
14445
+ {
14446
+ title: "Clear Overlays",
14447
+ description: "Work through blocking overlays and modals until the page is unblocked, using overlay-specific heuristics for consent banners and radio-selection dialogs.",
14448
+ inputSchema: {
14449
+ strategy: zod.z.enum(["auto", "interactive"]).optional().describe(
14450
+ 'How aggressively to clear overlays. "auto" uses heuristics; "interactive" stops earlier when human judgment may be needed.'
14451
+ )
14452
+ }
14453
+ },
14454
+ async ({ strategy }) => {
14455
+ const tab = tabManager.getActiveTab();
14456
+ if (!tab) return asTextResponse("Error: No active tab");
14457
+ return withAction(
14458
+ runtime,
14459
+ tabManager,
14460
+ "clear_overlays",
14461
+ { strategy: strategy || "auto" },
14462
+ async () => clearOverlays(
14463
+ tab.view.webContents,
14464
+ strategy === "interactive" ? "interactive" : "auto"
14465
+ )
14466
+ );
14467
+ }
14468
+ );
12409
14469
  server.registerTool(
12410
14470
  "vessel_wait_for",
12411
14471
  {
@@ -12667,8 +14727,14 @@ ${buildScopedContext(pageContent, mode)}`;
12667
14727
  `Error capturing screenshot: ${screenshot.error}`
12668
14728
  );
12669
14729
  }
12670
- const screenshotPath = path$1.join(os.tmpdir(), `vessel_screenshot_${Date.now()}.png`);
12671
- fs$1.writeFileSync(screenshotPath, Buffer.from(screenshot.base64, "base64"));
14730
+ const screenshotPath = path$1.join(
14731
+ os.tmpdir(),
14732
+ `vessel_screenshot_${Date.now()}.png`
14733
+ );
14734
+ fs$1.writeFileSync(
14735
+ screenshotPath,
14736
+ Buffer.from(screenshot.base64, "base64")
14737
+ );
12672
14738
  return {
12673
14739
  content: [
12674
14740
  {
@@ -12699,7 +14765,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
12699
14765
  inputSchema: {
12700
14766
  index: zod.z.number().optional().describe("Element index from extracted content to highlight"),
12701
14767
  selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
12702
- text: zod.z.string().optional().describe(
14768
+ text: normalizedOptionalStringSchema().describe(
12703
14769
  "Text to find and highlight on the page (highlights all occurrences)"
12704
14770
  ),
12705
14771
  label: zod.z.string().optional().describe("Optional annotation label to display near the highlight"),
@@ -12717,18 +14783,27 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
12717
14783
  async ({ index, selector, text, label, durationMs, persist, color }) => {
12718
14784
  const tab = tabManager.getActiveTab();
12719
14785
  if (!tab) return asTextResponse("Error: No active tab");
14786
+ const normalizedText = normalizeLooseString(text);
12720
14787
  return withAction(
12721
14788
  runtime,
12722
14789
  tabManager,
12723
14790
  "highlight",
12724
- { index, selector, text, label, durationMs, persist, color },
14791
+ {
14792
+ index,
14793
+ selector,
14794
+ text: normalizedText,
14795
+ label,
14796
+ durationMs,
14797
+ persist,
14798
+ color
14799
+ },
12725
14800
  async () => {
12726
14801
  const wc = tab.view.webContents;
12727
14802
  const resolvedSelector = await resolveSelector(wc, index, selector);
12728
14803
  const result = await highlightOnPage(
12729
14804
  wc,
12730
14805
  resolvedSelector,
12731
- text,
14806
+ normalizedText,
12732
14807
  label,
12733
14808
  durationMs,
12734
14809
  color
@@ -12738,7 +14813,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
12738
14813
  addHighlight(
12739
14814
  url,
12740
14815
  resolvedSelector ?? void 0,
12741
- text,
14816
+ normalizedText,
12742
14817
  label,
12743
14818
  color,
12744
14819
  "agent"
@@ -12758,12 +14833,18 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
12758
14833
  async () => {
12759
14834
  const tab = tabManager.getActiveTab();
12760
14835
  if (!tab) return asTextResponse("Error: No active tab");
12761
- return withAction(runtime, tabManager, "clear_highlights", {}, async () => {
12762
- const wc = tab.view.webContents;
12763
- const url = normalizeUrl(wc.getURL());
12764
- clearHighlightsForUrl(url);
12765
- return clearHighlights(wc);
12766
- });
14836
+ return withAction(
14837
+ runtime,
14838
+ tabManager,
14839
+ "clear_highlights",
14840
+ {},
14841
+ async () => {
14842
+ const wc = tab.view.webContents;
14843
+ const url = normalizeUrl(wc.getURL());
14844
+ clearHighlightsForUrl(url);
14845
+ return clearHighlights(wc);
14846
+ }
14847
+ );
12767
14848
  }
12768
14849
  );
12769
14850
  server.registerTool(
@@ -12934,12 +15015,22 @@ ${JSON.stringify(otherHighlights, null, 2)}`
12934
15015
  title: "Save Bookmark",
12935
15016
  description: "Save the current page, a specific URL, or a link target from the current page into a bookmark folder. You can provide folder_id or folder_name; missing folder names can be created automatically.",
12936
15017
  inputSchema: {
12937
- url: zod.z.string().optional().describe("URL to bookmark. Omit to use the current page or provide index/selector to bookmark a link target from the page"),
12938
- title: zod.z.string().optional().describe("Human-readable title for the bookmark. Omit to use the page or link text"),
12939
- index: zod.z.number().optional().describe("Element index of a link on the current page to bookmark without opening it"),
12940
- selector: zod.z.string().optional().describe("CSS selector of a link on the current page to bookmark without opening it"),
15018
+ url: zod.z.string().optional().describe(
15019
+ "URL to bookmark. Omit to use the current page or provide index/selector to bookmark a link target from the page"
15020
+ ),
15021
+ title: zod.z.string().optional().describe(
15022
+ "Human-readable title for the bookmark. Omit to use the page or link text"
15023
+ ),
15024
+ index: zod.z.number().optional().describe(
15025
+ "Element index of a link on the current page to bookmark without opening it"
15026
+ ),
15027
+ selector: zod.z.string().optional().describe(
15028
+ "CSS selector of a link on the current page to bookmark without opening it"
15029
+ ),
12941
15030
  folder_id: zod.z.string().optional().describe("Folder ID to save into (omit for Unsorted)"),
12942
- folder_name: zod.z.string().optional().describe("Folder name to save into. Created automatically if missing"),
15031
+ folder_name: zod.z.string().optional().describe(
15032
+ "Folder name to save into. Created automatically if missing"
15033
+ ),
12943
15034
  folder_summary: zod.z.string().optional().describe("Optional one-sentence summary if a new folder is created"),
12944
15035
  create_folder_if_missing: zod.z.boolean().optional().describe("Create folder_name automatically when it does not exist"),
12945
15036
  note: zod.z.string().optional().describe("Optional note about why this was bookmarked"),
@@ -13085,10 +15176,16 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13085
15176
  description: "Organize a bookmark by intent: save or move a bookmark into a folder, creating the folder if needed. Works with bookmark_id, url, a link target from the current page, or the current page itself.",
13086
15177
  inputSchema: {
13087
15178
  bookmark_id: zod.z.string().optional().describe("Existing bookmark ID to move or update"),
13088
- url: zod.z.string().optional().describe("URL to organize. Omit to use the current page or provide index/selector to target a link"),
15179
+ url: zod.z.string().optional().describe(
15180
+ "URL to organize. Omit to use the current page or provide index/selector to target a link"
15181
+ ),
13089
15182
  title: zod.z.string().optional().describe("Optional title when saving a new bookmark"),
13090
- index: zod.z.number().optional().describe("Element index of a link on the current page to organize without opening it"),
13091
- selector: zod.z.string().optional().describe("CSS selector of a link on the current page to organize without opening it"),
15183
+ index: zod.z.number().optional().describe(
15184
+ "Element index of a link on the current page to organize without opening it"
15185
+ ),
15186
+ selector: zod.z.string().optional().describe(
15187
+ "CSS selector of a link on the current page to organize without opening it"
15188
+ ),
13092
15189
  folder_id: zod.z.string().optional().describe("Folder ID to organize into"),
13093
15190
  folder_name: zod.z.string().optional().describe("Folder name to organize into"),
13094
15191
  folder_summary: zod.z.string().optional().describe("Optional summary used if a new folder is created"),
@@ -13215,10 +15312,16 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13215
15312
  description: 'Archive the current page, a URL, a link target from the current page, or an existing bookmark into the default "Archive" folder.',
13216
15313
  inputSchema: {
13217
15314
  bookmark_id: zod.z.string().optional().describe("Existing bookmark ID to archive"),
13218
- url: zod.z.string().optional().describe("URL to archive. Omit to use the current page or provide index/selector to target a link"),
15315
+ url: zod.z.string().optional().describe(
15316
+ "URL to archive. Omit to use the current page or provide index/selector to target a link"
15317
+ ),
13219
15318
  title: zod.z.string().optional().describe("Optional title when saving a new archived bookmark"),
13220
- index: zod.z.number().optional().describe("Element index of a link on the current page to archive without opening it"),
13221
- selector: zod.z.string().optional().describe("CSS selector of a link on the current page to archive without opening it"),
15319
+ index: zod.z.number().optional().describe(
15320
+ "Element index of a link on the current page to archive without opening it"
15321
+ ),
15322
+ selector: zod.z.string().optional().describe(
15323
+ "CSS selector of a link on the current page to archive without opening it"
15324
+ ),
13222
15325
  note: zod.z.string().optional().describe("Optional note to store with the archived bookmark")
13223
15326
  }
13224
15327
  },
@@ -13374,7 +15477,11 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13374
15477
  `Folder "${existing.name}" already exists (id=${existing.id})`
13375
15478
  );
13376
15479
  }
13377
- const folder = renameFolder(folder_id, new_name, summary);
15480
+ const folder = renameFolder(
15481
+ folder_id,
15482
+ new_name,
15483
+ summary
15484
+ );
13378
15485
  return folder ? composeFolderAwareResponse(`Renamed folder to "${folder.name}"`) : `Folder ${folder_id} not found`;
13379
15486
  }
13380
15487
  );
@@ -13575,13 +15682,22 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13575
15682
  title: "Start Workflow",
13576
15683
  description: "Begin tracking a multi-step web workflow. Vessel will show progress after every action so you always know where you are in the flow.",
13577
15684
  inputSchema: {
13578
- goal: zod.z.string().describe("What this workflow accomplishes (e.g. 'Purchase item from Amazon')"),
13579
- steps: zod.z.array(zod.z.string()).describe("Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])")
15685
+ goal: zod.z.string().describe(
15686
+ "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
15687
+ ),
15688
+ steps: stringArrayLikeSchema().describe(
15689
+ "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
15690
+ )
13580
15691
  }
13581
15692
  },
13582
15693
  async ({ goal, steps }) => {
15694
+ const normalizedSteps = coerceStringArray(steps) ?? [];
13583
15695
  const tab = tabManager.getActiveTab();
13584
- const flow = runtime.startFlow(goal, steps, tab?.view.webContents.getURL());
15696
+ const flow = runtime.startFlow(
15697
+ goal,
15698
+ normalizedSteps,
15699
+ tab?.view.webContents.getURL()
15700
+ );
13585
15701
  return asTextResponse(
13586
15702
  `Flow started: ${flow.goal}
13587
15703
  ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
@@ -13635,13 +15751,18 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13635
15751
  },
13636
15752
  async () => {
13637
15753
  const tab = tabManager.getActiveTab();
13638
- if (!tab) return asTextResponse("No active tab. Use vessel_navigate to open a page.");
15754
+ if (!tab)
15755
+ return asTextResponse(
15756
+ "No active tab. Use vessel_navigate to open a page."
15757
+ );
13639
15758
  const wc = tab.view.webContents;
13640
15759
  let page;
13641
15760
  try {
13642
15761
  page = await extractContent(wc);
13643
15762
  } catch {
13644
- return asTextResponse("Could not read page. Try vessel_navigate to a working URL.");
15763
+ return asTextResponse(
15764
+ "Could not read page. Try vessel_navigate to a working URL."
15765
+ );
13645
15766
  }
13646
15767
  const suggestions = [];
13647
15768
  suggestions.push(`Page: ${page.title || "(untitled)"}`);
@@ -13661,23 +15782,32 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13661
15782
  );
13662
15783
  const formCount = page.forms.length;
13663
15784
  const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
13664
- const linkCount = page.interactiveElements.filter((el) => el.type === "link").length;
15785
+ const linkCount = page.interactiveElements.filter(
15786
+ (el) => el.type === "link"
15787
+ ).length;
13665
15788
  const hasPagination = page.interactiveElements.some(
13666
15789
  (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
13667
15790
  );
13668
15791
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
13669
15792
  if (hasOverlays) {
13670
15793
  suggestions.push("⚠ BLOCKING OVERLAY detected — dismiss it first:");
13671
- suggestions.push(" → vessel_dismiss_popup or vessel_click on close/accept button");
15794
+ suggestions.push(" → vessel_clear_overlays for stacked modals");
15795
+ suggestions.push(" → or vessel_dismiss_popup for a single popup");
13672
15796
  suggestions.push("");
13673
15797
  }
13674
15798
  if (hasPasswordField) {
13675
15799
  suggestions.push("🔑 LOGIN PAGE detected:");
13676
- suggestions.push(" → vessel_login(username, password) — handles the full flow");
13677
- suggestions.push(" → Or vessel_fill_form + vessel_submit_form for manual control");
15800
+ suggestions.push(
15801
+ " → vessel_login(username, password) handles the full flow"
15802
+ );
15803
+ suggestions.push(
15804
+ " → Or vessel_fill_form + vessel_submit_form for manual control"
15805
+ );
13678
15806
  } else if (hasSearchInput && linkCount < 10) {
13679
15807
  suggestions.push("🔍 SEARCH PAGE detected:");
13680
- suggestions.push(" → vessel_search(query) — finds the box, types, submits");
15808
+ suggestions.push(
15809
+ " → vessel_search(query) — finds the box, types, submits"
15810
+ );
13681
15811
  } else if (hasSearchInput && linkCount >= 10) {
13682
15812
  suggestions.push("📋 SEARCH RESULTS detected:");
13683
15813
  suggestions.push(" → vessel_click on a result link");
@@ -13686,7 +15816,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13686
15816
  }
13687
15817
  } else if (formCount > 0) {
13688
15818
  suggestions.push(`📝 FORM detected (${totalFields} fields):`);
13689
- suggestions.push(" → vessel_fill_form(fields) — fill all fields at once");
15819
+ suggestions.push(
15820
+ " → vessel_fill_form(fields) — fill all fields at once"
15821
+ );
13690
15822
  suggestions.push(" → Or vessel_type for individual fields");
13691
15823
  } else if (hasPagination) {
13692
15824
  suggestions.push("📄 PAGINATED CONTENT:");
@@ -13698,12 +15830,16 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13698
15830
  suggestions.push(" → vessel_scroll to see more");
13699
15831
  } else {
13700
15832
  suggestions.push("🌐 GENERAL PAGE:");
13701
- suggestions.push(" → vessel_extract_content to understand the page structure");
15833
+ suggestions.push(
15834
+ " → vessel_extract_content to understand the page structure"
15835
+ );
13702
15836
  suggestions.push(" → vessel_click on any element by index");
13703
15837
  suggestions.push(" → vessel_navigate to go somewhere new");
13704
15838
  }
13705
15839
  suggestions.push("");
13706
- suggestions.push(`Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`);
15840
+ suggestions.push(
15841
+ `Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`
15842
+ );
13707
15843
  return asTextResponse(suggestions.join("\n"));
13708
15844
  }
13709
15845
  );
@@ -13717,9 +15853,14 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13717
15853
  zod.z.object({
13718
15854
  index: zod.z.number().optional().describe("Element index from page content"),
13719
15855
  selector: zod.z.string().optional().describe("CSS selector fallback"),
15856
+ name: zod.z.string().optional().describe("Field name or id, such as custname"),
15857
+ label: zod.z.string().optional().describe("Visible label or aria-label text"),
15858
+ placeholder: zod.z.string().optional().describe("Placeholder text shown in the field"),
13720
15859
  value: zod.z.string().describe("Value to enter")
13721
15860
  })
13722
- ).describe("Fields to fill"),
15861
+ ).describe(
15862
+ "Fields to fill, matched by index, selector, name, label, or placeholder"
15863
+ ),
13723
15864
  submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
13724
15865
  }
13725
15866
  },
@@ -13733,18 +15874,10 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13733
15874
  { fieldCount: fields.length, submit },
13734
15875
  async () => {
13735
15876
  const wc = tab.view.webContents;
13736
- const results = [];
13737
- for (const field of fields) {
13738
- const sel = await resolveSelector(wc, field.index, field.selector);
13739
- if (!sel) {
13740
- results.push(`Skipped: no selector for index=${field.index}`);
13741
- continue;
13742
- }
13743
- const result = await setElementValue(wc, sel, field.value);
13744
- results.push(result);
13745
- }
15877
+ const fillResults = await fillFormFields(wc, fields);
15878
+ const results = fillResults.map((item) => item.result);
13746
15879
  if (submit) {
13747
- const firstSel = await resolveSelector(wc, fields[0]?.index, fields[0]?.selector);
15880
+ const firstSel = fillResults.find((item) => item.selector)?.selector ?? null;
13748
15881
  if (firstSel) {
13749
15882
  const beforeUrl = wc.getURL();
13750
15883
  const submitResult = await submitForm(wc, void 0, firstSel);
@@ -13770,12 +15903,25 @@ ${results.join("\n")}`;
13770
15903
  url: zod.z.string().optional().describe("Login page URL (skip if already on login page)"),
13771
15904
  username: zod.z.string().describe("Username or email"),
13772
15905
  password: zod.z.string().describe("Password"),
13773
- username_selector: zod.z.string().optional().describe("CSS selector for username field (auto-detected if omitted)"),
13774
- password_selector: zod.z.string().optional().describe("CSS selector for password field (auto-detected if omitted)"),
13775
- submit_selector: zod.z.string().optional().describe("CSS selector for submit button (auto-detected if omitted)")
15906
+ username_selector: zod.z.string().optional().describe(
15907
+ "CSS selector for username field (auto-detected if omitted)"
15908
+ ),
15909
+ password_selector: zod.z.string().optional().describe(
15910
+ "CSS selector for password field (auto-detected if omitted)"
15911
+ ),
15912
+ submit_selector: zod.z.string().optional().describe(
15913
+ "CSS selector for submit button (auto-detected if omitted)"
15914
+ )
13776
15915
  }
13777
15916
  },
13778
- async ({ url, username, password, username_selector, password_selector, submit_selector }) => {
15917
+ async ({
15918
+ url,
15919
+ username,
15920
+ password,
15921
+ username_selector,
15922
+ password_selector,
15923
+ submit_selector
15924
+ }) => {
13779
15925
  const tab = tabManager.getActiveTab();
13780
15926
  if (!tab) return asTextResponse("Error: No active tab");
13781
15927
  return withAction(
@@ -13798,14 +15944,16 @@ ${results.join("\n")}`;
13798
15944
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
13799
15945
  })()
13800
15946
  `);
13801
- if (!userSel) return "Error: Could not find username/email field. Try providing username_selector.";
15947
+ if (!userSel)
15948
+ return "Error: Could not find username/email field. Try providing username_selector.";
13802
15949
  const passSel = password_selector || await wc.executeJavaScript(`
13803
15950
  (function() {
13804
15951
  var el = document.querySelector('input[type="password"]');
13805
15952
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
13806
15953
  })()
13807
15954
  `);
13808
- if (!passSel) return "Error: Could not find password field. Try providing password_selector.";
15955
+ if (!passSel)
15956
+ return "Error: Could not find password field. Try providing password_selector.";
13809
15957
  const userResult = await setElementValue(wc, userSel, username);
13810
15958
  steps.push(userResult);
13811
15959
  const passResult = await setElementValue(wc, passSel, password);
@@ -13823,7 +15971,8 @@ ${results.join("\n")}`;
13823
15971
  return false;
13824
15972
  })()
13825
15973
  `);
13826
- if (!clicked) return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
15974
+ if (!clicked)
15975
+ return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
13827
15976
  }
13828
15977
  await waitForPotentialNavigation(wc, beforeUrl);
13829
15978
  const afterUrl = wc.getURL();
@@ -13849,14 +15998,41 @@ ${steps.join("\n")}`;
13849
15998
  async ({ query, selector }) => {
13850
15999
  const tab = tabManager.getActiveTab();
13851
16000
  if (!tab) return asTextResponse("Error: No active tab");
13852
- return withAction(
13853
- runtime,
13854
- tabManager,
13855
- "search",
13856
- { query },
13857
- async () => {
13858
- const wc = tab.view.webContents;
13859
- const searchSel = selector || await wc.executeJavaScript(`
16001
+ const qLower = query.toLowerCase().trim();
16002
+ const buttonLabels = [
16003
+ "add to cart",
16004
+ "add to bag",
16005
+ "add to basket",
16006
+ "buy now",
16007
+ "buy it now",
16008
+ "purchase",
16009
+ "continue shopping",
16010
+ "keep shopping",
16011
+ "view cart",
16012
+ "view bag",
16013
+ "view basket",
16014
+ "go to cart",
16015
+ "go to checkout",
16016
+ "checkout",
16017
+ "check out",
16018
+ "proceed to checkout",
16019
+ "place order",
16020
+ "submit",
16021
+ "subscribe",
16022
+ "sign up",
16023
+ "sign in",
16024
+ "log in",
16025
+ "register",
16026
+ "continue"
16027
+ ];
16028
+ if (buttonLabels.some((p) => qLower.includes(p))) {
16029
+ return asTextResponse(
16030
+ `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`
16031
+ );
16032
+ }
16033
+ return withAction(runtime, tabManager, "search", { query }, async () => {
16034
+ const wc = tab.view.webContents;
16035
+ const searchSel = selector || await wc.executeJavaScript(`
13860
16036
  (function() {
13861
16037
  var el = document.querySelector('input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]');
13862
16038
  if (!el) {
@@ -13872,24 +16048,24 @@ ${steps.join("\n")}`;
13872
16048
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
13873
16049
  })()
13874
16050
  `);
13875
- if (!searchSel) return "Error: Could not find search input. Try providing a selector.";
13876
- await setElementValue(wc, searchSel, query);
13877
- await wc.executeJavaScript(`
16051
+ if (!searchSel)
16052
+ return "Error: Could not find search input. Try providing a selector.";
16053
+ await setElementValue(wc, searchSel, query);
16054
+ await wc.executeJavaScript(`
13878
16055
  (function() {
13879
16056
  var el = document.querySelector(${JSON.stringify(searchSel)});
13880
16057
  if (el) el.focus();
13881
16058
  })()
13882
16059
  `);
13883
- await new Promise((r) => setTimeout(r, 50));
13884
- const beforeUrl = wc.getURL();
13885
- wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
13886
- await new Promise((r) => setTimeout(r, 16));
13887
- wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
13888
- await waitForPotentialNavigation(wc, beforeUrl);
13889
- const afterUrl = wc.getURL();
13890
- return afterUrl !== beforeUrl ? `Searched "${query}" → ${afterUrl}` : `Searched "${query}" (same page — results may have loaded dynamically)`;
13891
- }
13892
- );
16060
+ await new Promise((r) => setTimeout(r, 50));
16061
+ const beforeUrl = wc.getURL();
16062
+ wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
16063
+ await new Promise((r) => setTimeout(r, 16));
16064
+ wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
16065
+ await waitForPotentialNavigation(wc, beforeUrl);
16066
+ const afterUrl = wc.getURL();
16067
+ return afterUrl !== beforeUrl ? `Searched "${query}" → ${afterUrl}` : `Searched "${query}" (same page — results may have loaded dynamically)`;
16068
+ });
13893
16069
  }
13894
16070
  );
13895
16071
  server.registerTool(
@@ -13899,7 +16075,9 @@ ${steps.join("\n")}`;
13899
16075
  description: "Navigate to the next or previous page of results. Auto-detects pagination controls.",
13900
16076
  inputSchema: {
13901
16077
  direction: zod.z.enum(["next", "prev"]).describe("Pagination direction"),
13902
- selector: zod.z.string().optional().describe("CSS selector for the pagination link (auto-detected if omitted)")
16078
+ selector: zod.z.string().optional().describe(
16079
+ "CSS selector for the pagination link (auto-detected if omitted)"
16080
+ )
13903
16081
  }
13904
16082
  },
13905
16083
  async ({ direction, selector }) => {
@@ -13937,7 +16115,8 @@ ${steps.join("\n")}`;
13937
16115
  return false;
13938
16116
  })()
13939
16117
  `);
13940
- if (!clicked) return `Error: Could not find ${direction} pagination control. Try providing a selector.`;
16118
+ if (!clicked)
16119
+ return `Error: Could not find ${direction} pagination control. Try providing a selector.`;
13941
16120
  await waitForPotentialNavigation(wc, beforeUrl);
13942
16121
  const afterUrl = wc.getURL();
13943
16122
  return afterUrl !== beforeUrl ? `Paginated ${direction} → ${afterUrl}` : `Clicked ${direction} (page may have updated dynamically)`;
@@ -13953,12 +16132,15 @@ ${steps.join("\n")}`;
13953
16132
  inputSchema: zod.z.object({})
13954
16133
  },
13955
16134
  async () => {
16135
+ const tab = tabManager.getActiveTab();
16136
+ if (!tab) return asTextResponse("Error: No active tab");
13956
16137
  return withAction(
13957
- tabManager,
13958
16138
  runtime,
16139
+ tabManager,
13959
16140
  "vessel_accept_cookies",
13960
16141
  {},
13961
- async (wc) => {
16142
+ async () => {
16143
+ const wc = tab.view.webContents;
13962
16144
  const dismissed = await wc.executeJavaScript(`
13963
16145
  (function() {
13964
16146
  var selectors = [
@@ -14008,12 +16190,15 @@ ${steps.join("\n")}`;
14008
16190
  })
14009
16191
  },
14010
16192
  async ({ index, selector: rawSelector }) => {
16193
+ const tab = tabManager.getActiveTab();
16194
+ if (!tab) return asTextResponse("Error: No active tab");
14011
16195
  return withAction(
14012
- tabManager,
14013
16196
  runtime,
16197
+ tabManager,
14014
16198
  "vessel_extract_table",
14015
16199
  { index, selector: rawSelector },
14016
- async (wc) => {
16200
+ async () => {
16201
+ const wc = tab.view.webContents;
14017
16202
  const sel = rawSelector || (index != null ? await resolveSelector(wc, index) : null);
14018
16203
  const tableJson = await wc.executeJavaScript(`
14019
16204
  (function() {
@@ -14060,12 +16245,15 @@ ${JSON.stringify(tableJson, null, 2)}`;
14060
16245
  })
14061
16246
  },
14062
16247
  async ({ index, selector: rawSelector, position }) => {
16248
+ const tab = tabManager.getActiveTab();
16249
+ if (!tab) return asTextResponse("Error: No active tab");
14063
16250
  return withAction(
14064
- tabManager,
14065
16251
  runtime,
16252
+ tabManager,
14066
16253
  "vessel_scroll_to_element",
14067
16254
  { index, selector: rawSelector, position },
14068
- async (wc) => {
16255
+ async () => {
16256
+ const wc = tab.view.webContents;
14069
16257
  const sel = rawSelector || (index != null ? await resolveSelector(wc, index) : null);
14070
16258
  if (!sel) return "Error: Provide an index or selector.";
14071
16259
  const block = position === "top" ? "start" : position === "bottom" ? "end" : "center";
@@ -14115,12 +16303,15 @@ ${JSON.stringify(tableJson, null, 2)}`;
14115
16303
  })
14116
16304
  },
14117
16305
  async ({ timeoutMs }) => {
16306
+ const tab = tabManager.getActiveTab();
16307
+ if (!tab) return asTextResponse("Error: No active tab");
14118
16308
  return withAction(
14119
- tabManager,
14120
16309
  runtime,
16310
+ tabManager,
14121
16311
  "vessel_wait_for_navigation",
14122
16312
  { timeoutMs },
14123
- async (wc) => {
16313
+ async () => {
16314
+ const wc = tab.view.webContents;
14124
16315
  const timeout = timeoutMs || 1e4;
14125
16316
  const beforeUrl = wc.getURL();
14126
16317
  if (wc.isLoading()) {
@@ -14134,9 +16325,12 @@ ${JSON.stringify(tableJson, null, 2)}`;
14134
16325
  } else {
14135
16326
  await new Promise((resolve) => {
14136
16327
  let navigated = false;
14137
- const timer = setTimeout(() => {
14138
- if (!navigated) resolve();
14139
- }, Math.min(timeout, 2e3));
16328
+ const timer = setTimeout(
16329
+ () => {
16330
+ if (!navigated) resolve();
16331
+ },
16332
+ Math.min(timeout, 2e3)
16333
+ );
14140
16334
  wc.once("did-start-loading", () => {
14141
16335
  navigated = true;
14142
16336
  clearTimeout(timer);
@@ -14174,7 +16368,9 @@ ${JSON.stringify(tableJson, null, 2)}`;
14174
16368
  `Tool breakdown:`
14175
16369
  ];
14176
16370
  for (const [name, stats] of Object.entries(m.toolBreakdown)) {
14177
- lines.push(` ${name}: ${stats.count} calls, avg ${stats.avgMs}ms${stats.errors > 0 ? `, ${stats.errors} errors` : ""}`);
16371
+ lines.push(
16372
+ ` ${name}: ${stats.count} calls, avg ${stats.avgMs}ms${stats.errors > 0 ? `, ${stats.errors} errors` : ""}`
16373
+ );
14178
16374
  }
14179
16375
  return asTextResponse(lines.join("\n"));
14180
16376
  }