@quanta-intellect/vessel-browser 0.1.12 → 0.1.13

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
@@ -2004,6 +2004,7 @@ const Channels = {
2004
2004
  BOOKMARKS_UPDATE: "bookmarks:update",
2005
2005
  BOOKMARK_SAVE: "bookmarks:save",
2006
2006
  BOOKMARK_REMOVE: "bookmarks:remove",
2007
+ BOOKMARK_ADD_CONTEXT_TO_CHAT: "bookmarks:add-context-to-chat",
2007
2008
  FOLDER_CREATE: "bookmarks:folder-create",
2008
2009
  FOLDER_REMOVE: "bookmarks:folder-remove",
2009
2010
  FOLDER_RENAME: "bookmarks:folder-rename",
@@ -2062,6 +2063,10 @@ async function getSidebarContextTarget(sidebarView, x, y) {
2062
2063
  return {
2063
2064
  inHighlightNav: !!nav,
2064
2065
  canRemoveCurrent: /\\d+\\s*\\/\\s*\\d+/.test(label),
2066
+ bookmarkId:
2067
+ el && typeof el.closest === "function"
2068
+ ? el.closest("[data-bookmark-id]")?.getAttribute("data-bookmark-id") || undefined
2069
+ : undefined,
2065
2070
  };
2066
2071
  })()`,
2067
2072
  true
@@ -2095,6 +2100,20 @@ async function showSidebarContextMenu(mainWindow, sidebarView, params) {
2095
2100
  })
2096
2101
  );
2097
2102
  }
2103
+ if (target.bookmarkId) {
2104
+ if (menu.items.length > 0) {
2105
+ menu.append(new electron.MenuItem({ type: "separator" }));
2106
+ }
2107
+ menu.append(
2108
+ new electron.MenuItem({
2109
+ label: "Add Context to Chat",
2110
+ click: () => sidebarView.webContents.send(
2111
+ Channels.BOOKMARK_ADD_CONTEXT_TO_CHAT,
2112
+ target.bookmarkId
2113
+ )
2114
+ })
2115
+ );
2116
+ }
2098
2117
  if (params.isEditable) {
2099
2118
  if (menu.items.length > 0) {
2100
2119
  menu.append(new electron.MenuItem({ type: "separator" }));
@@ -2226,7 +2245,14 @@ function createMainWindow(onTabStateChange) {
2226
2245
  return state2;
2227
2246
  }
2228
2247
  function layoutViews(state2) {
2229
- const { mainWindow, chromeView, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
2248
+ const {
2249
+ mainWindow,
2250
+ chromeView,
2251
+ sidebarView,
2252
+ devtoolsPanelView,
2253
+ tabManager,
2254
+ uiState
2255
+ } = state2;
2230
2256
  const [width, height] = mainWindow.getContentSize();
2231
2257
  const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
2232
2258
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
@@ -2971,11 +2997,25 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
2971
2997
  }
2972
2998
 
2973
2999
  function viewportWidth() {
2974
- return window.innerWidth || document.documentElement?.clientWidth || 0;
3000
+ return Math.max(
3001
+ window.innerWidth || 0,
3002
+ window.visualViewport?.width || 0,
3003
+ document.documentElement?.clientWidth || 0,
3004
+ document.scrollingElement?.clientWidth || 0,
3005
+ document.body?.clientWidth || 0,
3006
+ window.screen?.availWidth || 0,
3007
+ );
2975
3008
  }
2976
3009
 
2977
3010
  function viewportHeight() {
2978
- return window.innerHeight || document.documentElement?.clientHeight || 0;
3011
+ return Math.max(
3012
+ window.innerHeight || 0,
3013
+ window.visualViewport?.height || 0,
3014
+ document.documentElement?.clientHeight || 0,
3015
+ document.scrollingElement?.clientHeight || 0,
3016
+ document.body?.clientHeight || 0,
3017
+ window.screen?.availHeight || 0,
3018
+ );
2979
3019
  }
2980
3020
 
2981
3021
  function scrollingElement() {
@@ -3032,6 +3072,60 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3032
3072
  rect.bottom >= centerY;
3033
3073
  }
3034
3074
 
3075
+ function touchesViewportEdge(rect) {
3076
+ const edgePadding = 24;
3077
+ return rect.left <= edgePadding ||
3078
+ rect.top <= edgePadding ||
3079
+ rect.right >= viewportWidth() - edgePadding ||
3080
+ rect.bottom >= viewportHeight() - edgePadding;
3081
+ }
3082
+
3083
+ function hasFixedAncestor(el) {
3084
+ var cur = el.parentElement;
3085
+ while (cur && cur !== document.body) {
3086
+ var ps = getComputedStyle(cur).position;
3087
+ if (ps === "fixed" || ps === "sticky") return true;
3088
+ cur = cur.parentElement;
3089
+ }
3090
+ return false;
3091
+ }
3092
+
3093
+ function isPositioned(style) {
3094
+ return style.position === "fixed" || style.position === "sticky" ||
3095
+ style.position === "absolute";
3096
+ }
3097
+
3098
+ function effectiveZIndex(style, el) {
3099
+ var z = parseZIndex(style);
3100
+ if (z > 0) return z;
3101
+ var cur = el.parentElement;
3102
+ while (cur && cur !== document.body) {
3103
+ var pz = parseZIndex(getComputedStyle(cur));
3104
+ if (pz > 0) return pz;
3105
+ cur = cur.parentElement;
3106
+ }
3107
+ return 0;
3108
+ }
3109
+
3110
+ function looksLikeDrawer(style, rect, areaRatio, el) {
3111
+ if (rect.width < 220 || rect.height < 160 || areaRatio < 0.08) return false;
3112
+ if (!touchesViewportEdge(rect)) return false;
3113
+ if (style.position === "fixed" || style.position === "sticky") {
3114
+ return effectiveZIndex(style, el) >= 5;
3115
+ }
3116
+ if (style.position === "absolute" && hasFixedAncestor(el)) {
3117
+ return effectiveZIndex(style, el) >= 5;
3118
+ }
3119
+ return false;
3120
+ }
3121
+
3122
+ function looksLikeCartConfirmation(node) {
3123
+ var t = (node.textContent || "").slice(0, 500).toLowerCase();
3124
+ var signals = ["added to cart", "added to bag", "added to basket",
3125
+ "added to your cart", "added to your bag", "added to your basket"];
3126
+ return signals.some(function(s) { return t.indexOf(s) !== -1; });
3127
+ }
3128
+
3035
3129
  function detectOverlays() {
3036
3130
  if (!document.body) return [];
3037
3131
  const viewportArea = Math.max(1, viewportWidth() * viewportHeight());
@@ -3070,7 +3164,13 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3070
3164
  var type = overlayType(node);
3071
3165
  var dialogLike = type === "dialog" || type === "modal";
3072
3166
  var areaRatio = (rect.width * rect.height) / viewportArea;
3167
+ var drawerLike = looksLikeDrawer(style, rect, areaRatio, node);
3168
+ var cartConfirm = !dialogLike && !drawerLike && isPositioned(style) &&
3169
+ rect.width >= 160 && rect.height >= 100 &&
3170
+ looksLikeCartConfirmation(node);
3073
3171
  var blocksInteraction = dialogLike ||
3172
+ drawerLike ||
3173
+ cartConfirm ||
3074
3174
  ((style.position === "fixed" || style.position === "sticky") &&
3075
3175
  parseZIndex(style) >= 10 &&
3076
3176
  areaRatio >= 0.3 &&
@@ -3188,6 +3288,11 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3188
3288
  }
3189
3289
 
3190
3290
  function contextOf(el) {
3291
+ const overlayRoot = overlays.find(
3292
+ (overlay) => overlay.element === el || overlay.element.contains(el),
3293
+ );
3294
+ if (overlayRoot) return "dialog";
3295
+
3191
3296
  let current = el.parentElement;
3192
3297
  while (current) {
3193
3298
  const tag = current.tagName.toLowerCase();
@@ -4536,7 +4641,7 @@ function formatElementMeta(el) {
4536
4641
  if (el.description) {
4537
4642
  meta.push(`desc="${el.description.slice(0, 80)}"`);
4538
4643
  }
4539
- if (el.value) {
4644
+ if (el.value !== void 0 && el.value !== null && el.value !== "") {
4540
4645
  meta.push(`value="${el.value.slice(0, 60)}"`);
4541
4646
  }
4542
4647
  if (el.selector) {
@@ -4545,14 +4650,207 @@ function formatElementMeta(el) {
4545
4650
  }
4546
4651
  return meta;
4547
4652
  }
4653
+ function summarizeElementValue(el) {
4654
+ const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
4655
+ if (!value) return null;
4656
+ if (el.type === "select") {
4657
+ return { label: "selected", value: value.slice(0, 60) };
4658
+ }
4659
+ if (el.type === "textarea") {
4660
+ return { label: "current", value: value.slice(0, 60) };
4661
+ }
4662
+ if (el.type === "input") {
4663
+ return { label: "current", value: value.slice(0, 60) };
4664
+ }
4665
+ return null;
4666
+ }
4667
+ function isQuantityLike(el) {
4668
+ const text = [
4669
+ el.label,
4670
+ el.name,
4671
+ el.placeholder,
4672
+ el.text,
4673
+ el.description,
4674
+ el.selector
4675
+ ].filter(Boolean).join(" ").toLowerCase();
4676
+ 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");
4677
+ }
4678
+ function getQuantityElements(page) {
4679
+ const seen = /* @__PURE__ */ new Set();
4680
+ const elements = [
4681
+ ...page.interactiveElements,
4682
+ ...page.forms.flatMap((form) => form.fields)
4683
+ ];
4684
+ return elements.filter((el) => {
4685
+ if (!isQuantityLike(el)) return false;
4686
+ const key = String(
4687
+ el.index ?? el.selector ?? `${el.type}|${el.name || ""}|${el.label || ""}|${el.value || ""}`
4688
+ );
4689
+ if (seen.has(key)) return false;
4690
+ seen.add(key);
4691
+ return true;
4692
+ });
4693
+ }
4694
+ function formatQuantityElements(elements) {
4695
+ if (elements.length === 0) return "None detected";
4696
+ return limitItems(elements, 12).map((el) => {
4697
+ const prefix = el.index ? `[#${el.index}]` : "-";
4698
+ const name = el.label || el.name || el.placeholder || "Quantity";
4699
+ const summary = summarizeElementValue(el);
4700
+ const parts = [prefix, `[${name}]`, el.type];
4701
+ if (summary) {
4702
+ parts.push(`${summary.label}="${summary.value}"`);
4703
+ }
4704
+ const meta = formatElementMeta({
4705
+ ...el,
4706
+ value: void 0
4707
+ });
4708
+ if (meta.length > 0) {
4709
+ parts.push(`(${meta.join(", ")})`);
4710
+ }
4711
+ return parts.join(" ");
4712
+ }).join("\n");
4713
+ }
4714
+ function isCartLikePage(page) {
4715
+ const url = page.url.toLowerCase();
4716
+ const text = `${page.title}
4717
+ ${page.content}`.toLowerCase();
4718
+ 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);
4719
+ }
4720
+ function getCartItemLinks(page) {
4721
+ const blockedText = /\b(remove|delete|wishlist|save for later|move to|checkout|view cart|continue shopping|edit|details?)\b/i;
4722
+ const blockedHref = /\/(cart|checkout|wishlist|account|login|signin|remove|delete)(\/|$)|[?&](remove|delete|wishlist)=/i;
4723
+ const seen = /* @__PURE__ */ new Set();
4724
+ return page.interactiveElements.filter((el) => el.type === "link").filter((el) => {
4725
+ const text = (el.text || "").trim();
4726
+ const href = (el.href || "").trim();
4727
+ if (!text || text.length < 3 || !href) return false;
4728
+ if (el.context === "nav" || el.context === "footer" || el.context === "sidebar") {
4729
+ return false;
4730
+ }
4731
+ if (blockedText.test(text) || blockedHref.test(href)) return false;
4732
+ const key = `${normalizeComparable(text)}|${normalizeUrlForMatch(href) || href}`;
4733
+ if (seen.has(key)) return false;
4734
+ seen.add(key);
4735
+ return true;
4736
+ }).slice(0, 12);
4737
+ }
4738
+ function extractCartTotals(content) {
4739
+ const lines = content.split(/\n+/).map((line) => line.trim()).filter(Boolean);
4740
+ const totalLines = [];
4741
+ const seen = /* @__PURE__ */ new Set();
4742
+ const keyword = /\b(subtotal|order total|estimated total|total|tax|shipping|discount|savings?)\b/i;
4743
+ const money = /([$€£]\s?\d[\d,]*(?:\.\d{2})?|\d[\d,]*(?:\.\d{2})?\s?(?:usd|eur|gbp))/i;
4744
+ for (const line of lines) {
4745
+ if (!keyword.test(line)) continue;
4746
+ if (!money.test(line) && line.length > 90) continue;
4747
+ const cleaned = line.replace(/\s+/g, " ").trim();
4748
+ if (seen.has(cleaned.toLowerCase())) continue;
4749
+ seen.add(cleaned.toLowerCase());
4750
+ totalLines.push(cleaned);
4751
+ if (totalLines.length >= 6) break;
4752
+ }
4753
+ return totalLines;
4754
+ }
4755
+ function formatCartSnapshot(page) {
4756
+ if (!isCartLikePage(page)) return null;
4757
+ const itemLinks = getCartItemLinks(page);
4758
+ const quantityElements = getQuantityElements(page);
4759
+ const quantityValues = quantityElements.map((el) => summarizeElementValue(el)?.value || "").filter(Boolean);
4760
+ const numericQuantities = quantityValues.map((value) => Number.parseFloat(value)).filter((value) => Number.isFinite(value) && value >= 0);
4761
+ const totalLines = extractCartTotals(page.content);
4762
+ const lines = [];
4763
+ if (itemLinks.length > 0) {
4764
+ lines.push(`Distinct items: ${itemLinks.length}`);
4765
+ lines.push(
4766
+ `Items: ${itemLinks.slice(0, 8).map((item) => item.text || item.label || "Untitled item").join(" | ")}`
4767
+ );
4768
+ }
4769
+ if (quantityElements.length > 0) {
4770
+ if (numericQuantities.length === quantityElements.length && numericQuantities.length > 0) {
4771
+ const unique = Array.from(new Set(numericQuantities));
4772
+ const totalUnits = numericQuantities.reduce(
4773
+ (sum, value) => sum + value,
4774
+ 0
4775
+ );
4776
+ lines.push(
4777
+ unique.length === 1 ? `Quantity controls: ${quantityElements.length} (all set to ${unique[0]})` : `Quantity controls: ${quantityElements.length} (${numericQuantities.join(", ")})`
4778
+ );
4779
+ lines.push(`Total units inferred: ${totalUnits}`);
4780
+ if (itemLinks.length > 0 && totalUnits > itemLinks.length) {
4781
+ lines.push(
4782
+ `Attention: ${itemLinks.length} distinct items but ${totalUnits} total units. Check for duplicate quantities.`
4783
+ );
4784
+ }
4785
+ } else {
4786
+ lines.push(
4787
+ `Quantity controls: ${quantityElements.length}${quantityValues.length > 0 ? ` (${quantityValues.join(", ")})` : ""}`
4788
+ );
4789
+ }
4790
+ }
4791
+ if (totalLines.length > 0) {
4792
+ lines.push("Totals:");
4793
+ totalLines.forEach((line) => lines.push(`- ${line}`));
4794
+ }
4795
+ if (lines.length === 0) return null;
4796
+ return lines.join("\n");
4797
+ }
4548
4798
  function isVisibleToUser(el) {
4549
4799
  return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
4550
4800
  }
4801
+ function getDialogFocusedElements(page) {
4802
+ return page.interactiveElements.filter(
4803
+ (el) => isVisibleToUser(el) && el.context === "dialog"
4804
+ );
4805
+ }
4806
+ function normalizeOverlayText(value) {
4807
+ return (value || "").trim().toLowerCase();
4808
+ }
4809
+ function isCartConfirmationLike(page) {
4810
+ const overlayText = page.overlays.map(
4811
+ (overlay) => normalizeOverlayText(
4812
+ [overlay.label, overlay.text].filter(Boolean).join(" ")
4813
+ )
4814
+ ).join(" ");
4815
+ const dialogText = getDialogFocusedElements(page).map((el) => normalizeOverlayText(el.text || el.label || el.description)).join(" ");
4816
+ const haystack = `${overlayText} ${dialogText}`.trim();
4817
+ if (!haystack) return false;
4818
+ const cartSignals = [
4819
+ "added to cart",
4820
+ "added to bag",
4821
+ "added to basket",
4822
+ "shopping cart",
4823
+ "view cart",
4824
+ "go to cart",
4825
+ "continue shopping",
4826
+ "keep shopping",
4827
+ "checkout"
4828
+ ];
4829
+ return cartSignals.some((signal) => haystack.includes(signal));
4830
+ }
4831
+ function formatDialogFocus(page) {
4832
+ const dialogElements = getDialogFocusedElements(page);
4833
+ if (dialogElements.length === 0) return null;
4834
+ const lines = [];
4835
+ lines.push(
4836
+ "A live dialog/modal is open. Prioritize its controls before acting on the page behind it."
4837
+ );
4838
+ if (isCartConfirmationLike(page)) {
4839
+ lines.push(
4840
+ "Cart confirmation detected: choose a dialog action such as Continue Shopping, View Cart, or Checkout. Do not click background Add to Cart again."
4841
+ );
4842
+ }
4843
+ lines.push("");
4844
+ lines.push("Visible dialog controls:");
4845
+ lines.push(formatInteractiveElements(dialogElements));
4846
+ return lines.join("\n");
4847
+ }
4551
4848
  function formatInteractiveElements(elements) {
4552
4849
  if (elements.length === 0) return "None";
4553
4850
  const sorted = [...elements].sort((a, b) => {
4554
4851
  const scoreEl = (el) => {
4555
4852
  let s = 0;
4853
+ if (el.context === "dialog") s -= 40;
4556
4854
  if (el.visible === false) s += 100;
4557
4855
  if (el.inViewport === false) s += 50;
4558
4856
  if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
@@ -4578,10 +4876,14 @@ function formatInteractiveElements(elements) {
4578
4876
  parts.push(`[${el.label || el.placeholder || "Input"}]`);
4579
4877
  parts.push(el.inputType || "text");
4580
4878
  parts.push("input");
4879
+ const summary = summarizeElementValue(el);
4880
+ if (summary) parts.push(`${summary.label}="${summary.value}"`);
4581
4881
  if (el.required) parts.push("(required)");
4582
4882
  } else if (el.type === "select") {
4583
4883
  parts.push(`[${el.label || "Select"}]`);
4584
4884
  parts.push("dropdown");
4885
+ const summary = summarizeElementValue(el);
4886
+ if (summary) parts.push(`${summary.label}="${summary.value}"`);
4585
4887
  if (el.options?.length) {
4586
4888
  parts.push(
4587
4889
  `options=${el.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
@@ -4590,6 +4892,8 @@ function formatInteractiveElements(elements) {
4590
4892
  } else if (el.type === "textarea") {
4591
4893
  parts.push(`[${el.label || "Text Area"}]`);
4592
4894
  parts.push("textarea");
4895
+ const summary = summarizeElementValue(el);
4896
+ if (summary) parts.push(`${summary.label}="${summary.value}"`);
4593
4897
  }
4594
4898
  const meta = formatElementMeta(el);
4595
4899
  if (meta.length > 0) parts.push(`(${meta.join(", ")})`);
@@ -4632,10 +4936,14 @@ function formatForms(forms) {
4632
4936
  } else if (field.type === "input") {
4633
4937
  fieldParts.push(`[${field.label || field.placeholder || "Input"}]`);
4634
4938
  fieldParts.push(field.inputType || "text");
4939
+ const summary = summarizeElementValue(field);
4940
+ if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
4635
4941
  if (field.required) fieldParts.push("(required)");
4636
4942
  } else if (field.type === "select") {
4637
4943
  fieldParts.push(`[${field.label || "Select"}]`);
4638
4944
  fieldParts.push("dropdown");
4945
+ const summary = summarizeElementValue(field);
4946
+ if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
4639
4947
  if (field.options?.length) {
4640
4948
  fieldParts.push(
4641
4949
  `options=${field.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
@@ -4644,6 +4952,8 @@ function formatForms(forms) {
4644
4952
  } else if (field.type === "textarea") {
4645
4953
  fieldParts.push(`[${field.label || "Text"}]`);
4646
4954
  fieldParts.push("textarea");
4955
+ const summary = summarizeElementValue(field);
4956
+ if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
4647
4957
  }
4648
4958
  const meta = formatElementMeta(field);
4649
4959
  if (meta.length > 0) fieldParts.push(`(${meta.join(", ")})`);
@@ -5009,6 +5319,7 @@ function buildScopedContext(page, mode) {
5009
5319
  switch (mode) {
5010
5320
  case "summary": {
5011
5321
  const sections = [];
5322
+ const cartSnapshot = formatCartSnapshot(page);
5012
5323
  sections.push(`**URL:** ${page.url}`);
5013
5324
  sections.push(`**Title:** ${page.title}`);
5014
5325
  sections.push(`**Viewport:** ${formatViewport(page)}`);
@@ -5022,6 +5333,11 @@ function buildScopedContext(page, mode) {
5022
5333
  sections.push(summaryIntent);
5023
5334
  sections.push("");
5024
5335
  }
5336
+ if (cartSnapshot) {
5337
+ sections.push("### Cart Snapshot");
5338
+ sections.push(cartSnapshot);
5339
+ sections.push("");
5340
+ }
5025
5341
  if ((page.pageIssues?.length ?? 0) > 0) {
5026
5342
  sections.push("### Page Access Warnings");
5027
5343
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5073,6 +5389,9 @@ function buildScopedContext(page, mode) {
5073
5389
  }
5074
5390
  case "interactives_only": {
5075
5391
  const sections = [];
5392
+ const quantityElements = getQuantityElements(page);
5393
+ const cartSnapshot = formatCartSnapshot(page);
5394
+ const dialogFocus = formatDialogFocus(page);
5076
5395
  sections.push(`**URL:** ${page.url}`);
5077
5396
  sections.push(`**Title:** ${page.title}`);
5078
5397
  sections.push(`**Viewport:** ${formatViewport(page)}`);
@@ -5088,6 +5407,11 @@ function buildScopedContext(page, mode) {
5088
5407
  sections.push(formatHighlights(interactivesHighlights));
5089
5408
  sections.push("");
5090
5409
  }
5410
+ if (cartSnapshot) {
5411
+ sections.push("### Cart Snapshot");
5412
+ sections.push(cartSnapshot);
5413
+ sections.push("");
5414
+ }
5091
5415
  if ((page.pageIssues?.length ?? 0) > 0) {
5092
5416
  sections.push("### Page Access Warnings");
5093
5417
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5098,6 +5422,11 @@ function buildScopedContext(page, mode) {
5098
5422
  sections.push(formatOverlays(page.overlays));
5099
5423
  sections.push("");
5100
5424
  }
5425
+ if (dialogFocus) {
5426
+ sections.push("### Immediate Overlay Actions");
5427
+ sections.push(dialogFocus);
5428
+ sections.push("");
5429
+ }
5101
5430
  if (page.dormantOverlays.length > 0) {
5102
5431
  sections.push("### Dormant Consent / Modal UI");
5103
5432
  sections.push(formatDormantOverlays(page.dormantOverlays));
@@ -5108,6 +5437,11 @@ function buildScopedContext(page, mode) {
5108
5437
  sections.push(formatNavigation(page.navigation));
5109
5438
  sections.push("");
5110
5439
  }
5440
+ if (quantityElements.length > 0) {
5441
+ sections.push("### Quantity / Count Controls");
5442
+ sections.push(formatQuantityElements(quantityElements));
5443
+ sections.push("");
5444
+ }
5111
5445
  if (page.interactiveElements.length > 0) {
5112
5446
  sections.push(
5113
5447
  `### Interactive Elements (${page.interactiveElements.length})`
@@ -5118,6 +5452,8 @@ function buildScopedContext(page, mode) {
5118
5452
  }
5119
5453
  case "forms_only": {
5120
5454
  const sections = [];
5455
+ const quantityElements = getQuantityElements(page);
5456
+ const cartSnapshot = formatCartSnapshot(page);
5121
5457
  sections.push(`**URL:** ${page.url}`);
5122
5458
  sections.push(`**Title:** ${page.title}`);
5123
5459
  sections.push(`**Viewport:** ${formatViewport(page)}`);
@@ -5128,6 +5464,11 @@ function buildScopedContext(page, mode) {
5128
5464
  sections.push(formatHighlights(formsHighlights));
5129
5465
  sections.push("");
5130
5466
  }
5467
+ if (cartSnapshot) {
5468
+ sections.push("### Cart Snapshot");
5469
+ sections.push(cartSnapshot);
5470
+ sections.push("");
5471
+ }
5131
5472
  if ((page.pageIssues?.length ?? 0) > 0) {
5132
5473
  sections.push("### Page Access Warnings");
5133
5474
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5143,6 +5484,11 @@ function buildScopedContext(page, mode) {
5143
5484
  sections.push(formatDormantOverlays(page.dormantOverlays));
5144
5485
  sections.push("");
5145
5486
  }
5487
+ if (quantityElements.length > 0) {
5488
+ sections.push("### Quantity / Count Controls");
5489
+ sections.push(formatQuantityElements(quantityElements));
5490
+ sections.push("");
5491
+ }
5146
5492
  if (page.forms.length > 0) {
5147
5493
  sections.push(`### Forms (${page.forms.length})`);
5148
5494
  sections.push(formatForms(page.forms));
@@ -5175,10 +5521,21 @@ function buildScopedContext(page, mode) {
5175
5521
  case "visible_only": {
5176
5522
  const visibleElements = page.interactiveElements.filter(isVisibleToUser);
5177
5523
  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);
5524
+ const dialogFocusedElements = getDialogFocusedElements(page);
5525
+ const visiblePage = {
5526
+ ...page,
5527
+ interactiveElements: dialogFocusedElements.length > 0 ? dialogFocusedElements : visibleElements,
5528
+ forms: page.forms.map((form) => ({
5529
+ ...form,
5530
+ fields: form.fields.filter(
5531
+ (field) => isVisibleToUser(field) && (dialogFocusedElements.length === 0 || field.context === "dialog")
5532
+ )
5533
+ })).filter((form) => form.fields.length > 0)
5534
+ };
5535
+ const quantityElements = getQuantityElements(visiblePage);
5536
+ const cartSnapshot = formatCartSnapshot(visiblePage);
5537
+ const visibleForms = visiblePage.forms;
5538
+ const dialogFocus = formatDialogFocus(page);
5182
5539
  const sections = [];
5183
5540
  sections.push(`**URL:** ${page.url}`);
5184
5541
  sections.push(`**Title:** ${page.title}`);
@@ -5190,6 +5547,11 @@ function buildScopedContext(page, mode) {
5190
5547
  sections.push(formatHighlights(visibleHighlights));
5191
5548
  sections.push("");
5192
5549
  }
5550
+ if (cartSnapshot) {
5551
+ sections.push("### Cart Snapshot");
5552
+ sections.push(cartSnapshot);
5553
+ sections.push("");
5554
+ }
5193
5555
  if ((page.pageIssues?.length ?? 0) > 0) {
5194
5556
  sections.push("### Page Access Warnings");
5195
5557
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -5200,6 +5562,17 @@ function buildScopedContext(page, mode) {
5200
5562
  sections.push(formatOverlays(page.overlays));
5201
5563
  sections.push("");
5202
5564
  }
5565
+ if (dialogFocus) {
5566
+ sections.push("### Immediate Overlay Actions");
5567
+ sections.push(dialogFocus);
5568
+ if (visibleElements.length > dialogFocusedElements.length) {
5569
+ sections.push("");
5570
+ sections.push(
5571
+ `Background controls hidden while the dialog is active: ${visibleElements.length - dialogFocusedElements.length}`
5572
+ );
5573
+ }
5574
+ sections.push("");
5575
+ }
5203
5576
  if (page.dormantOverlays.length > 0) {
5204
5577
  sections.push("### Dormant Consent / Modal UI");
5205
5578
  sections.push(formatDormantOverlays(page.dormantOverlays));
@@ -5210,11 +5583,18 @@ function buildScopedContext(page, mode) {
5210
5583
  sections.push(formatNavigation(visibleNav));
5211
5584
  sections.push("");
5212
5585
  }
5213
- if (visibleElements.length > 0) {
5586
+ if (quantityElements.length > 0) {
5587
+ sections.push("### Quantity / Count Controls");
5588
+ sections.push(formatQuantityElements(quantityElements));
5589
+ sections.push("");
5590
+ }
5591
+ if (visiblePage.interactiveElements.length > 0) {
5592
+ sections.push(
5593
+ `### Visible In-Viewport Interactive Elements (${visiblePage.interactiveElements.length})`
5594
+ );
5214
5595
  sections.push(
5215
- `### Visible In-Viewport Interactive Elements (${visibleElements.length})`
5596
+ formatInteractiveElements(visiblePage.interactiveElements)
5216
5597
  );
5217
- sections.push(formatInteractiveElements(visibleElements));
5218
5598
  sections.push("");
5219
5599
  }
5220
5600
  if (visibleForms.length > 0) {
@@ -5654,6 +6034,18 @@ const TOOL_DEFINITIONS = [
5654
6034
  description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions.",
5655
6035
  tier: 1
5656
6036
  },
6037
+ {
6038
+ name: "inspect_element",
6039
+ title: "Inspect Element",
6040
+ 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.",
6041
+ inputSchema: {
6042
+ index: zod.z.number().optional().describe("Element index to inspect"),
6043
+ selector: zod.z.string().optional().describe("CSS selector to inspect"),
6044
+ limit: zod.z.number().optional().describe("Maximum nearby controls to include (default 8)")
6045
+ },
6046
+ tier: 1,
6047
+ relevance: ["SEARCH_RESULTS", "SHOPPING", "FORM"]
6048
+ },
5657
6049
  {
5658
6050
  name: "read_page",
5659
6051
  title: "Read Page",
@@ -5870,7 +6262,8 @@ const TOOL_DEFINITIONS = [
5870
6262
  "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
5871
6263
  )
5872
6264
  },
5873
- tier: 1
6265
+ tier: 1,
6266
+ hiddenByDefault: true
5874
6267
  },
5875
6268
  {
5876
6269
  name: "flow_advance",
@@ -5879,26 +6272,30 @@ const TOOL_DEFINITIONS = [
5879
6272
  inputSchema: {
5880
6273
  detail: zod.z.string().optional().describe("Brief note about what was accomplished")
5881
6274
  },
5882
- tier: 1
6275
+ tier: 1,
6276
+ hiddenByDefault: true
5883
6277
  },
5884
6278
  {
5885
6279
  name: "flow_status",
5886
6280
  title: "Workflow Status",
5887
6281
  description: "Check the current workflow progress.",
5888
- tier: 2
6282
+ tier: 2,
6283
+ hiddenByDefault: true
5889
6284
  },
5890
6285
  {
5891
6286
  name: "flow_end",
5892
6287
  title: "End Workflow",
5893
6288
  description: "Clear the active workflow tracker.",
5894
- tier: 2
6289
+ tier: 2,
6290
+ hiddenByDefault: true
5895
6291
  },
5896
6292
  // --- Speedee System: Suggestion Engine ---
5897
6293
  {
5898
6294
  name: "suggest",
5899
6295
  title: "What Should I Do?",
5900
6296
  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
6297
+ tier: 1,
6298
+ hiddenByDefault: true
5902
6299
  },
5903
6300
  // --- Speedee System: Composable Macros ---
5904
6301
  {
@@ -5910,9 +6307,14 @@ const TOOL_DEFINITIONS = [
5910
6307
  zod.z.object({
5911
6308
  index: zod.z.number().optional().describe("Element index from page content"),
5912
6309
  selector: zod.z.string().optional().describe("CSS selector fallback"),
6310
+ name: zod.z.string().optional().describe("Field name or id, such as custname"),
6311
+ label: zod.z.string().optional().describe("Visible label or aria-label text"),
6312
+ placeholder: zod.z.string().optional().describe("Placeholder text shown in the field"),
5913
6313
  value: zod.z.string().describe("Value to enter")
5914
6314
  })
5915
- ).describe("Fields to fill"),
6315
+ ).describe(
6316
+ "Fields to fill, matched by index, selector, name, label, or placeholder"
6317
+ ),
5916
6318
  submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
5917
6319
  },
5918
6320
  tier: 1,
@@ -5998,14 +6400,16 @@ const TOOL_DEFINITIONS = [
5998
6400
  inputSchema: {
5999
6401
  timeoutMs: zod.z.number().optional().describe("Maximum time to wait in milliseconds (default 10000)")
6000
6402
  },
6001
- tier: 1
6403
+ tier: 1,
6404
+ hiddenByDefault: true
6002
6405
  },
6003
6406
  // --- Speedee System: Metrics ---
6004
6407
  {
6005
6408
  name: "metrics",
6006
6409
  title: "Session Metrics",
6007
6410
  description: "Show performance metrics for this session: total tool calls, average duration, per-tool breakdown, and error rates.",
6008
- tier: 2
6411
+ tier: 2,
6412
+ hiddenByDefault: true
6009
6413
  }
6010
6414
  ];
6011
6415
  function toAnthropicTools(defs) {
@@ -6047,16 +6451,19 @@ const CONTEXT_HINTS = {
6047
6451
  SEARCH_RESULTS: {
6048
6452
  paginate: "⚡ PAGINATION DETECTED — ",
6049
6453
  search: "⚡ Refine search — ",
6454
+ inspect_element: "⚡ Inspect one result card without reading the whole page — ",
6050
6455
  highlight: "💡 Mark interesting results — "
6051
6456
  },
6052
6457
  SHOPPING: {
6053
6458
  fill_form: "⚡ CHECKOUT FIELDS DETECTED — ",
6054
- select_option: "⚡ Payment/shipping options available — "
6459
+ select_option: "⚡ Payment/shipping options available — ",
6460
+ inspect_element: "⚡ Inspect the current product card or option group — "
6055
6461
  },
6056
6462
  FORM: {
6057
6463
  fill_form: "⚡ FORM DETECTED — ",
6058
6464
  select_option: "⚡ Dropdown fields on page — ",
6059
- submit_form: "⚡ Form ready to submit — "
6465
+ submit_form: "⚡ Form ready to submit — ",
6466
+ inspect_element: "⚡ Inspect just this form section — "
6060
6467
  },
6061
6468
  PAGINATED_LIST: {
6062
6469
  paginate: "⚡ PAGINATION DETECTED — ",
@@ -6079,10 +6486,105 @@ function scoreForContext(toolName, pageType) {
6079
6486
  if (tier === 2 && isRelevant) return 20;
6080
6487
  return 40;
6081
6488
  }
6082
- function pruneToolsForContext(tools, pageType) {
6489
+ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
6490
+ "current_tab",
6491
+ "navigate",
6492
+ "click",
6493
+ "type_text",
6494
+ "press_key",
6495
+ "search",
6496
+ "scroll",
6497
+ "dismiss_popup",
6498
+ "accept_cookies",
6499
+ "wait_for",
6500
+ "read_page",
6501
+ "inspect_element"
6502
+ ]);
6503
+ function inferIntent(query) {
6504
+ const lowered = query.toLowerCase();
6505
+ const intents = /* @__PURE__ */ new Set();
6506
+ if (/\b(tab|tabs|window|windows)\b/.test(lowered)) intents.add("tabs");
6507
+ if (/\b(bookmark|bookmarks|save this|folder)\b/.test(lowered)) {
6508
+ intents.add("bookmarks");
6509
+ }
6510
+ if (/\b(session|cookies|log in|login|sign in|sign-in|resume)\b/.test(lowered)) {
6511
+ intents.add("sessions");
6512
+ }
6513
+ if (/\b(flow|workflow|checkpoint|step|progress|plan)\b/.test(lowered)) {
6514
+ intents.add("workflow");
6515
+ }
6516
+ if (/\b(metric|metrics|performance|slow|latency)\b/.test(lowered)) {
6517
+ intents.add("metrics");
6518
+ }
6519
+ if (/\b(highlight|mark|annotate)\b/.test(lowered)) intents.add("highlight");
6520
+ if (/\b(table|csv|rows|columns)\b/.test(lowered)) intents.add("table");
6521
+ if (/\b(debug|diagnose|what should i do|stuck|inspect)\b/.test(lowered)) {
6522
+ intents.add("debug");
6523
+ }
6524
+ return intents;
6525
+ }
6526
+ function shouldIncludeTool(toolName, pageType, intents) {
6527
+ if (ALWAYS_FAST_TOOL_NAMES.has(toolName)) return true;
6528
+ switch (toolName) {
6529
+ case "select_option":
6530
+ case "submit_form":
6531
+ case "fill_form":
6532
+ return pageType === "FORM" || pageType === "SHOPPING" || pageType === "LOGIN";
6533
+ case "paginate":
6534
+ return pageType === "SEARCH_RESULTS" || pageType === "PAGINATED_LIST";
6535
+ case "login":
6536
+ return pageType === "LOGIN" || intents.has("sessions");
6537
+ case "focus":
6538
+ return pageType === "FORM" || pageType === "LOGIN" || pageType === "SEARCH_READY";
6539
+ case "scroll_to_element":
6540
+ return pageType === "SEARCH_RESULTS" || pageType === "SHOPPING" || intents.has("debug");
6541
+ case "go_back":
6542
+ return true;
6543
+ case "go_forward":
6544
+ case "reload":
6545
+ case "hover":
6546
+ return intents.has("debug");
6547
+ case "highlight":
6548
+ case "clear_highlights":
6549
+ return intents.has("highlight");
6550
+ case "list_tabs":
6551
+ case "switch_tab":
6552
+ case "create_tab":
6553
+ case "set_ad_blocking":
6554
+ return intents.has("tabs") || intents.has("debug");
6555
+ case "save_session":
6556
+ case "load_session":
6557
+ case "list_sessions":
6558
+ case "delete_session":
6559
+ return intents.has("sessions");
6560
+ case "list_bookmarks":
6561
+ case "search_bookmarks":
6562
+ case "create_bookmark_folder":
6563
+ case "save_bookmark":
6564
+ case "organize_bookmark":
6565
+ case "archive_bookmark":
6566
+ case "open_bookmark":
6567
+ return intents.has("bookmarks");
6568
+ case "flow_start":
6569
+ case "flow_advance":
6570
+ case "flow_status":
6571
+ case "flow_end":
6572
+ return intents.has("workflow");
6573
+ case "suggest":
6574
+ case "wait_for_navigation":
6575
+ case "metrics":
6576
+ return intents.has("debug") || intents.has("metrics");
6577
+ case "extract_table":
6578
+ return intents.has("table");
6579
+ default:
6580
+ return !defByName[toolName]?.hiddenByDefault;
6581
+ }
6582
+ }
6583
+ function pruneToolsForContext(tools, pageType, query = "") {
6083
6584
  const ctx = pageType ?? "GENERAL";
6084
6585
  const hints = CONTEXT_HINTS[ctx] ?? {};
6085
- const scored = tools.map((tool) => ({
6586
+ const intents = inferIntent(query);
6587
+ const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
6086
6588
  tool,
6087
6589
  score: scoreForContext(tool.name, ctx)
6088
6590
  }));
@@ -6682,7 +7184,7 @@ async function validateLinkDestination(url, timeoutMs = 3500) {
6682
7184
  function formatDeadLinkMessage(label, result) {
6683
7185
  const destination = result.finalUrl || result.checkedUrl;
6684
7186
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
6685
- return `Skipped stale link "${label}" because ${destination} returned ${status}.`;
7187
+ return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
6686
7188
  }
6687
7189
  const SESSION_VERSION = 1;
6688
7190
  function getSessionsDir() {
@@ -7320,10 +7822,297 @@ async function describeElementForClick$1(wc, selector) {
7320
7822
  href: "href" in result && typeof result.href === "string" ? result.href : void 0
7321
7823
  };
7322
7824
  }
7825
+ async function inspectElement(wc, selector, limit = 8) {
7826
+ const result = await executePageScript(
7827
+ wc,
7828
+ `
7829
+ (function() {
7830
+ function text(value) {
7831
+ const trimmed = value == null ? "" : String(value).trim();
7832
+ return trimmed || undefined;
7833
+ }
7834
+
7835
+ function escapeSelectorValue(value) {
7836
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
7837
+ return CSS.escape(value);
7838
+ }
7839
+ return String(value).replace(/["\\\\]/g, "\\\\$&");
7840
+ }
7841
+
7842
+ function uniqueSelector(candidate) {
7843
+ if (!candidate) return null;
7844
+ try {
7845
+ return document.querySelectorAll(candidate).length === 1 ? candidate : null;
7846
+ } catch {
7847
+ return null;
7848
+ }
7849
+ }
7850
+
7851
+ function uniqueAttributeSelector(el, attribute) {
7852
+ const value = text(el.getAttribute && el.getAttribute(attribute));
7853
+ if (!value) return null;
7854
+ const candidate = el.tagName.toLowerCase() + "[" + attribute + "=\\"" + escapeSelectorValue(value) + "\\"]";
7855
+ return uniqueSelector(candidate);
7856
+ }
7857
+
7858
+ function selectorFor(el) {
7859
+ if (!el) return null;
7860
+ if (el.id) return "#" + escapeSelectorValue(el.id);
7861
+ for (const attribute of ["data-testid", "name", "form", "aria-label", "title"]) {
7862
+ const candidate = uniqueAttributeSelector(el, attribute);
7863
+ if (candidate) return candidate;
7864
+ }
7865
+ const parts = [];
7866
+ let current = el;
7867
+ while (current) {
7868
+ if (current.id) {
7869
+ parts.unshift("#" + escapeSelectorValue(current.id));
7870
+ break;
7871
+ }
7872
+ const tag = current.tagName.toLowerCase();
7873
+ const parent = current.parentElement;
7874
+ if (!parent) { parts.unshift(tag); break; }
7875
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
7876
+ const index = siblings.indexOf(current) + 1;
7877
+ parts.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
7878
+ current = parent;
7879
+ }
7880
+ const selector = parts.join(" > ");
7881
+ return uniqueSelector(selector) || selector;
7882
+ }
7883
+
7884
+ function isVisible(el) {
7885
+ if (!(el instanceof HTMLElement)) return true;
7886
+ const style = window.getComputedStyle(el);
7887
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
7888
+ return false;
7889
+ }
7890
+ const rect = el.getBoundingClientRect();
7891
+ return rect.width > 0 && rect.height > 0;
7892
+ }
7893
+
7894
+ function labelFor(el) {
7895
+ return text(
7896
+ el.getAttribute("aria-label") ||
7897
+ el.getAttribute("title") ||
7898
+ el.getAttribute("name") ||
7899
+ el.getAttribute("placeholder") ||
7900
+ el.textContent ||
7901
+ el.getAttribute("value") ||
7902
+ el.tagName
7903
+ ) || "element";
7904
+ }
7905
+
7906
+ function chooseRegion(target) {
7907
+ const preferred = target.closest(
7908
+ "[data-testid], article, [role='article'], [role='listitem'], li, tr, form, section, aside, dialog, [role='dialog']"
7909
+ );
7910
+ if (preferred) return preferred;
7911
+ let current = target.parentElement;
7912
+ let depth = 0;
7913
+ while (current && depth < 5) {
7914
+ const count = current.querySelectorAll("a[href], button, input, select, textarea").length;
7915
+ if (count >= 2 && count <= 16) return current;
7916
+ current = current.parentElement;
7917
+ depth += 1;
7918
+ }
7919
+ return target.parentElement || target;
7920
+ }
7921
+
7922
+ const target = document.querySelector(${JSON.stringify(selector)});
7923
+ if (!target) return { error: "Element not found" };
7924
+ if (target instanceof HTMLElement) {
7925
+ target.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
7926
+ }
7927
+
7928
+ const region = chooseRegion(target);
7929
+ const nearby = [];
7930
+ const seen = new Set();
7931
+ region.querySelectorAll("a[href], button, input:not([type='hidden']), select, textarea").forEach((el) => {
7932
+ if (!(el instanceof HTMLElement) || !isVisible(el)) return;
7933
+ const candidateSelector = selectorFor(el);
7934
+ if (!candidateSelector || seen.has(candidateSelector)) return;
7935
+ seen.add(candidateSelector);
7936
+ nearby.push({
7937
+ label: labelFor(el).slice(0, 100),
7938
+ type: el.tagName.toLowerCase(),
7939
+ selector: candidateSelector,
7940
+ href: el instanceof HTMLAnchorElement ? text(el.href) : undefined,
7941
+ });
7942
+ });
7943
+
7944
+ return {
7945
+ target: {
7946
+ label: labelFor(target).slice(0, 120),
7947
+ tag: target.tagName.toLowerCase(),
7948
+ text: text(target.textContent)?.slice(0, 240),
7949
+ href: target instanceof HTMLAnchorElement ? text(target.href) : undefined,
7950
+ value: target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement
7951
+ ? text(target.value)?.slice(0, 120)
7952
+ : undefined,
7953
+ },
7954
+ region: {
7955
+ tag: region.tagName.toLowerCase(),
7956
+ label: labelFor(region).slice(0, 120),
7957
+ text: text(region.textContent)?.slice(0, 400),
7958
+ },
7959
+ nearby: nearby.slice(0, ${Math.max(1, Math.min(20, limit))}),
7960
+ };
7961
+ })()
7962
+ `,
7963
+ {
7964
+ timeoutMs: 2e3,
7965
+ label: "inspect element"
7966
+ }
7967
+ );
7968
+ if (result === PAGE_SCRIPT_TIMEOUT) {
7969
+ return pageBusyError("inspect_element");
7970
+ }
7971
+ if (!result || typeof result !== "object") {
7972
+ return "Error: Could not inspect element";
7973
+ }
7974
+ if ("error" in result && typeof result.error === "string") {
7975
+ return `Error: ${result.error}`;
7976
+ }
7977
+ const lines = [];
7978
+ if (result.target) {
7979
+ lines.push(`Target: ${result.target.label} <${result.target.tag}>`);
7980
+ if (result.target.text) lines.push(`Target text: ${result.target.text}`);
7981
+ if (result.target.href) lines.push(`Target href: ${result.target.href}`);
7982
+ if (result.target.value) lines.push(`Target value: ${result.target.value}`);
7983
+ }
7984
+ if (result.region) {
7985
+ lines.push(`Region: ${result.region.label} <${result.region.tag}>`);
7986
+ if (result.region.text) lines.push(`Region text: ${result.region.text}`);
7987
+ }
7988
+ if (Array.isArray(result.nearby) && result.nearby.length > 0) {
7989
+ lines.push("Nearby controls:");
7990
+ for (const item of result.nearby) {
7991
+ const hrefSuffix = item.href ? ` -> ${item.href}` : "";
7992
+ lines.push(
7993
+ `- ${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
7994
+ );
7995
+ }
7996
+ }
7997
+ return lines.join("\n");
7998
+ }
7999
+ async function getLocaleSnapshot(wc) {
8000
+ const snapshot = await executePageScript(
8001
+ wc,
8002
+ `
8003
+ (function() {
8004
+ return {
8005
+ lang:
8006
+ document.documentElement?.lang ||
8007
+ document.body?.lang ||
8008
+ navigator.language ||
8009
+ "",
8010
+ url: window.location.href || "",
8011
+ title: document.title || "",
8012
+ };
8013
+ })()
8014
+ `,
8015
+ {
8016
+ label: "locale snapshot"
8017
+ }
8018
+ );
8019
+ if (!snapshot || snapshot === PAGE_SCRIPT_TIMEOUT || typeof snapshot !== "object") {
8020
+ return null;
8021
+ }
8022
+ return {
8023
+ lang: typeof snapshot.lang === "string" ? snapshot.lang.trim() : "",
8024
+ url: typeof snapshot.url === "string" ? snapshot.url : wc.getURL(),
8025
+ title: typeof snapshot.title === "string" ? snapshot.title : wc.getTitle()
8026
+ };
8027
+ }
8028
+ function primaryLanguageTag(value) {
8029
+ return value.trim().toLowerCase().split(/[-_]/)[0] || "";
8030
+ }
8031
+ function localeChanged(before, after) {
8032
+ if (!before || !after) return false;
8033
+ const beforeLang = primaryLanguageTag(before.lang);
8034
+ const afterLang = primaryLanguageTag(after.lang);
8035
+ if (beforeLang && afterLang && beforeLang !== afterLang) {
8036
+ return true;
8037
+ }
8038
+ const localeHint = /[?&](lang|locale|language|hl)=|\/(ja|jp|en|fr|de|es|it|ko|zh)(\/|$)/i;
8039
+ return before.url !== after.url && localeHint.test(after.url);
8040
+ }
8041
+ async function restoreLocaleSnapshot(wc, snapshot) {
8042
+ if (!snapshot || wc.isDestroyed()) return;
8043
+ try {
8044
+ if (typeof wc.canGoBack === "function" && wc.canGoBack()) {
8045
+ wc.goBack();
8046
+ await waitForLoad$1(wc, 3e3);
8047
+ const reverted = await getLocaleSnapshot(wc);
8048
+ if (!localeChanged(snapshot, reverted)) {
8049
+ return;
8050
+ }
8051
+ }
8052
+ } catch {
8053
+ }
8054
+ if (snapshot.url && snapshot.url !== wc.getURL()) {
8055
+ try {
8056
+ await wc.loadURL(snapshot.url);
8057
+ await waitForLoad$1(wc, 3e3);
8058
+ return;
8059
+ } catch {
8060
+ }
8061
+ }
8062
+ if (snapshot.url) {
8063
+ try {
8064
+ await wc.reload();
8065
+ await waitForLoad$1(wc, 3e3);
8066
+ } catch {
8067
+ }
8068
+ }
8069
+ }
8070
+ const ADD_TO_CART_PATTERNS = [
8071
+ "add to cart",
8072
+ "add to bag",
8073
+ "add to basket",
8074
+ "add to my cart",
8075
+ "add to my bag",
8076
+ "add to my basket",
8077
+ "add item to cart",
8078
+ "add item to bag",
8079
+ "add item to basket"
8080
+ ];
8081
+ const recentCartClicks = /* @__PURE__ */ new Map();
8082
+ const CART_CLICK_COOLDOWN_MS = 15e3;
8083
+ function isAddToCartText(text) {
8084
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
8085
+ return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
8086
+ }
8087
+ function recordCartClick(url, text) {
8088
+ recentCartClicks.set(url, { text, ts: Date.now() });
8089
+ for (const [key, entry] of recentCartClicks) {
8090
+ if (Date.now() - entry.ts > CART_CLICK_COOLDOWN_MS) {
8091
+ recentCartClicks.delete(key);
8092
+ }
8093
+ }
8094
+ }
8095
+ function isDuplicateCartClick(url, text) {
8096
+ const recent = recentCartClicks.get(url);
8097
+ if (!recent) return false;
8098
+ if (Date.now() - recent.ts > CART_CLICK_COOLDOWN_MS) {
8099
+ recentCartClicks.delete(url);
8100
+ return false;
8101
+ }
8102
+ return isAddToCartText(text);
8103
+ }
7323
8104
  async function clickResolvedSelector$1(wc, selector) {
7324
8105
  if (selector.startsWith("__vessel_idx:")) {
7325
8106
  const idx = Number(selector.slice("__vessel_idx:".length));
7326
8107
  const beforeUrl2 = wc.getURL();
8108
+ const idxLabel = await executePageScript(
8109
+ wc,
8110
+ `window.__vessel?.getElementText?.(${idx}) || ""`,
8111
+ { label: "shadow element text" }
8112
+ );
8113
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
8114
+ 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).`;
8115
+ }
7327
8116
  const result = await executePageScript(
7328
8117
  wc,
7329
8118
  `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
@@ -7333,12 +8122,29 @@ async function clickResolvedSelector$1(wc, selector) {
7333
8122
  );
7334
8123
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
7335
8124
  if (typeof result === "string" && result.startsWith("Error")) return result;
8125
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
8126
+ recordCartClick(beforeUrl2, idxLabel);
8127
+ }
7336
8128
  await waitForPotentialNavigation$1(wc, beforeUrl2);
7337
8129
  const afterUrl2 = wc.getURL();
7338
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
8130
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
8131
+ const idxOverlay = await detectPostClickOverlay$1(wc);
8132
+ return idxOverlay ? `${result}
8133
+ ${idxOverlay}` : result;
7339
8134
  }
7340
8135
  if (selector.includes(" >>> ")) {
7341
8136
  const beforeUrl2 = wc.getURL();
8137
+ const shadowLabel = await executePageScript(
8138
+ wc,
8139
+ `(function() {
8140
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
8141
+ return el ? (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || "") : "";
8142
+ })()`,
8143
+ { label: "shadow element text" }
8144
+ );
8145
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
8146
+ 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).`;
8147
+ }
7342
8148
  const result = await executePageScript(
7343
8149
  wc,
7344
8150
  `
@@ -7355,19 +8161,48 @@ async function clickResolvedSelector$1(wc, selector) {
7355
8161
  );
7356
8162
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
7357
8163
  if (typeof result === "string" && result.startsWith("Error")) return result;
8164
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
8165
+ recordCartClick(beforeUrl2, shadowLabel);
8166
+ }
7358
8167
  await waitForPotentialNavigation$1(wc, beforeUrl2);
7359
8168
  const afterUrl2 = wc.getURL();
7360
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
8169
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
8170
+ const shadowOverlay = await detectPostClickOverlay$1(wc);
8171
+ return shadowOverlay ? `${result}
8172
+ ${shadowOverlay}` : result;
7361
8173
  }
7362
8174
  const beforeUrl = wc.getURL();
7363
8175
  const elInfo = await describeElementForClick$1(wc, selector);
7364
8176
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
8177
+ const cartMatch = isAddToCartText(elInfo.text);
8178
+ console.log(
8179
+ `[Vessel cart-guard] text="${elInfo.text}" cartMatch=${cartMatch} url=${beforeUrl} hasPrior=${recentCartClicks.has(beforeUrl)}`
8180
+ );
8181
+ if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
8182
+ console.log(`[Vessel cart-guard] BLOCKED duplicate add-to-cart click`);
8183
+ 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).`;
8184
+ }
8185
+ if (!cartMatch && recentCartClicks.has(beforeUrl)) {
8186
+ const dialogActions = await getCartDialogActions$1(wc);
8187
+ if (dialogActions) {
8188
+ console.log(
8189
+ `[Vessel cart-guard] BLOCKED background click while cart dialog is open`
8190
+ );
8191
+ return `Blocked: a cart confirmation dialog is open. Do not click background elements.
8192
+ ${dialogActions}
8193
+ Click one of these dialog actions instead.`;
8194
+ }
8195
+ }
7365
8196
  if (elInfo.href) {
7366
8197
  const validation = await validateLinkDestination(elInfo.href);
7367
8198
  if (validation.status === "dead") {
7368
8199
  return formatDeadLinkMessage(elInfo.text, validation);
7369
8200
  }
7370
8201
  }
8202
+ if (cartMatch) {
8203
+ console.log(`[Vessel cart-guard] RECORDED cart click for url=${beforeUrl}`);
8204
+ recordCartClick(beforeUrl, elInfo.text);
8205
+ }
7371
8206
  const clickText = `Clicked: ${elInfo.text}`;
7372
8207
  const clickResult = await clickElement$1(wc, selector);
7373
8208
  if (clickResult.startsWith("Error:")) return clickResult;
@@ -7376,6 +8211,18 @@ async function clickResolvedSelector$1(wc, selector) {
7376
8211
  if (afterUrl !== beforeUrl) {
7377
8212
  return `${clickText} -> ${afterUrl}`;
7378
8213
  }
8214
+ const overlayHint = await detectPostClickOverlay$1(wc);
8215
+ if (overlayHint) {
8216
+ const dialogActions = cartMatch ? await getCartDialogActions$1(wc) : null;
8217
+ const actionsSuffix = dialogActions ? `
8218
+ ${dialogActions}
8219
+ Click one of these dialog actions. Do NOT click any other element.` : "";
8220
+ return `${clickText} (${clickResult})
8221
+ ${overlayHint}${actionsSuffix}`;
8222
+ }
8223
+ if (cartMatch) {
8224
+ return `${clickText} (${clickResult})`;
8225
+ }
7379
8226
  const activationResult = await activateElement$1(wc, selector);
7380
8227
  if (!activationResult.startsWith("Error:")) {
7381
8228
  await waitForPotentialNavigation$1(wc, beforeUrl);
@@ -7384,14 +8231,226 @@ async function clickResolvedSelector$1(wc, selector) {
7384
8231
  return `${clickText} -> ${fallbackUrl} (recovered via DOM activation)`;
7385
8232
  }
7386
8233
  }
8234
+ const postActivationOverlayHint = await detectPostClickOverlay$1(wc);
8235
+ if (postActivationOverlayHint) {
8236
+ return `${clickText} (${clickResult})
8237
+ ${postActivationOverlayHint}`;
8238
+ }
7387
8239
  return `${clickText} (${clickResult})`;
7388
8240
  }
8241
+ async function getCartDialogActions$1(wc) {
8242
+ const result = await executePageScript(
8243
+ wc,
8244
+ `
8245
+ (function() {
8246
+ var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
8247
+ if (!dialog) return { found: false, actions: [] };
8248
+ var cs = getComputedStyle(dialog);
8249
+ if (cs.display === 'none' || cs.visibility === 'hidden') return { found: false, actions: [] };
8250
+ var text = (dialog.textContent || '').slice(0, 500).toLowerCase();
8251
+ var cartSignals = ['added to cart','added to bag','added to basket',
8252
+ 'item added','your basket','your cart','your bag',
8253
+ 'view basket','view cart','continue shopping'];
8254
+ var isCart = cartSignals.some(function(s) { return text.indexOf(s) !== -1; });
8255
+ if (!isCart) return { found: false, actions: [] };
8256
+ var actions = [];
8257
+ dialog.querySelectorAll('button, a[href], [role="button"]').forEach(function(el) {
8258
+ var cs2 = getComputedStyle(el);
8259
+ if (cs2.display === 'none' || cs2.visibility === 'hidden') return;
8260
+ var r = el.getBoundingClientRect();
8261
+ if (r.width < 20 || r.height < 10) return;
8262
+ var label = (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 80);
8263
+ if (!label || label.length < 2) return;
8264
+ var href = el.getAttribute('href') || '';
8265
+ var sel = el.id ? '#' + el.id
8266
+ : el.getAttribute('data-test') ? '[data-test="' + el.getAttribute('data-test') + '"]'
8267
+ : el.getAttribute('aria-label') ? '[aria-label="' + el.getAttribute('aria-label') + '"]'
8268
+ : null;
8269
+ if (sel) actions.push({ label: label, selector: sel, href: href });
8270
+ });
8271
+ return {
8272
+ found: true,
8273
+ actions: actions.map(function(a) {
8274
+ return '- "' + a.label + '"' + (a.href ? ' → ' + a.href : '') + (a.selector ? ' (selector: ' + a.selector + ')' : '');
8275
+ }),
8276
+ };
8277
+ })()
8278
+ `,
8279
+ { timeoutMs: 800, label: "get cart dialog actions" }
8280
+ );
8281
+ if (!result || result === PAGE_SCRIPT_TIMEOUT || !result.found) return null;
8282
+ if (result.actions.length === 0) return null;
8283
+ return `Available dialog actions:
8284
+ ${result.actions.join("\n")}`;
8285
+ }
8286
+ async function detectPostClickOverlay$1(wc) {
8287
+ const result = await executePageScript(
8288
+ wc,
8289
+ `
8290
+ (function() {
8291
+ var vw = window.innerWidth || document.documentElement.clientWidth;
8292
+ var vh = window.innerHeight || document.documentElement.clientHeight;
8293
+ var vpArea = Math.max(1, vw * vh);
8294
+
8295
+ function isVis(el) {
8296
+ var cs = getComputedStyle(el);
8297
+ return cs.display !== 'none' && cs.visibility !== 'hidden' &&
8298
+ el.getBoundingClientRect().width > 0;
8299
+ }
8300
+
8301
+ function hasFixedAncestor(el) {
8302
+ var cur = el.parentElement;
8303
+ while (cur && cur !== document.body) {
8304
+ var ps = getComputedStyle(cur).position;
8305
+ if (ps === 'fixed' || ps === 'sticky') return true;
8306
+ cur = cur.parentElement;
8307
+ }
8308
+ return false;
8309
+ }
8310
+
8311
+ function effectiveZ(el) {
8312
+ var cur = el;
8313
+ while (cur && cur !== document.body) {
8314
+ var z = parseInt(getComputedStyle(cur).zIndex, 10);
8315
+ if (z > 0) return z;
8316
+ cur = cur.parentElement;
8317
+ }
8318
+ return 0;
8319
+ }
8320
+
8321
+ function edgePad(r) {
8322
+ return r.left <= 24 || r.top <= 24 ||
8323
+ r.right >= vw - 24 || r.bottom >= vh - 24;
8324
+ }
8325
+
8326
+ var cartPhrases = ['added to cart','added to bag','added to basket',
8327
+ 'added to your cart','added to your bag','added to your basket'];
8328
+ var cartActions = ['view cart','go to cart','continue shopping',
8329
+ 'keep shopping','checkout','view basket','go to basket'];
8330
+
8331
+ // Phase 1: semantic dialog elements
8332
+ var selectors = 'dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"]';
8333
+ var candidates = document.querySelectorAll(selectors);
8334
+ var hit = null;
8335
+ for (var j = 0; j < candidates.length; j++) {
8336
+ if (isVis(candidates[j])) { hit = candidates[j]; break; }
8337
+ }
8338
+
8339
+ // Phase 2: positioned drawer-like elements
8340
+ if (!hit) {
8341
+ var els = document.querySelectorAll('*');
8342
+ for (var i = 0; i < els.length; i++) {
8343
+ var s = getComputedStyle(els[i]);
8344
+ if (s.display === 'none' || s.visibility === 'hidden') continue;
8345
+ var pos = s.position;
8346
+ var isFixed = pos === 'fixed' || pos === 'sticky';
8347
+ var isAbs = pos === 'absolute';
8348
+ if (!isFixed && !isAbs) continue;
8349
+ if (isAbs && !hasFixedAncestor(els[i])) continue;
8350
+ if (effectiveZ(els[i]) < 5) continue;
8351
+ var r = els[i].getBoundingClientRect();
8352
+ var area = (r.width * r.height) / vpArea;
8353
+ if (r.width >= 160 && r.height >= 100 && area >= 0.05 && edgePad(r)) {
8354
+ hit = els[i]; break;
8355
+ }
8356
+ }
8357
+ }
8358
+
8359
+ // Phase 3: text-based fallback — any positioned element with cart confirmation text
8360
+ if (!hit) {
8361
+ var els2 = document.querySelectorAll('*');
8362
+ for (var k = 0; k < els2.length; k++) {
8363
+ var s2 = getComputedStyle(els2[k]);
8364
+ if (s2.display === 'none' || s2.visibility === 'hidden') continue;
8365
+ var p2 = s2.position;
8366
+ if (p2 !== 'fixed' && p2 !== 'sticky' && p2 !== 'absolute') continue;
8367
+ var r2 = els2[k].getBoundingClientRect();
8368
+ if (r2.width < 120 || r2.height < 80) continue;
8369
+ var innerText = (els2[k].textContent || '').slice(0, 500).toLowerCase();
8370
+ var hasConfirm = cartPhrases.some(function(ph) { return innerText.indexOf(ph) !== -1; });
8371
+ if (hasConfirm) { hit = els2[k]; break; }
8372
+ }
8373
+ }
8374
+
8375
+ if (!hit) return { found: false, label: '', cartLike: false };
8376
+ var text = (hit.textContent || '').slice(0, 500).toLowerCase();
8377
+ var cartLike = cartPhrases.concat(cartActions).some(function(s) { return text.indexOf(s) !== -1; });
8378
+ var label = (hit.getAttribute('aria-label') || (hit.querySelector('h1,h2,h3,h4') || {}).textContent || '').trim().slice(0, 80);
8379
+ return { found: true, label: label, cartLike: cartLike };
8380
+ })()
8381
+ `,
8382
+ { timeoutMs: 800, label: "post-click overlay check" }
8383
+ );
8384
+ if (!result || result === PAGE_SCRIPT_TIMEOUT || !result.found) return null;
8385
+ if (result.cartLike) {
8386
+ const desc2 = result.label ? ` ("${result.label}")` : "";
8387
+ return `A cart confirmation dialog appeared${desc2}. Call read_page to see available actions — do not click Add to Cart again.`;
8388
+ }
8389
+ const desc = result.label ? ` ("${result.label}")` : "";
8390
+ return `A dialog or overlay appeared${desc}. Call read_page to see available actions.`;
8391
+ }
7389
8392
  async function dismissPopup$1(wc) {
7390
8393
  const before = await extractContent(wc);
7391
8394
  const initialBlocking = before.overlays.filter(
7392
8395
  (overlay) => overlay.blocksInteraction
7393
8396
  ).length;
8397
+ if (initialBlocking > 0) {
8398
+ const overlayText = before.overlays.map((o) => [o.label, o.text].filter(Boolean).join(" ")).join(" ").toLowerCase();
8399
+ const cartSignals = [
8400
+ "added to cart",
8401
+ "added to bag",
8402
+ "added to basket",
8403
+ "item added",
8404
+ "items in your basket",
8405
+ "items in your cart",
8406
+ "items in your bag",
8407
+ "your basket",
8408
+ "your cart",
8409
+ "your bag",
8410
+ "view basket",
8411
+ "view cart",
8412
+ "continue shopping"
8413
+ ];
8414
+ if (cartSignals.some((s) => overlayText.includes(s))) {
8415
+ const continueResult = await executePageScript(
8416
+ wc,
8417
+ `
8418
+ (function() {
8419
+ var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
8420
+ if (!dialog) return "Error: dialog not found";
8421
+ var buttons = dialog.querySelectorAll('button, a[href], [role="button"]');
8422
+ var continueBtn = null;
8423
+ var viewCartBtn = null;
8424
+ for (var i = 0; i < buttons.length; i++) {
8425
+ var label = (buttons[i].getAttribute('aria-label') || buttons[i].textContent || '').trim().toLowerCase();
8426
+ if (/continue shopping|keep shopping/.test(label)) { continueBtn = buttons[i]; break; }
8427
+ if (/view (basket|cart|bag)|checkout/.test(label) && !viewCartBtn) { viewCartBtn = buttons[i]; }
8428
+ }
8429
+ var target = continueBtn || viewCartBtn;
8430
+ if (!target) return "Error: no dialog action found";
8431
+ var actionLabel = (target.getAttribute('aria-label') || target.textContent || '').trim();
8432
+ if (target.tagName === 'A' && target.href) {
8433
+ window.location.href = target.href;
8434
+ return "Clicked: " + actionLabel + " -> " + target.href;
8435
+ }
8436
+ target.click();
8437
+ return "Clicked: " + actionLabel;
8438
+ })()
8439
+ `,
8440
+ { timeoutMs: 1500, label: "cart dialog continue shopping" }
8441
+ );
8442
+ if (continueResult && continueResult !== PAGE_SCRIPT_TIMEOUT && typeof continueResult === "string" && !continueResult.startsWith("Error")) {
8443
+ console.log(
8444
+ `[Vessel cart-guard] dismiss_popup auto-clicked dialog action: ${continueResult}`
8445
+ );
8446
+ return `Cart confirmation handled: ${continueResult}. Item was already added to your cart.`;
8447
+ }
8448
+ const dialogActions = await getCartDialogActions$1(wc);
8449
+ 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."}`;
8450
+ }
8451
+ }
7394
8452
  const initialDormant = before.dormantOverlays.length;
8453
+ const initialLocale = await getLocaleSnapshot(wc);
7395
8454
  const candidates = await executePageScript(
7396
8455
  wc,
7397
8456
  `
@@ -7503,7 +8562,8 @@ async function dismissPopup$1(wc) {
7503
8562
  ).toLowerCase();
7504
8563
  const classText = text(typeof el.className === "string" ? el.className : "").toLowerCase();
7505
8564
  const idText = text(el.id).toLowerCase();
7506
- const combined = classText + " " + idText;
8565
+ const hrefText = text(el.getAttribute && el.getAttribute("href")).toLowerCase();
8566
+ const combined = label + " " + classText + " " + idText + " " + hrefText;
7507
8567
  let score = rooted ? 30 : 0;
7508
8568
  if (/^x$|^×$/.test(label)) score += 120;
7509
8569
  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 +8573,11 @@ async function dismissPopup$1(wc) {
7513
8573
  // OneTrust "Accept" is valid for dismissing the banner (user just wants it gone)
7514
8574
  if (/onetrust-accept|cookie.*accept|consent.*accept/.test(combined)) score += 80;
7515
8575
  if (el.getAttribute("aria-label")) score += 20;
8576
+ if (/(language|locale|region|country|currency)\b/.test(combined)) score -= 320;
8577
+ if (/\b(english|japanese|japan|francais|espanol|deutsch|italiano|portuguese|nihongo)\b/.test(label)) score -= 280;
8578
+ if (/日本語|中文|한국어/.test(label)) score -= 280;
8579
+ if (/[?&](lang|locale|language|hl)=/.test(hrefText)) score -= 260;
8580
+ if (//(ja|jp|en|fr|de|es|it|ko|zh)(/|$)/.test(hrefText)) score -= 220;
7516
8581
  // Penalize general accept/subscribe buttons that aren't consent-related
7517
8582
  if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label) && !/cookie|consent|onetrust/.test(combined)) score -= 80;
7518
8583
  const rect = el.getBoundingClientRect();
@@ -7590,6 +8655,11 @@ async function dismissPopup$1(wc) {
7590
8655
  const result = await clickElement$1(wc, candidate.selector);
7591
8656
  if (result.startsWith("Error:")) continue;
7592
8657
  await sleep$1(250);
8658
+ const postClickLocale = await getLocaleSnapshot(wc);
8659
+ if (localeChanged(initialLocale, postClickLocale)) {
8660
+ await restoreLocaleSnapshot(wc, initialLocale);
8661
+ continue;
8662
+ }
7593
8663
  const after = await extractContent(wc);
7594
8664
  const blocking = after.overlays.filter(
7595
8665
  (overlay) => overlay.blocksInteraction
@@ -7722,10 +8792,201 @@ async function resolveSelector$1(wc, index, selector) {
7722
8792
  if (typeof fallbackSelector === "string" && fallbackSelector) {
7723
8793
  return fallbackSelector;
7724
8794
  }
7725
- const page = await extractContent(wc);
7726
- const extractedSelector = findSelectorByIndex(page, index);
7727
- if (extractedSelector) return extractedSelector;
7728
- return null;
8795
+ const page = await extractContent(wc);
8796
+ const extractedSelector = findSelectorByIndex(page, index);
8797
+ if (extractedSelector) return extractedSelector;
8798
+ return null;
8799
+ }
8800
+ function normalizeFieldToken(value) {
8801
+ return typeof value === "string" ? value.trim() : "";
8802
+ }
8803
+ function describeFillField(field) {
8804
+ if (field.selector) return `selector=${field.selector}`;
8805
+ if (field.index != null) return `index=${field.index}`;
8806
+ if (field.name) return `name=${field.name}`;
8807
+ if (field.label) return `label=${field.label}`;
8808
+ if (field.placeholder) return `placeholder=${field.placeholder}`;
8809
+ return "field";
8810
+ }
8811
+ async function resolveFieldSelector(wc, field) {
8812
+ const directSelector = await resolveSelector$1(wc, field.index, field.selector);
8813
+ if (directSelector) return directSelector;
8814
+ const name = normalizeFieldToken(field.name);
8815
+ const label = normalizeFieldToken(field.label);
8816
+ const placeholder = normalizeFieldToken(field.placeholder);
8817
+ if (!name && !label && !placeholder) return null;
8818
+ const selector = await executePageScript(
8819
+ wc,
8820
+ `
8821
+ (function() {
8822
+ function normalize(value) {
8823
+ return value == null ? "" : String(value).trim().toLowerCase();
8824
+ }
8825
+
8826
+ function text(value) {
8827
+ return value == null ? "" : String(value).trim();
8828
+ }
8829
+
8830
+ function isVisible(el) {
8831
+ if (!(el instanceof HTMLElement)) return true;
8832
+ const style = window.getComputedStyle(el);
8833
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
8834
+ return false;
8835
+ }
8836
+ if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") {
8837
+ return false;
8838
+ }
8839
+ const rect = el.getBoundingClientRect();
8840
+ return rect.width > 0 && rect.height > 0;
8841
+ }
8842
+
8843
+ function escapeSelectorValue(value) {
8844
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
8845
+ return CSS.escape(value);
8846
+ }
8847
+ return String(value).replace(/["\\\\]/g, "\\\\$&");
8848
+ }
8849
+
8850
+ function uniqueSelector(candidate) {
8851
+ if (!candidate) return null;
8852
+ try {
8853
+ return document.querySelectorAll(candidate).length === 1 ? candidate : null;
8854
+ } catch {
8855
+ return null;
8856
+ }
8857
+ }
8858
+
8859
+ function uniqueAttributeSelector(el, attribute) {
8860
+ const value = text(el.getAttribute && el.getAttribute(attribute));
8861
+ if (!value) return null;
8862
+ const candidate = el.tagName.toLowerCase() + "[" + attribute + "=\\"" + escapeSelectorValue(value) + "\\"]";
8863
+ return uniqueSelector(candidate);
8864
+ }
8865
+
8866
+ function selectorFor(el) {
8867
+ if (!el) return null;
8868
+ if (el.id) return "#" + escapeSelectorValue(el.id);
8869
+ const attributes = ["data-testid", "name", "form", "aria-label", "placeholder"];
8870
+ for (const attribute of attributes) {
8871
+ const attributeCandidate = uniqueAttributeSelector(el, attribute);
8872
+ if (attributeCandidate) return attributeCandidate;
8873
+ }
8874
+ const parts = [];
8875
+ let current = el;
8876
+ while (current) {
8877
+ if (current.id) {
8878
+ parts.unshift("#" + escapeSelectorValue(current.id));
8879
+ break;
8880
+ }
8881
+ const tag = current.tagName.toLowerCase();
8882
+ const parent = current.parentElement;
8883
+ if (!parent) {
8884
+ parts.unshift(tag);
8885
+ break;
8886
+ }
8887
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
8888
+ const index = siblings.indexOf(current) + 1;
8889
+ parts.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
8890
+ current = parent;
8891
+ }
8892
+ const candidate = parts.join(" > ");
8893
+ return uniqueSelector(candidate) || candidate;
8894
+ }
8895
+
8896
+ function getLabelText(el) {
8897
+ const parts = [];
8898
+ if (el.labels) {
8899
+ Array.from(el.labels).forEach((labelEl) => {
8900
+ const value = text(labelEl.textContent);
8901
+ if (value) parts.push(value);
8902
+ });
8903
+ }
8904
+ const ariaLabel = text(el.getAttribute && el.getAttribute("aria-label"));
8905
+ if (ariaLabel) parts.push(ariaLabel);
8906
+ const labelledBy = text(el.getAttribute && el.getAttribute("aria-labelledby"));
8907
+ if (labelledBy) {
8908
+ labelledBy.split(/\\s+/).forEach((id) => {
8909
+ const ref = document.getElementById(id);
8910
+ const value = text(ref && ref.textContent);
8911
+ if (value) parts.push(value);
8912
+ });
8913
+ }
8914
+ return normalize(parts.join(" "));
8915
+ }
8916
+
8917
+ function scoreField(el) {
8918
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
8919
+ return -1;
8920
+ }
8921
+ if (!isVisible(el) || el.disabled || el.getAttribute("aria-disabled") === "true") {
8922
+ return -1;
8923
+ }
8924
+
8925
+ const normalizedName = normalize(el.getAttribute("name")) || normalize(el.id);
8926
+ const normalizedLabel = getLabelText(el);
8927
+ const normalizedPlaceholder = normalize(el.getAttribute("placeholder"));
8928
+ let score = 0;
8929
+
8930
+ if (${JSON.stringify(name)}) {
8931
+ if (normalizedName === ${JSON.stringify(name.toLowerCase())}) score += 120;
8932
+ else if (normalizedName.includes(${JSON.stringify(name.toLowerCase())})) score += 70;
8933
+ }
8934
+
8935
+ if (${JSON.stringify(label)}) {
8936
+ if (normalizedLabel === ${JSON.stringify(label.toLowerCase())}) score += 110;
8937
+ else if (normalizedLabel.includes(${JSON.stringify(label.toLowerCase())})) score += 65;
8938
+ }
8939
+
8940
+ if (${JSON.stringify(placeholder)}) {
8941
+ if (normalizedPlaceholder === ${JSON.stringify(placeholder.toLowerCase())}) score += 105;
8942
+ else if (normalizedPlaceholder.includes(${JSON.stringify(placeholder.toLowerCase())})) score += 60;
8943
+ }
8944
+
8945
+ if (score === 0) return -1;
8946
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) score += 5;
8947
+ return score;
8948
+ }
8949
+
8950
+ const candidates = Array.from(document.querySelectorAll("input, textarea, select"));
8951
+ let best = null;
8952
+ let bestScore = -1;
8953
+ for (const el of candidates) {
8954
+ const score = scoreField(el);
8955
+ if (score > bestScore) {
8956
+ best = el;
8957
+ bestScore = score;
8958
+ }
8959
+ }
8960
+
8961
+ return best ? selectorFor(best) : null;
8962
+ })()
8963
+ `,
8964
+ {
8965
+ label: "resolve form field"
8966
+ }
8967
+ );
8968
+ return typeof selector === "string" && selector ? selector : null;
8969
+ }
8970
+ async function fillFormFields(wc, fields) {
8971
+ const results = [];
8972
+ for (const field of fields) {
8973
+ const selector = await resolveFieldSelector(wc, field);
8974
+ if (!selector) {
8975
+ results.push({
8976
+ field,
8977
+ selector: null,
8978
+ result: `Skipped: no selector for ${describeFillField(field)}`
8979
+ });
8980
+ continue;
8981
+ }
8982
+ const result = await setElementValue$1(
8983
+ wc,
8984
+ selector,
8985
+ String(field.value || "")
8986
+ );
8987
+ results.push({ field, selector, result });
8988
+ }
8989
+ return results;
7729
8990
  }
7730
8991
  function getTabByMatch$1(tabManager, match) {
7731
8992
  if (!match) return null;
@@ -8327,7 +9588,8 @@ async function getPostActionState$1(ctx, name) {
8327
9588
  "select_option",
8328
9589
  "hover",
8329
9590
  "focus",
8330
- "fill_form"
9591
+ "fill_form",
9592
+ "inspect_element"
8331
9593
  ];
8332
9594
  const tabActions = [
8333
9595
  "create_tab",
@@ -8367,6 +9629,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
8367
9629
  "go_forward",
8368
9630
  "reload",
8369
9631
  "click",
9632
+ "inspect_element",
8370
9633
  "type_text",
8371
9634
  "select_option",
8372
9635
  "submit_form",
@@ -8508,6 +9771,10 @@ async function executeAction(name, args, ctx) {
8508
9771
  }
8509
9772
  case "navigate": {
8510
9773
  if (!wc || !tabId) return "Error: No active tab";
9774
+ const navValidation = await validateLinkDestination(args.url);
9775
+ if (navValidation.status === "dead") {
9776
+ return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
9777
+ }
8511
9778
  ctx.tabManager.navigateTab(tabId, args.url);
8512
9779
  await waitForLoad$1(wc);
8513
9780
  return `Navigated to ${wc.getURL()}${getPostNavSummary(wc)}`;
@@ -8546,6 +9813,16 @@ async function executeAction(name, args, ctx) {
8546
9813
  if (!selector) return "Error: No element index or selector provided";
8547
9814
  return clickResolvedSelector$1(wc, selector);
8548
9815
  }
9816
+ case "inspect_element": {
9817
+ if (!wc) return "Error: No active tab";
9818
+ const selector = await resolveSelector$1(wc, args.index, args.selector);
9819
+ if (!selector) return "Error: No element index or selector provided";
9820
+ return inspectElement(
9821
+ wc,
9822
+ selector,
9823
+ typeof args.limit === "number" ? args.limit : 8
9824
+ );
9825
+ }
8549
9826
  case "type_text": {
8550
9827
  if (!wc) return "Error: No active tab";
8551
9828
  const selector = await resolveSelector$1(wc, args.index, args.selector);
@@ -9108,6 +10385,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
9108
10385
  );
9109
10386
  } else if (hasSearchInput && linkCount >= 10) {
9110
10387
  suggestions.push("SEARCH RESULTS detected:");
10388
+ suggestions.push(
10389
+ " → inspect_element(index) to inspect one result card"
10390
+ );
9111
10391
  suggestions.push(" → click on a result link");
9112
10392
  if (hasPagination)
9113
10393
  suggestions.push(" → paginate('next') for more results");
@@ -9145,26 +10425,10 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
9145
10425
  if (!wc) return "Error: No active tab";
9146
10426
  const fields = Array.isArray(args.fields) ? args.fields : [];
9147
10427
  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
- }
10428
+ const fillResults = await fillFormFields(wc, fields);
10429
+ const results = fillResults.map((item) => item.result);
9162
10430
  if (args.submit) {
9163
- const firstSel = await resolveSelector$1(
9164
- wc,
9165
- fields[0]?.index,
9166
- fields[0]?.selector
9167
- );
10431
+ const firstSel = fillResults.find((item) => item.selector)?.selector ?? null;
9168
10432
  if (firstSel) {
9169
10433
  const beforeUrl = wc.getURL();
9170
10434
  const submitResult = await submitForm$1(wc, { selector: firstSel });
@@ -9264,6 +10528,36 @@ ${steps.join("\n")}`;
9264
10528
  if (!wc) return "Error: No active tab";
9265
10529
  const query = String(args.query || "");
9266
10530
  if (!query) return "Error: No search query provided.";
10531
+ const queryLower = query.toLowerCase().trim();
10532
+ const buttonLikePatterns = [
10533
+ "add to cart",
10534
+ "add to bag",
10535
+ "add to basket",
10536
+ "buy now",
10537
+ "buy it now",
10538
+ "purchase",
10539
+ "continue shopping",
10540
+ "keep shopping",
10541
+ "view cart",
10542
+ "view bag",
10543
+ "view basket",
10544
+ "go to cart",
10545
+ "go to checkout",
10546
+ "checkout",
10547
+ "check out",
10548
+ "proceed to checkout",
10549
+ "place order",
10550
+ "submit",
10551
+ "subscribe",
10552
+ "sign up",
10553
+ "sign in",
10554
+ "log in",
10555
+ "register",
10556
+ "continue"
10557
+ ];
10558
+ if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
10559
+ return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
10560
+ }
9267
10561
  const searchInfo = args.selector ? { selector: args.selector, formAction: null, formMethod: null } : await executePageScript(
9268
10562
  wc,
9269
10563
  `
@@ -9634,6 +10928,7 @@ Instructions:
9634
10928
  - Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.
9635
10929
  - 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
10930
  - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
10931
+ - When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
9637
10932
  - 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
10933
  - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
9639
10934
  - 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").
@@ -9652,7 +10947,11 @@ Instructions:
9652
10947
  - 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
10948
  - NEVER USE EMOJIS unless the user uses them first.`;
9654
10949
  const actionCtx = { tabManager, runtime };
9655
- const contextualTools = pruneToolsForContext(AGENT_TOOLS, pageType);
10950
+ const contextualTools = pruneToolsForContext(
10951
+ AGENT_TOOLS,
10952
+ pageType,
10953
+ query
10954
+ );
9656
10955
  await provider.streamAgentQuery(
9657
10956
  systemPrompt,
9658
10957
  query,
@@ -10777,16 +12076,42 @@ async function clickResolvedSelector(wc, selector) {
10777
12076
  if (selector.startsWith("__vessel_idx:")) {
10778
12077
  const idx = Number(selector.slice("__vessel_idx:".length));
10779
12078
  const beforeUrl2 = wc.getURL();
12079
+ const idxLabel = await wc.executeJavaScript(
12080
+ `window.__vessel?.getElementText?.(${idx}) || ""`
12081
+ );
12082
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
12083
+ 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).`;
12084
+ }
10780
12085
  const result = await wc.executeJavaScript(
10781
12086
  `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`
10782
12087
  );
10783
12088
  if (typeof result === "string" && result.startsWith("Error")) return result;
12089
+ if (typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
12090
+ recordCartClick(beforeUrl2, idxLabel);
12091
+ }
10784
12092
  await waitForPotentialNavigation(wc, beforeUrl2);
10785
12093
  const afterUrl2 = wc.getURL();
10786
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
12094
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
12095
+ const overlayHint2 = await detectPostClickOverlay(wc);
12096
+ if (!overlayHint2) return result;
12097
+ const dialogActions = typeof idxLabel === "string" && isAddToCartText(idxLabel) ? await getCartDialogActions(wc) : null;
12098
+ const actionsSuffix = dialogActions ? `
12099
+ ${dialogActions}
12100
+ Click one of these dialog actions. Do NOT click any other element.` : "";
12101
+ return `${result}
12102
+ ${overlayHint2}${actionsSuffix}`;
10787
12103
  }
10788
12104
  if (selector.includes(" >>> ")) {
10789
12105
  const beforeUrl2 = wc.getURL();
12106
+ const shadowLabel = await wc.executeJavaScript(`
12107
+ (function() {
12108
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
12109
+ return el ? (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || "") : "";
12110
+ })()
12111
+ `);
12112
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
12113
+ 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).`;
12114
+ }
10790
12115
  const result = await wc.executeJavaScript(`
10791
12116
  (function() {
10792
12117
  var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
@@ -10796,19 +12121,45 @@ async function clickResolvedSelector(wc, selector) {
10796
12121
  })()
10797
12122
  `);
10798
12123
  if (typeof result === "string" && result.startsWith("Error")) return result;
12124
+ if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
12125
+ recordCartClick(beforeUrl2, shadowLabel);
12126
+ }
10799
12127
  await waitForPotentialNavigation(wc, beforeUrl2);
10800
12128
  const afterUrl2 = wc.getURL();
10801
- return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
12129
+ if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
12130
+ const overlayHint2 = await detectPostClickOverlay(wc);
12131
+ if (!overlayHint2) return result;
12132
+ const dialogActions = typeof shadowLabel === "string" && isAddToCartText(shadowLabel) ? await getCartDialogActions(wc) : null;
12133
+ const actionsSuffix = dialogActions ? `
12134
+ ${dialogActions}
12135
+ Click one of these dialog actions. Do NOT click any other element.` : "";
12136
+ return `${result}
12137
+ ${overlayHint2}${actionsSuffix}`;
10802
12138
  }
10803
12139
  const beforeUrl = wc.getURL();
10804
12140
  const elInfo = await describeElementForClick(wc, selector);
10805
12141
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
12142
+ const cartMatch = isAddToCartText(elInfo.text);
12143
+ if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
12144
+ 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).`;
12145
+ }
12146
+ if (!cartMatch) {
12147
+ const dialogActions = await getCartDialogActions(wc);
12148
+ if (dialogActions) {
12149
+ return `Blocked: a cart confirmation dialog is open. Do not click background elements.
12150
+ ${dialogActions}
12151
+ Click one of these dialog actions instead.`;
12152
+ }
12153
+ }
10806
12154
  if (elInfo.href) {
10807
12155
  const validation = await validateLinkDestination(elInfo.href);
10808
12156
  if (validation.status === "dead") {
10809
12157
  return formatDeadLinkMessage(elInfo.text, validation);
10810
12158
  }
10811
12159
  }
12160
+ if (cartMatch) {
12161
+ recordCartClick(beforeUrl, elInfo.text);
12162
+ }
10812
12163
  const clickText = `Clicked: ${elInfo.text}`;
10813
12164
  const clickResult = await clickElement(wc, selector);
10814
12165
  if (clickResult.startsWith("Error:")) return clickResult;
@@ -10817,6 +12168,18 @@ async function clickResolvedSelector(wc, selector) {
10817
12168
  if (afterUrl !== beforeUrl) {
10818
12169
  return `${clickText} -> ${afterUrl}`;
10819
12170
  }
12171
+ const overlayHint = await detectPostClickOverlay(wc);
12172
+ if (overlayHint) {
12173
+ const dialogActions = cartMatch ? await getCartDialogActions(wc) : null;
12174
+ const actionsSuffix = dialogActions ? `
12175
+ ${dialogActions}
12176
+ Click one of these dialog actions. Do NOT click any other element.` : "";
12177
+ return `${clickText} (${clickResult})
12178
+ ${overlayHint}${actionsSuffix}`;
12179
+ }
12180
+ if (cartMatch) {
12181
+ return `${clickText} (${clickResult})`;
12182
+ }
10820
12183
  const activationResult = await activateElement(wc, selector);
10821
12184
  if (!activationResult.startsWith("Error:")) {
10822
12185
  await waitForPotentialNavigation(wc, beforeUrl);
@@ -10825,13 +12188,239 @@ async function clickResolvedSelector(wc, selector) {
10825
12188
  return `${clickText} -> ${fallbackUrl} (recovered via DOM activation)`;
10826
12189
  }
10827
12190
  }
12191
+ const postActivationOverlayHint = await detectPostClickOverlay(wc);
12192
+ if (postActivationOverlayHint) {
12193
+ return `${clickText} (${clickResult})
12194
+ ${postActivationOverlayHint}`;
12195
+ }
10828
12196
  return `${clickText} (${clickResult})`;
10829
12197
  }
12198
+ async function getCartDialogActions(wc) {
12199
+ const result = await wc.executeJavaScript(`
12200
+ (function() {
12201
+ function isVisible(el) {
12202
+ if (!(el instanceof HTMLElement)) return false;
12203
+ const style = getComputedStyle(el);
12204
+ if (style.display === "none" || style.visibility === "hidden") return false;
12205
+ const rect = el.getBoundingClientRect();
12206
+ return rect.width >= 20 && rect.height >= 10;
12207
+ }
12208
+
12209
+ function findDialogRoot() {
12210
+ const selectors = [
12211
+ '[data-test="basket-flyout"]',
12212
+ '[role="dialog"]',
12213
+ 'dialog[open]',
12214
+ '[role="alertdialog"]',
12215
+ '[aria-modal="true"]',
12216
+ ];
12217
+ for (const selector of selectors) {
12218
+ const nodes = document.querySelectorAll(selector);
12219
+ for (const node of nodes) {
12220
+ if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
12221
+ const text = (node.textContent || "").slice(0, 800).toLowerCase();
12222
+ const cartSignals = [
12223
+ "added to cart", "added to bag", "added to basket",
12224
+ "item added", "your basket", "your cart", "your bag",
12225
+ "view basket", "view cart", "continue shopping",
12226
+ ];
12227
+ if (cartSignals.some((signal) => text.includes(signal))) {
12228
+ return node;
12229
+ }
12230
+ }
12231
+ }
12232
+ return null;
12233
+ }
12234
+
12235
+ const dialog = findDialogRoot();
12236
+ if (!dialog) return { found: false, actions: [] };
12237
+
12238
+ const actions = [];
12239
+ dialog.querySelectorAll('button, a[href], [role="button"]').forEach((el) => {
12240
+ if (!(el instanceof HTMLElement) || !isVisible(el)) return;
12241
+ const label = (el.getAttribute("aria-label") || el.textContent || "").trim().slice(0, 80);
12242
+ if (!label || label.length < 2) return;
12243
+ const href = el.getAttribute("href") || "";
12244
+ const selector = el.id ? "#" + el.id
12245
+ : el.getAttribute("data-test") ? '[data-test="' + el.getAttribute("data-test") + '"]'
12246
+ : el.getAttribute("aria-label") ? '[aria-label="' + el.getAttribute("aria-label") + '"]'
12247
+ : null;
12248
+ if (selector) {
12249
+ actions.push({ label: label, href: href, selector: selector });
12250
+ }
12251
+ });
12252
+
12253
+ return {
12254
+ found: true,
12255
+ actions: actions.map((action) =>
12256
+ '- "' + action.label + '"' +
12257
+ (action.href ? ' -> ' + action.href : "") +
12258
+ (action.selector ? ' (selector: ' + action.selector + ')' : "")
12259
+ ),
12260
+ };
12261
+ })()
12262
+ `);
12263
+ if (!result || typeof result !== "object" || !("found" in result) || !result.found) {
12264
+ return null;
12265
+ }
12266
+ if (!("actions" in result) || !Array.isArray(result.actions) || result.actions.length === 0) {
12267
+ return null;
12268
+ }
12269
+ return `Available dialog actions:
12270
+ ${result.actions.join("\n")}`;
12271
+ }
12272
+ async function detectPostClickOverlay(wc) {
12273
+ const result = await wc.executeJavaScript(`
12274
+ (function() {
12275
+ var vw = window.innerWidth || document.documentElement.clientWidth;
12276
+ var vh = window.innerHeight || document.documentElement.clientHeight;
12277
+ var vpArea = Math.max(1, vw * vh);
12278
+
12279
+ function isVisible(el) {
12280
+ if (!(el instanceof HTMLElement)) return false;
12281
+ var style = getComputedStyle(el);
12282
+ if (style.display === "none" || style.visibility === "hidden") return false;
12283
+ return el.getBoundingClientRect().width > 0;
12284
+ }
12285
+
12286
+ function hasFixedAncestor(el) {
12287
+ var current = el.parentElement;
12288
+ while (current && current !== document.body) {
12289
+ var position = getComputedStyle(current).position;
12290
+ if (position === "fixed" || position === "sticky") return true;
12291
+ current = current.parentElement;
12292
+ }
12293
+ return false;
12294
+ }
12295
+
12296
+ function effectiveZ(el) {
12297
+ var current = el;
12298
+ while (current && current !== document.body) {
12299
+ var z = parseInt(getComputedStyle(current).zIndex, 10);
12300
+ if (z > 0) return z;
12301
+ current = current.parentElement;
12302
+ }
12303
+ return 0;
12304
+ }
12305
+
12306
+ function touchesViewportEdge(rect) {
12307
+ return rect.left <= 24 || rect.top <= 24 ||
12308
+ rect.right >= vw - 24 || rect.bottom >= vh - 24;
12309
+ }
12310
+
12311
+ var cartPhrases = [
12312
+ "added to cart", "added to bag", "added to basket",
12313
+ "added to your cart", "added to your bag", "added to your basket",
12314
+ ];
12315
+ var cartActions = [
12316
+ "view cart", "go to cart", "view basket", "go to basket",
12317
+ "continue shopping", "keep shopping", "checkout",
12318
+ ];
12319
+
12320
+ var selectors = 'dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"], [data-test="basket-flyout"]';
12321
+ var candidates = document.querySelectorAll(selectors);
12322
+ var hit = null;
12323
+ for (var i = 0; i < candidates.length; i++) {
12324
+ if (isVisible(candidates[i])) {
12325
+ hit = candidates[i];
12326
+ break;
12327
+ }
12328
+ }
12329
+
12330
+ if (!hit) {
12331
+ var elements = document.querySelectorAll("*");
12332
+ for (var j = 0; j < elements.length; j++) {
12333
+ var el = elements[j];
12334
+ if (!(el instanceof HTMLElement) || !isVisible(el)) continue;
12335
+ var style = getComputedStyle(el);
12336
+ var position = style.position;
12337
+ var isFixed = position === "fixed" || position === "sticky";
12338
+ var isAbsolute = position === "absolute";
12339
+ if (!isFixed && !isAbsolute) continue;
12340
+ if (isAbsolute && !hasFixedAncestor(el)) continue;
12341
+ if (effectiveZ(el) < 5) continue;
12342
+ var rect = el.getBoundingClientRect();
12343
+ var areaRatio = (rect.width * rect.height) / vpArea;
12344
+ if (rect.width >= 160 && rect.height >= 100 && areaRatio >= 0.05 && touchesViewportEdge(rect)) {
12345
+ hit = el;
12346
+ break;
12347
+ }
12348
+ }
12349
+ }
12350
+
12351
+ if (!hit) return { found: false, label: "", cartLike: false };
12352
+ var text = (hit.textContent || "").slice(0, 800).toLowerCase();
12353
+ var cartLike = cartPhrases.concat(cartActions).some(function(signal) {
12354
+ return text.indexOf(signal) !== -1;
12355
+ });
12356
+ var heading = hit.querySelector("h1,h2,h3,h4");
12357
+ var label = (hit.getAttribute("aria-label") || (heading && heading.textContent) || "").trim().slice(0, 80);
12358
+ return { found: true, label: label, cartLike: cartLike };
12359
+ })()
12360
+ `);
12361
+ if (!result || typeof result !== "object" || !("found" in result) || !result.found) {
12362
+ return null;
12363
+ }
12364
+ const label = typeof result.label === "string" && result.label ? ` ("${result.label}")` : "";
12365
+ if ("cartLike" in result && result.cartLike) {
12366
+ return `A cart confirmation dialog appeared${label}. Call read_page to see available actions — do not click Add to Cart again.`;
12367
+ }
12368
+ return `A dialog or overlay appeared${label}. Call read_page to see available actions.`;
12369
+ }
10830
12370
  async function dismissPopup(wc) {
10831
12371
  const before = await extractContent(wc);
10832
12372
  const initialBlocking = before.overlays.filter(
10833
12373
  (overlay) => overlay.blocksInteraction
10834
12374
  ).length;
12375
+ if (initialBlocking > 0) {
12376
+ const overlayText = before.overlays.map(
12377
+ (o) => [o.label, o.text].filter(Boolean).join(" ")
12378
+ ).join(" ").toLowerCase();
12379
+ const cartSignals = [
12380
+ "added to cart",
12381
+ "added to bag",
12382
+ "added to basket",
12383
+ "item added",
12384
+ "items in your basket",
12385
+ "items in your cart",
12386
+ "items in your bag",
12387
+ "your basket",
12388
+ "your cart",
12389
+ "your bag",
12390
+ "view basket",
12391
+ "view cart",
12392
+ "continue shopping"
12393
+ ];
12394
+ if (cartSignals.some((s) => overlayText.includes(s))) {
12395
+ const continueResult = await wc.executeJavaScript(`
12396
+ (function() {
12397
+ var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
12398
+ if (!dialog) return "Error: dialog not found";
12399
+ var buttons = dialog.querySelectorAll('button, a[href], [role="button"]');
12400
+ var continueBtn = null;
12401
+ var viewCartBtn = null;
12402
+ for (var i = 0; i < buttons.length; i++) {
12403
+ var label = (buttons[i].getAttribute('aria-label') || buttons[i].textContent || '').trim().toLowerCase();
12404
+ if (/continue shopping|keep shopping/.test(label)) { continueBtn = buttons[i]; break; }
12405
+ if (/view (basket|cart|bag)|checkout/.test(label) && !viewCartBtn) { viewCartBtn = buttons[i]; }
12406
+ }
12407
+ var target = continueBtn || viewCartBtn;
12408
+ if (!target) return "Error: no dialog action found";
12409
+ var actionLabel = (target.getAttribute('aria-label') || target.textContent || '').trim();
12410
+ if (target.tagName === 'A' && target.href) {
12411
+ window.location.href = target.href;
12412
+ return "Clicked: " + actionLabel + " -> " + target.href;
12413
+ }
12414
+ target.click();
12415
+ return "Clicked: " + actionLabel;
12416
+ })()
12417
+ `);
12418
+ if (typeof continueResult === "string" && !continueResult.startsWith("Error")) {
12419
+ return `Cart confirmation handled: ${continueResult}. Item was already added to your cart.`;
12420
+ }
12421
+ 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.";
12422
+ }
12423
+ }
10835
12424
  const initialDormant = before.dormantOverlays.length;
10836
12425
  const candidates = await wc.executeJavaScript(`
10837
12426
  (function() {
@@ -11077,7 +12666,13 @@ async function getPostActionState(tabManager, name) {
11077
12666
  "reload",
11078
12667
  "press_key"
11079
12668
  ];
11080
- const interactActions = ["type", "type_text", "select_option", "hover", "focus"];
12669
+ const interactActions = [
12670
+ "type",
12671
+ "type_text",
12672
+ "select_option",
12673
+ "hover",
12674
+ "focus"
12675
+ ];
11081
12676
  const tabActions = ["create_tab", "switch_tab", "close_tab"];
11082
12677
  if (navActions.includes(name)) {
11083
12678
  let warning = "";
@@ -11260,7 +12855,8 @@ async function hoverElement(wc, selector) {
11260
12855
  if ("error" in pos && typeof pos.error === "string") return pos.error;
11261
12856
  const x = typeof pos.x === "number" ? pos.x : null;
11262
12857
  const y = typeof pos.y === "number" ? pos.y : null;
11263
- if (x == null || y == null) return "Error: Could not resolve hover coordinates";
12858
+ if (x == null || y == null)
12859
+ return "Error: Could not resolve hover coordinates";
11264
12860
  wc.sendInputEvent({ type: "mouseMove", x, y });
11265
12861
  const label = typeof pos.label === "string" ? pos.label : "element";
11266
12862
  return `Hovered: ${label}`;
@@ -11479,7 +13075,10 @@ async function waitForCondition(wc, text, selector, timeoutMs) {
11479
13075
  const expectedText = (text || "").trim();
11480
13076
  const expectedSelector = (selector || "").trim();
11481
13077
  if (!expectedText && !expectedSelector) {
11482
- return JSON.stringify({ matched: false, error: "wait_for requires text or selector" });
13078
+ return JSON.stringify({
13079
+ matched: false,
13080
+ error: "wait_for requires text or selector"
13081
+ });
11483
13082
  }
11484
13083
  if (wc.isLoading()) {
11485
13084
  await waitForLoad(wc, Math.min(effectiveTimeout, 5e3));
@@ -11503,13 +13102,26 @@ async function waitForCondition(wc, text, selector, timeoutMs) {
11503
13102
  `);
11504
13103
  const elapsedMs2 = Date.now() - startedAt;
11505
13104
  if (result === "selector") {
11506
- return JSON.stringify({ matched: true, type: "selector", value: expectedSelector, elapsed_ms: elapsedMs2 });
13105
+ return JSON.stringify({
13106
+ matched: true,
13107
+ type: "selector",
13108
+ value: expectedSelector,
13109
+ elapsed_ms: elapsedMs2
13110
+ });
11507
13111
  }
11508
13112
  if (result === "text") {
11509
- return JSON.stringify({ matched: true, type: "text", value: expectedText.slice(0, 80), elapsed_ms: elapsedMs2 });
13113
+ return JSON.stringify({
13114
+ matched: true,
13115
+ type: "text",
13116
+ value: expectedText.slice(0, 80),
13117
+ elapsed_ms: elapsedMs2
13118
+ });
11510
13119
  }
11511
13120
  if (typeof result === "string" && result.startsWith("invalid_selector:")) {
11512
- return JSON.stringify({ matched: false, error: `Invalid selector "${expectedSelector}" — ${result.slice(17)}` });
13121
+ return JSON.stringify({
13122
+ matched: false,
13123
+ error: `Invalid selector "${expectedSelector}" — ${result.slice(17)}`
13124
+ });
11513
13125
  }
11514
13126
  await new Promise((resolve) => setTimeout(resolve, 150));
11515
13127
  }
@@ -11653,7 +13265,12 @@ function registerTools(server, tabManager, runtime) {
11653
13265
  pageType,
11654
13266
  pageUrl,
11655
13267
  pageTitle,
11656
- recommended: scored.filter((t) => t.score <= 20).map(({ name, title, description, relevance }) => ({ name, title, description, relevance })),
13268
+ recommended: scored.filter((t) => t.score <= 20).map(({ name, title, description, relevance }) => ({
13269
+ name,
13270
+ title,
13271
+ description,
13272
+ relevance
13273
+ })),
11657
13274
  available: scored.filter((t) => t.score > 20).map(({ name, title, relevance }) => ({ name, title, relevance }))
11658
13275
  };
11659
13276
  return {
@@ -11685,8 +13302,12 @@ function registerTools(server, tabManager, runtime) {
11685
13302
  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
13303
  inputSchema: {
11687
13304
  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"),
13305
+ stream_id: zod.z.string().optional().describe(
13306
+ "Stable stream ID for incremental updates to the same entry"
13307
+ ),
13308
+ mode: zod.z.enum(["append", "replace", "final"]).optional().describe(
13309
+ "append (default), replace current stream text, or mark the stream final"
13310
+ ),
11690
13311
  kind: zod.z.enum(["thinking", "message", "status"]).optional().describe("Visual style for the transcript entry"),
11691
13312
  title: zod.z.string().optional().describe("Optional short label such as Plan, Search, or Summary")
11692
13313
  }
@@ -11737,7 +13358,9 @@ function registerTools(server, tabManager, runtime) {
11737
13358
  ];
11738
13359
  async function buildExtractResponse(pageContent, mode, adBlockingEnabled, wc) {
11739
13360
  const adBlockLine = `**Ad Blocking:** ${adBlockingEnabled ? "On" : "Off"}`;
11740
- const savedHighlights = getHighlightsForUrl(pageContent.url);
13361
+ const savedHighlights = getHighlightsForUrl(
13362
+ pageContent.url
13363
+ );
11741
13364
  const liveSelectionSection = wc ? formatLiveSelectionSection(
11742
13365
  await captureLiveHighlightSnapshot(wc, savedHighlights)
11743
13366
  ) : null;
@@ -11854,10 +13477,18 @@ ${buildScopedContext(pageContent, mode)}`;
11854
13477
  async ({ url }) => {
11855
13478
  const tab = tabManager.getActiveTab();
11856
13479
  if (!tab) return asTextResponse("Error: No active tab");
13480
+ const preCheck = await validateLinkDestination(url);
13481
+ if (preCheck.status === "dead") {
13482
+ return asTextResponse(
13483
+ `Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
13484
+ );
13485
+ }
11857
13486
  return withAction(runtime, tabManager, "navigate", { url }, async () => {
11858
13487
  const id = tabManager.getActiveTabId();
11859
13488
  tabManager.navigateTab(id, url);
11860
- const { httpStatus } = await waitForLoadWithStatus(tab.view.webContents);
13489
+ const { httpStatus } = await waitForLoadWithStatus(
13490
+ tab.view.webContents
13491
+ );
11861
13492
  const finalUrl = tab.view.webContents.getURL();
11862
13493
  const statusNote = httpStatus !== null && httpStatus >= 400 ? ` [HTTP ${httpStatus} — page may be missing or unavailable, consider navigating back and trying a different link]` : "";
11863
13494
  return `Navigated to ${finalUrl}${statusNote}`;
@@ -11927,7 +13558,9 @@ ${buildScopedContext(pageContent, mode)}`;
11927
13558
  const pageContent = await extractContent(tab.view.webContents);
11928
13559
  const requestedType = typeof type === "string" && type.trim() ? type.trim().toLowerCase() : "";
11929
13560
  const entities = (pageContent.structuredData ?? []).filter(
11930
- (entity) => requestedType ? entity.types.some((entry) => entry.toLowerCase() === requestedType) : true
13561
+ (entity) => requestedType ? entity.types.some(
13562
+ (entry) => entry.toLowerCase() === requestedType
13563
+ ) : true
11931
13564
  );
11932
13565
  const sourceCounts = {
11933
13566
  json_ld: pageContent.jsonLd?.length ?? 0,
@@ -12166,7 +13799,9 @@ ${buildScopedContext(pageContent, mode)}`;
12166
13799
  })()
12167
13800
  `);
12168
13801
  if (!result || typeof result !== "object") {
12169
- return asTextResponse("Error: Element text extraction returned no result");
13802
+ return asTextResponse(
13803
+ "Error: Element text extraction returned no result"
13804
+ );
12170
13805
  }
12171
13806
  if ("error" in result && typeof result.error === "string") {
12172
13807
  return asTextResponse(`Error: ${result.error}`);
@@ -12216,11 +13851,7 @@ ${buildScopedContext(pageContent, mode)}`;
12216
13851
  return "Error: No index or selector provided";
12217
13852
  }
12218
13853
  if (mode === "keystroke") {
12219
- return typeKeystroke(
12220
- tab.view.webContents,
12221
- resolvedSelector,
12222
- text
12223
- );
13854
+ return typeKeystroke(tab.view.webContents, resolvedSelector, text);
12224
13855
  }
12225
13856
  return setElementValue(tab.view.webContents, resolvedSelector, text);
12226
13857
  }
@@ -12259,11 +13890,7 @@ ${buildScopedContext(pageContent, mode)}`;
12259
13890
  return "Error: No index or selector provided";
12260
13891
  }
12261
13892
  if (mode === "keystroke") {
12262
- return typeKeystroke(
12263
- tab.view.webContents,
12264
- resolvedSelector,
12265
- text
12266
- );
13893
+ return typeKeystroke(tab.view.webContents, resolvedSelector, text);
12267
13894
  }
12268
13895
  return setElementValue(tab.view.webContents, resolvedSelector, text);
12269
13896
  }
@@ -12667,8 +14294,14 @@ ${buildScopedContext(pageContent, mode)}`;
12667
14294
  `Error capturing screenshot: ${screenshot.error}`
12668
14295
  );
12669
14296
  }
12670
- const screenshotPath = path$1.join(os.tmpdir(), `vessel_screenshot_${Date.now()}.png`);
12671
- fs$1.writeFileSync(screenshotPath, Buffer.from(screenshot.base64, "base64"));
14297
+ const screenshotPath = path$1.join(
14298
+ os.tmpdir(),
14299
+ `vessel_screenshot_${Date.now()}.png`
14300
+ );
14301
+ fs$1.writeFileSync(
14302
+ screenshotPath,
14303
+ Buffer.from(screenshot.base64, "base64")
14304
+ );
12672
14305
  return {
12673
14306
  content: [
12674
14307
  {
@@ -12758,12 +14391,18 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
12758
14391
  async () => {
12759
14392
  const tab = tabManager.getActiveTab();
12760
14393
  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
- });
14394
+ return withAction(
14395
+ runtime,
14396
+ tabManager,
14397
+ "clear_highlights",
14398
+ {},
14399
+ async () => {
14400
+ const wc = tab.view.webContents;
14401
+ const url = normalizeUrl(wc.getURL());
14402
+ clearHighlightsForUrl(url);
14403
+ return clearHighlights(wc);
14404
+ }
14405
+ );
12767
14406
  }
12768
14407
  );
12769
14408
  server.registerTool(
@@ -12934,12 +14573,22 @@ ${JSON.stringify(otherHighlights, null, 2)}`
12934
14573
  title: "Save Bookmark",
12935
14574
  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
14575
  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"),
14576
+ url: zod.z.string().optional().describe(
14577
+ "URL to bookmark. Omit to use the current page or provide index/selector to bookmark a link target from the page"
14578
+ ),
14579
+ title: zod.z.string().optional().describe(
14580
+ "Human-readable title for the bookmark. Omit to use the page or link text"
14581
+ ),
14582
+ index: zod.z.number().optional().describe(
14583
+ "Element index of a link on the current page to bookmark without opening it"
14584
+ ),
14585
+ selector: zod.z.string().optional().describe(
14586
+ "CSS selector of a link on the current page to bookmark without opening it"
14587
+ ),
12941
14588
  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"),
14589
+ folder_name: zod.z.string().optional().describe(
14590
+ "Folder name to save into. Created automatically if missing"
14591
+ ),
12943
14592
  folder_summary: zod.z.string().optional().describe("Optional one-sentence summary if a new folder is created"),
12944
14593
  create_folder_if_missing: zod.z.boolean().optional().describe("Create folder_name automatically when it does not exist"),
12945
14594
  note: zod.z.string().optional().describe("Optional note about why this was bookmarked"),
@@ -13085,10 +14734,16 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13085
14734
  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
14735
  inputSchema: {
13087
14736
  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"),
14737
+ url: zod.z.string().optional().describe(
14738
+ "URL to organize. Omit to use the current page or provide index/selector to target a link"
14739
+ ),
13089
14740
  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"),
14741
+ index: zod.z.number().optional().describe(
14742
+ "Element index of a link on the current page to organize without opening it"
14743
+ ),
14744
+ selector: zod.z.string().optional().describe(
14745
+ "CSS selector of a link on the current page to organize without opening it"
14746
+ ),
13092
14747
  folder_id: zod.z.string().optional().describe("Folder ID to organize into"),
13093
14748
  folder_name: zod.z.string().optional().describe("Folder name to organize into"),
13094
14749
  folder_summary: zod.z.string().optional().describe("Optional summary used if a new folder is created"),
@@ -13215,10 +14870,16 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13215
14870
  description: 'Archive the current page, a URL, a link target from the current page, or an existing bookmark into the default "Archive" folder.',
13216
14871
  inputSchema: {
13217
14872
  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"),
14873
+ url: zod.z.string().optional().describe(
14874
+ "URL to archive. Omit to use the current page or provide index/selector to target a link"
14875
+ ),
13219
14876
  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"),
14877
+ index: zod.z.number().optional().describe(
14878
+ "Element index of a link on the current page to archive without opening it"
14879
+ ),
14880
+ selector: zod.z.string().optional().describe(
14881
+ "CSS selector of a link on the current page to archive without opening it"
14882
+ ),
13222
14883
  note: zod.z.string().optional().describe("Optional note to store with the archived bookmark")
13223
14884
  }
13224
14885
  },
@@ -13374,7 +15035,11 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13374
15035
  `Folder "${existing.name}" already exists (id=${existing.id})`
13375
15036
  );
13376
15037
  }
13377
- const folder = renameFolder(folder_id, new_name, summary);
15038
+ const folder = renameFolder(
15039
+ folder_id,
15040
+ new_name,
15041
+ summary
15042
+ );
13378
15043
  return folder ? composeFolderAwareResponse(`Renamed folder to "${folder.name}"`) : `Folder ${folder_id} not found`;
13379
15044
  }
13380
15045
  );
@@ -13575,13 +15240,21 @@ ${JSON.stringify(otherHighlights, null, 2)}`
13575
15240
  title: "Start Workflow",
13576
15241
  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
15242
  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'])")
15243
+ goal: zod.z.string().describe(
15244
+ "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
15245
+ ),
15246
+ steps: zod.z.array(zod.z.string()).describe(
15247
+ "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
15248
+ )
13580
15249
  }
13581
15250
  },
13582
15251
  async ({ goal, steps }) => {
13583
15252
  const tab = tabManager.getActiveTab();
13584
- const flow = runtime.startFlow(goal, steps, tab?.view.webContents.getURL());
15253
+ const flow = runtime.startFlow(
15254
+ goal,
15255
+ steps,
15256
+ tab?.view.webContents.getURL()
15257
+ );
13585
15258
  return asTextResponse(
13586
15259
  `Flow started: ${flow.goal}
13587
15260
  ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
@@ -13635,13 +15308,18 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13635
15308
  },
13636
15309
  async () => {
13637
15310
  const tab = tabManager.getActiveTab();
13638
- if (!tab) return asTextResponse("No active tab. Use vessel_navigate to open a page.");
15311
+ if (!tab)
15312
+ return asTextResponse(
15313
+ "No active tab. Use vessel_navigate to open a page."
15314
+ );
13639
15315
  const wc = tab.view.webContents;
13640
15316
  let page;
13641
15317
  try {
13642
15318
  page = await extractContent(wc);
13643
15319
  } catch {
13644
- return asTextResponse("Could not read page. Try vessel_navigate to a working URL.");
15320
+ return asTextResponse(
15321
+ "Could not read page. Try vessel_navigate to a working URL."
15322
+ );
13645
15323
  }
13646
15324
  const suggestions = [];
13647
15325
  suggestions.push(`Page: ${page.title || "(untitled)"}`);
@@ -13661,23 +15339,33 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13661
15339
  );
13662
15340
  const formCount = page.forms.length;
13663
15341
  const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
13664
- const linkCount = page.interactiveElements.filter((el) => el.type === "link").length;
15342
+ const linkCount = page.interactiveElements.filter(
15343
+ (el) => el.type === "link"
15344
+ ).length;
13665
15345
  const hasPagination = page.interactiveElements.some(
13666
15346
  (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
13667
15347
  );
13668
15348
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
13669
15349
  if (hasOverlays) {
13670
15350
  suggestions.push("⚠ BLOCKING OVERLAY detected — dismiss it first:");
13671
- suggestions.push(" → vessel_dismiss_popup or vessel_click on close/accept button");
15351
+ suggestions.push(
15352
+ " → vessel_dismiss_popup or vessel_click on close/accept button"
15353
+ );
13672
15354
  suggestions.push("");
13673
15355
  }
13674
15356
  if (hasPasswordField) {
13675
15357
  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");
15358
+ suggestions.push(
15359
+ " → vessel_login(username, password) handles the full flow"
15360
+ );
15361
+ suggestions.push(
15362
+ " → Or vessel_fill_form + vessel_submit_form for manual control"
15363
+ );
13678
15364
  } else if (hasSearchInput && linkCount < 10) {
13679
15365
  suggestions.push("🔍 SEARCH PAGE detected:");
13680
- suggestions.push(" → vessel_search(query) — finds the box, types, submits");
15366
+ suggestions.push(
15367
+ " → vessel_search(query) — finds the box, types, submits"
15368
+ );
13681
15369
  } else if (hasSearchInput && linkCount >= 10) {
13682
15370
  suggestions.push("📋 SEARCH RESULTS detected:");
13683
15371
  suggestions.push(" → vessel_click on a result link");
@@ -13686,7 +15374,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13686
15374
  }
13687
15375
  } else if (formCount > 0) {
13688
15376
  suggestions.push(`📝 FORM detected (${totalFields} fields):`);
13689
- suggestions.push(" → vessel_fill_form(fields) — fill all fields at once");
15377
+ suggestions.push(
15378
+ " → vessel_fill_form(fields) — fill all fields at once"
15379
+ );
13690
15380
  suggestions.push(" → Or vessel_type for individual fields");
13691
15381
  } else if (hasPagination) {
13692
15382
  suggestions.push("📄 PAGINATED CONTENT:");
@@ -13698,12 +15388,16 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13698
15388
  suggestions.push(" → vessel_scroll to see more");
13699
15389
  } else {
13700
15390
  suggestions.push("🌐 GENERAL PAGE:");
13701
- suggestions.push(" → vessel_extract_content to understand the page structure");
15391
+ suggestions.push(
15392
+ " → vessel_extract_content to understand the page structure"
15393
+ );
13702
15394
  suggestions.push(" → vessel_click on any element by index");
13703
15395
  suggestions.push(" → vessel_navigate to go somewhere new");
13704
15396
  }
13705
15397
  suggestions.push("");
13706
- suggestions.push(`Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`);
15398
+ suggestions.push(
15399
+ `Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`
15400
+ );
13707
15401
  return asTextResponse(suggestions.join("\n"));
13708
15402
  }
13709
15403
  );
@@ -13717,9 +15411,14 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13717
15411
  zod.z.object({
13718
15412
  index: zod.z.number().optional().describe("Element index from page content"),
13719
15413
  selector: zod.z.string().optional().describe("CSS selector fallback"),
15414
+ name: zod.z.string().optional().describe("Field name or id, such as custname"),
15415
+ label: zod.z.string().optional().describe("Visible label or aria-label text"),
15416
+ placeholder: zod.z.string().optional().describe("Placeholder text shown in the field"),
13720
15417
  value: zod.z.string().describe("Value to enter")
13721
15418
  })
13722
- ).describe("Fields to fill"),
15419
+ ).describe(
15420
+ "Fields to fill, matched by index, selector, name, label, or placeholder"
15421
+ ),
13723
15422
  submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
13724
15423
  }
13725
15424
  },
@@ -13733,18 +15432,10 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
13733
15432
  { fieldCount: fields.length, submit },
13734
15433
  async () => {
13735
15434
  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
- }
15435
+ const fillResults = await fillFormFields(wc, fields);
15436
+ const results = fillResults.map((item) => item.result);
13746
15437
  if (submit) {
13747
- const firstSel = await resolveSelector(wc, fields[0]?.index, fields[0]?.selector);
15438
+ const firstSel = fillResults.find((item) => item.selector)?.selector ?? null;
13748
15439
  if (firstSel) {
13749
15440
  const beforeUrl = wc.getURL();
13750
15441
  const submitResult = await submitForm(wc, void 0, firstSel);
@@ -13770,12 +15461,25 @@ ${results.join("\n")}`;
13770
15461
  url: zod.z.string().optional().describe("Login page URL (skip if already on login page)"),
13771
15462
  username: zod.z.string().describe("Username or email"),
13772
15463
  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)")
15464
+ username_selector: zod.z.string().optional().describe(
15465
+ "CSS selector for username field (auto-detected if omitted)"
15466
+ ),
15467
+ password_selector: zod.z.string().optional().describe(
15468
+ "CSS selector for password field (auto-detected if omitted)"
15469
+ ),
15470
+ submit_selector: zod.z.string().optional().describe(
15471
+ "CSS selector for submit button (auto-detected if omitted)"
15472
+ )
13776
15473
  }
13777
15474
  },
13778
- async ({ url, username, password, username_selector, password_selector, submit_selector }) => {
15475
+ async ({
15476
+ url,
15477
+ username,
15478
+ password,
15479
+ username_selector,
15480
+ password_selector,
15481
+ submit_selector
15482
+ }) => {
13779
15483
  const tab = tabManager.getActiveTab();
13780
15484
  if (!tab) return asTextResponse("Error: No active tab");
13781
15485
  return withAction(
@@ -13798,14 +15502,16 @@ ${results.join("\n")}`;
13798
15502
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
13799
15503
  })()
13800
15504
  `);
13801
- if (!userSel) return "Error: Could not find username/email field. Try providing username_selector.";
15505
+ if (!userSel)
15506
+ return "Error: Could not find username/email field. Try providing username_selector.";
13802
15507
  const passSel = password_selector || await wc.executeJavaScript(`
13803
15508
  (function() {
13804
15509
  var el = document.querySelector('input[type="password"]');
13805
15510
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
13806
15511
  })()
13807
15512
  `);
13808
- if (!passSel) return "Error: Could not find password field. Try providing password_selector.";
15513
+ if (!passSel)
15514
+ return "Error: Could not find password field. Try providing password_selector.";
13809
15515
  const userResult = await setElementValue(wc, userSel, username);
13810
15516
  steps.push(userResult);
13811
15517
  const passResult = await setElementValue(wc, passSel, password);
@@ -13823,7 +15529,8 @@ ${results.join("\n")}`;
13823
15529
  return false;
13824
15530
  })()
13825
15531
  `);
13826
- if (!clicked) return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
15532
+ if (!clicked)
15533
+ return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
13827
15534
  }
13828
15535
  await waitForPotentialNavigation(wc, beforeUrl);
13829
15536
  const afterUrl = wc.getURL();
@@ -13849,14 +15556,41 @@ ${steps.join("\n")}`;
13849
15556
  async ({ query, selector }) => {
13850
15557
  const tab = tabManager.getActiveTab();
13851
15558
  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(`
15559
+ const qLower = query.toLowerCase().trim();
15560
+ const buttonLabels = [
15561
+ "add to cart",
15562
+ "add to bag",
15563
+ "add to basket",
15564
+ "buy now",
15565
+ "buy it now",
15566
+ "purchase",
15567
+ "continue shopping",
15568
+ "keep shopping",
15569
+ "view cart",
15570
+ "view bag",
15571
+ "view basket",
15572
+ "go to cart",
15573
+ "go to checkout",
15574
+ "checkout",
15575
+ "check out",
15576
+ "proceed to checkout",
15577
+ "place order",
15578
+ "submit",
15579
+ "subscribe",
15580
+ "sign up",
15581
+ "sign in",
15582
+ "log in",
15583
+ "register",
15584
+ "continue"
15585
+ ];
15586
+ if (buttonLabels.some((p) => qLower.includes(p))) {
15587
+ return asTextResponse(
15588
+ `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`
15589
+ );
15590
+ }
15591
+ return withAction(runtime, tabManager, "search", { query }, async () => {
15592
+ const wc = tab.view.webContents;
15593
+ const searchSel = selector || await wc.executeJavaScript(`
13860
15594
  (function() {
13861
15595
  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
15596
  if (!el) {
@@ -13872,24 +15606,24 @@ ${steps.join("\n")}`;
13872
15606
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
13873
15607
  })()
13874
15608
  `);
13875
- if (!searchSel) return "Error: Could not find search input. Try providing a selector.";
13876
- await setElementValue(wc, searchSel, query);
13877
- await wc.executeJavaScript(`
15609
+ if (!searchSel)
15610
+ return "Error: Could not find search input. Try providing a selector.";
15611
+ await setElementValue(wc, searchSel, query);
15612
+ await wc.executeJavaScript(`
13878
15613
  (function() {
13879
15614
  var el = document.querySelector(${JSON.stringify(searchSel)});
13880
15615
  if (el) el.focus();
13881
15616
  })()
13882
15617
  `);
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
- );
15618
+ await new Promise((r) => setTimeout(r, 50));
15619
+ const beforeUrl = wc.getURL();
15620
+ wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
15621
+ await new Promise((r) => setTimeout(r, 16));
15622
+ wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
15623
+ await waitForPotentialNavigation(wc, beforeUrl);
15624
+ const afterUrl = wc.getURL();
15625
+ return afterUrl !== beforeUrl ? `Searched "${query}" → ${afterUrl}` : `Searched "${query}" (same page — results may have loaded dynamically)`;
15626
+ });
13893
15627
  }
13894
15628
  );
13895
15629
  server.registerTool(
@@ -13899,7 +15633,9 @@ ${steps.join("\n")}`;
13899
15633
  description: "Navigate to the next or previous page of results. Auto-detects pagination controls.",
13900
15634
  inputSchema: {
13901
15635
  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)")
15636
+ selector: zod.z.string().optional().describe(
15637
+ "CSS selector for the pagination link (auto-detected if omitted)"
15638
+ )
13903
15639
  }
13904
15640
  },
13905
15641
  async ({ direction, selector }) => {
@@ -13937,7 +15673,8 @@ ${steps.join("\n")}`;
13937
15673
  return false;
13938
15674
  })()
13939
15675
  `);
13940
- if (!clicked) return `Error: Could not find ${direction} pagination control. Try providing a selector.`;
15676
+ if (!clicked)
15677
+ return `Error: Could not find ${direction} pagination control. Try providing a selector.`;
13941
15678
  await waitForPotentialNavigation(wc, beforeUrl);
13942
15679
  const afterUrl = wc.getURL();
13943
15680
  return afterUrl !== beforeUrl ? `Paginated ${direction} → ${afterUrl}` : `Clicked ${direction} (page may have updated dynamically)`;
@@ -13953,12 +15690,15 @@ ${steps.join("\n")}`;
13953
15690
  inputSchema: zod.z.object({})
13954
15691
  },
13955
15692
  async () => {
15693
+ const tab = tabManager.getActiveTab();
15694
+ if (!tab) return asTextResponse("Error: No active tab");
13956
15695
  return withAction(
13957
- tabManager,
13958
15696
  runtime,
15697
+ tabManager,
13959
15698
  "vessel_accept_cookies",
13960
15699
  {},
13961
- async (wc) => {
15700
+ async () => {
15701
+ const wc = tab.view.webContents;
13962
15702
  const dismissed = await wc.executeJavaScript(`
13963
15703
  (function() {
13964
15704
  var selectors = [
@@ -14008,12 +15748,15 @@ ${steps.join("\n")}`;
14008
15748
  })
14009
15749
  },
14010
15750
  async ({ index, selector: rawSelector }) => {
15751
+ const tab = tabManager.getActiveTab();
15752
+ if (!tab) return asTextResponse("Error: No active tab");
14011
15753
  return withAction(
14012
- tabManager,
14013
15754
  runtime,
15755
+ tabManager,
14014
15756
  "vessel_extract_table",
14015
15757
  { index, selector: rawSelector },
14016
- async (wc) => {
15758
+ async () => {
15759
+ const wc = tab.view.webContents;
14017
15760
  const sel = rawSelector || (index != null ? await resolveSelector(wc, index) : null);
14018
15761
  const tableJson = await wc.executeJavaScript(`
14019
15762
  (function() {
@@ -14060,12 +15803,15 @@ ${JSON.stringify(tableJson, null, 2)}`;
14060
15803
  })
14061
15804
  },
14062
15805
  async ({ index, selector: rawSelector, position }) => {
15806
+ const tab = tabManager.getActiveTab();
15807
+ if (!tab) return asTextResponse("Error: No active tab");
14063
15808
  return withAction(
14064
- tabManager,
14065
15809
  runtime,
15810
+ tabManager,
14066
15811
  "vessel_scroll_to_element",
14067
15812
  { index, selector: rawSelector, position },
14068
- async (wc) => {
15813
+ async () => {
15814
+ const wc = tab.view.webContents;
14069
15815
  const sel = rawSelector || (index != null ? await resolveSelector(wc, index) : null);
14070
15816
  if (!sel) return "Error: Provide an index or selector.";
14071
15817
  const block = position === "top" ? "start" : position === "bottom" ? "end" : "center";
@@ -14115,12 +15861,15 @@ ${JSON.stringify(tableJson, null, 2)}`;
14115
15861
  })
14116
15862
  },
14117
15863
  async ({ timeoutMs }) => {
15864
+ const tab = tabManager.getActiveTab();
15865
+ if (!tab) return asTextResponse("Error: No active tab");
14118
15866
  return withAction(
14119
- tabManager,
14120
15867
  runtime,
15868
+ tabManager,
14121
15869
  "vessel_wait_for_navigation",
14122
15870
  { timeoutMs },
14123
- async (wc) => {
15871
+ async () => {
15872
+ const wc = tab.view.webContents;
14124
15873
  const timeout = timeoutMs || 1e4;
14125
15874
  const beforeUrl = wc.getURL();
14126
15875
  if (wc.isLoading()) {
@@ -14134,9 +15883,12 @@ ${JSON.stringify(tableJson, null, 2)}`;
14134
15883
  } else {
14135
15884
  await new Promise((resolve) => {
14136
15885
  let navigated = false;
14137
- const timer = setTimeout(() => {
14138
- if (!navigated) resolve();
14139
- }, Math.min(timeout, 2e3));
15886
+ const timer = setTimeout(
15887
+ () => {
15888
+ if (!navigated) resolve();
15889
+ },
15890
+ Math.min(timeout, 2e3)
15891
+ );
14140
15892
  wc.once("did-start-loading", () => {
14141
15893
  navigated = true;
14142
15894
  clearTimeout(timer);
@@ -14174,7 +15926,9 @@ ${JSON.stringify(tableJson, null, 2)}`;
14174
15926
  `Tool breakdown:`
14175
15927
  ];
14176
15928
  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` : ""}`);
15929
+ lines.push(
15930
+ ` ${name}: ${stats.count} calls, avg ${stats.avgMs}ms${stats.errors > 0 ? `, ${stats.errors} errors` : ""}`
15931
+ );
14178
15932
  }
14179
15933
  return asTextResponse(lines.join("\n"));
14180
15934
  }