@quanta-intellect/vessel-browser 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js CHANGED
@@ -710,7 +710,8 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
710
710
  if (text) {
711
711
  return wc.executeJavaScript(`
712
712
  (function() {
713
- var searchText = ${JSON.stringify(text)};
713
+ var searchText = (${JSON.stringify(text)} || '').trim();
714
+ var foldedSearchText = searchText.toLowerCase();
714
715
  var solidColor = ${JSON.stringify(c.solid)};
715
716
  var bgColor = ${JSON.stringify(c.bg)};
716
717
  var labelBg = ${JSON.stringify(c.label)};
@@ -748,7 +749,11 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
748
749
  });
749
750
  var n;
750
751
  while ((n = w.nextNode())) {
751
- var idx = n.textContent.indexOf(searchText);
752
+ var haystack = n.textContent || '';
753
+ var idx = haystack.indexOf(searchText);
754
+ if (idx === -1 && foldedSearchText) {
755
+ idx = haystack.toLowerCase().indexOf(foldedSearchText);
756
+ }
752
757
  if (idx !== -1) {
753
758
  matches.push({ node: n, idx: idx });
754
759
  if (matches.length >= limit) break;
@@ -4571,6 +4576,158 @@ function createProvider(config) {
4571
4576
  }
4572
4577
  return new OpenAICompatProvider(normalized);
4573
4578
  }
4579
+ const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
4580
+ const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
4581
+ function elementLabel(el) {
4582
+ return el.text?.trim() || el.label?.trim() || el.value?.trim() || el.placeholder?.trim() || void 0;
4583
+ }
4584
+ function isOverlayAction(el) {
4585
+ if (el.type === "button" || el.type === "link") return true;
4586
+ if (el.type !== "input") return false;
4587
+ return ["button", "submit", "radio", "checkbox"].includes(
4588
+ (el.inputType || "").toLowerCase()
4589
+ );
4590
+ }
4591
+ function isRadioOption(el) {
4592
+ return el.role === "radio" || el.type === "input" && (el.inputType || "").toLowerCase() === "radio";
4593
+ }
4594
+ function normalizeAction(el) {
4595
+ return {
4596
+ index: el.index,
4597
+ label: elementLabel(el),
4598
+ selector: el.selector,
4599
+ role: el.role,
4600
+ labelSource: el.labelSource,
4601
+ looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel(el))
4602
+ };
4603
+ }
4604
+ function normalizeStoredAction(action) {
4605
+ return {
4606
+ label: action.label,
4607
+ selector: action.selector,
4608
+ role: action.kind === "radio" ? "radio" : void 0
4609
+ };
4610
+ }
4611
+ function normalizeStoredRadioOption(option) {
4612
+ return {
4613
+ label: option.label,
4614
+ selector: option.selector,
4615
+ role: "radio",
4616
+ labelSource: option.labelSource,
4617
+ looksCorrect: option.looksCorrect !== void 0 ? option.looksCorrect : looksLikeCorrectOption(option.label)
4618
+ };
4619
+ }
4620
+ function dedupeCandidates(actions) {
4621
+ const seen = /* @__PURE__ */ new Set();
4622
+ return actions.filter((action) => {
4623
+ const key = [
4624
+ action.selector || "",
4625
+ action.label || "",
4626
+ action.role || "",
4627
+ action.labelSource || ""
4628
+ ].join("::");
4629
+ if (seen.has(key)) return false;
4630
+ seen.add(key);
4631
+ return true;
4632
+ });
4633
+ }
4634
+ function classifyOverlayKind(overlay, radioOptions) {
4635
+ if (overlay.kind) {
4636
+ return overlay.kind;
4637
+ }
4638
+ const haystack = [overlay.label, overlay.text, overlay.role].filter(Boolean).join(" ").toLowerCase();
4639
+ if (/cookie|consent|privacy|gdpr|ccpa|onetrust|trustarc|cookiebot/.test(
4640
+ haystack
4641
+ )) {
4642
+ return "cookie_consent";
4643
+ }
4644
+ if (radioOptions.length > 0) return "selection_modal";
4645
+ if (overlay.role === "alertdialog" || /\b(alert|warning|error)\b/.test(haystack)) {
4646
+ return "alert";
4647
+ }
4648
+ if (overlay.type === "dialog") return "dialog";
4649
+ if (overlay.type === "modal") return "modal";
4650
+ return "overlay";
4651
+ }
4652
+ function findAction(actions, matcher) {
4653
+ return actions.find(
4654
+ (action) => matcher.test((action.label || "").toLowerCase())
4655
+ );
4656
+ }
4657
+ function looksLikeCorrectOption(label) {
4658
+ const text = label?.trim();
4659
+ if (!text) return void 0;
4660
+ if (CORRECT_HINT_RE.test(text)) return true;
4661
+ if (WRONG_HINT_RE.test(text)) return false;
4662
+ return void 0;
4663
+ }
4664
+ function getBlockingOverlaySignature(overlays) {
4665
+ return overlays.filter((overlay) => overlay.blocksInteraction).map(
4666
+ (overlay) => [
4667
+ overlay.kind,
4668
+ overlay.selector || "",
4669
+ overlay.label || "",
4670
+ overlay.text || "",
4671
+ overlay.actions.map((action) => `${action.selector || ""}:${action.label || ""}`).join("|"),
4672
+ overlay.radioOptions.map((option) => `${option.selector || ""}:${option.label || ""}`).join("|")
4673
+ ].join("::")
4674
+ ).join("||");
4675
+ }
4676
+ function buildOverlayInventory(page) {
4677
+ if (page.overlays.length === 0) return [];
4678
+ return page.overlays.map((overlay) => {
4679
+ const controls = dedupeCandidates([
4680
+ ...page.interactiveElements.filter((el) => {
4681
+ if (overlay.selector && el.parentOverlay === overlay.selector) {
4682
+ return true;
4683
+ }
4684
+ return page.overlays.length === 1 && el.context === "dialog";
4685
+ }).filter(isOverlayAction).map(normalizeAction),
4686
+ ...(overlay.actions || []).map(normalizeStoredAction)
4687
+ ]).filter((action) => action.label || action.selector);
4688
+ const radioOptions = dedupeCandidates([
4689
+ ...page.interactiveElements.filter((el) => {
4690
+ if (!isRadioOption(el)) return false;
4691
+ if (overlay.selector && el.parentOverlay === overlay.selector) {
4692
+ return true;
4693
+ }
4694
+ return page.overlays.length === 1 && el.context === "dialog";
4695
+ }).map(normalizeAction),
4696
+ ...(overlay.radioOptions || []).map(normalizeStoredRadioOption)
4697
+ ]).filter((action) => action.label || action.selector);
4698
+ const kind = classifyOverlayKind(overlay, radioOptions);
4699
+ const dismissAction = findAction(
4700
+ controls,
4701
+ /\b(close|dismiss|skip|cancel|reject|decline|no thanks|not now|maybe later|continue without)\b/
4702
+ );
4703
+ const acceptAction = findAction(
4704
+ controls,
4705
+ /\b(accept|allow|agree|got it|ok|okay|consent)\b/
4706
+ );
4707
+ const submitAction = findAction(
4708
+ controls,
4709
+ /\b(submit|continue|confirm|done|next|save|apply|finish)\b/
4710
+ );
4711
+ const correctOption = radioOptions.find(
4712
+ (option) => option.looksCorrect === true
4713
+ );
4714
+ return {
4715
+ type: overlay.type,
4716
+ kind,
4717
+ role: overlay.role,
4718
+ label: overlay.label,
4719
+ selector: overlay.selector,
4720
+ text: overlay.text,
4721
+ blocksInteraction: overlay.blocksInteraction,
4722
+ actions: controls,
4723
+ radioOptions,
4724
+ dismissAction,
4725
+ acceptAction,
4726
+ submitAction,
4727
+ correctOption
4728
+ };
4729
+ });
4730
+ }
4574
4731
  const MAX_CONTENT_LENGTH = 6e4;
4575
4732
  const MAX_STRUCTURED_ITEMS = 100;
4576
4733
  const LARGE_PAGE_HINT_THRESHOLD = 12e3;
@@ -4638,6 +4795,14 @@ function formatElementMeta(el) {
4638
4795
  if (el.pattern) {
4639
4796
  meta.push(`pattern="${el.pattern}"`);
4640
4797
  }
4798
+ if (el.labelSource) {
4799
+ meta.push(`source=${el.labelSource}`);
4800
+ }
4801
+ if (el.looksCorrect === true) {
4802
+ meta.push("likely-correct");
4803
+ } else if (el.looksCorrect === false) {
4804
+ meta.push("likely-wrong");
4805
+ }
4641
4806
  if (el.description) {
4642
4807
  meta.push(`desc="${el.description.slice(0, 80)}"`);
4643
4808
  }
@@ -4867,7 +5032,7 @@ function formatInteractiveElements(elements) {
4867
5032
  const parts = [prefix];
4868
5033
  if (el.type === "button") {
4869
5034
  parts.push(`[${el.text || "Button"}]`);
4870
- parts.push("button");
5035
+ parts.push(el.role === "radio" ? "radio" : "button");
4871
5036
  } else if (el.type === "link") {
4872
5037
  parts.push(`[${el.text || "Link"}]`);
4873
5038
  parts.push("link");
@@ -4932,7 +5097,7 @@ function formatForms(forms) {
4932
5097
  ];
4933
5098
  if (field.type === "button") {
4934
5099
  fieldParts.push(`[${field.text || "Submit"}]`);
4935
- fieldParts.push("button");
5100
+ fieldParts.push(field.role === "radio" ? "radio" : "button");
4936
5101
  } else if (field.type === "input") {
4937
5102
  fieldParts.push(`[${field.label || field.placeholder || "Input"}]`);
4938
5103
  fieldParts.push(field.inputType || "text");
@@ -4979,18 +5144,51 @@ function formatLandmarks(landmarks) {
4979
5144
  function formatViewport(page) {
4980
5145
  return `${page.viewport.width}x${page.viewport.height} at scroll (${page.viewport.scrollX}, ${page.viewport.scrollY})`;
4981
5146
  }
4982
- function formatOverlays(overlays) {
4983
- if (overlays.length === 0) return "None detected";
4984
- const items = limitItems(overlays, 10);
5147
+ function formatOverlays(page) {
5148
+ if (page.overlays.length === 0) return "None detected";
5149
+ const items = limitItems(buildOverlayInventory(page), 10);
4985
5150
  return items.map((overlay) => {
4986
- const parts = [`- ${overlay.type}`];
4987
- if (overlay.role) parts.push(`role=${overlay.role}`);
4988
- if (overlay.blocksInteraction) parts.push("blocking");
4989
- if (overlay.label) parts.push(`label="${overlay.label.slice(0, 80)}"`);
4990
- if (overlay.text) parts.push(`text="${overlay.text.slice(0, 100)}"`);
4991
- return parts.join(" ");
5151
+ const lines = [
5152
+ [
5153
+ `- ${overlay.kind}`,
5154
+ overlay.role ? `role=${overlay.role}` : "",
5155
+ overlay.blocksInteraction ? "blocking" : "",
5156
+ overlay.label ? `label="${overlay.label.slice(0, 80)}"` : "",
5157
+ overlay.text ? `text="${overlay.text.slice(0, 100)}"` : ""
5158
+ ].filter(Boolean).join(" ")
5159
+ ];
5160
+ if (overlay.radioOptions.length > 0) {
5161
+ const options = overlay.radioOptions.slice(0, 4).map((option) => {
5162
+ const tags = [];
5163
+ if (option.labelSource) tags.push(`source=${option.labelSource}`);
5164
+ if (option.looksCorrect === true) tags.push("likely-correct");
5165
+ if (option.looksCorrect === false) tags.push("likely-wrong");
5166
+ const suffix = tags.length > 0 ? ` (${tags.join(", ")})` : "";
5167
+ return `${option.label || option.selector || "radio"}${suffix}`;
5168
+ }).join(" | ");
5169
+ lines.push(` options: ${options}`);
5170
+ }
5171
+ const actionLabels = [
5172
+ overlay.dismissAction?.label ? `dismiss="${overlay.dismissAction.label}"` : "",
5173
+ overlay.acceptAction?.label ? `accept="${overlay.acceptAction.label}"` : "",
5174
+ overlay.submitAction?.label ? `submit="${overlay.submitAction.label}"` : ""
5175
+ ].filter(Boolean);
5176
+ if (actionLabels.length > 0) {
5177
+ lines.push(` actions: ${actionLabels.join(" ")}`);
5178
+ }
5179
+ return lines.join("\n");
4992
5180
  }).join("\n");
4993
5181
  }
5182
+ function getScrollHints(page) {
5183
+ const candidates = page.interactiveElements.filter(
5184
+ (el) => el.visible !== false && el.inViewport === false && el.context !== "nav" && el.context !== "footer" && el.context !== "sidebar" && el.blockedByOverlay !== true && (el.type === "input" || el.type === "textarea" || el.type === "select" || el.type === "button")
5185
+ );
5186
+ if (candidates.length === 0) return [];
5187
+ const labels = limitItems(candidates, 3).map((el) => el.text || el.label || el.placeholder || el.type).filter(Boolean);
5188
+ return [
5189
+ `Scroll to reveal offscreen controls: ${labels.join(", ")}${candidates.length > labels.length ? ", ..." : ""}`
5190
+ ];
5191
+ }
4994
5192
  function formatDormantOverlays(overlays) {
4995
5193
  if (overlays.length === 0) return "None detected";
4996
5194
  const items = limitItems(overlays, 10);
@@ -5327,6 +5525,10 @@ function buildScopedContext(page, mode) {
5327
5525
  if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
5328
5526
  const largePageHint = formatLargePageHint(page);
5329
5527
  if (largePageHint) sections.push(`**Reading Hint:** ${largePageHint}`);
5528
+ const scrollHints = getScrollHints(page);
5529
+ if (scrollHints.length > 0) {
5530
+ sections.push(`**Scroll Hint:** ${scrollHints[0]}`);
5531
+ }
5330
5532
  sections.push("");
5331
5533
  const summaryIntent = analyzePageIntent(page);
5332
5534
  if (summaryIntent) {
@@ -5395,6 +5597,10 @@ function buildScopedContext(page, mode) {
5395
5597
  sections.push(`**URL:** ${page.url}`);
5396
5598
  sections.push(`**Title:** ${page.title}`);
5397
5599
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5600
+ const interactivesScrollHints = getScrollHints(page);
5601
+ if (interactivesScrollHints.length > 0) {
5602
+ sections.push(`**Scroll Hint:** ${interactivesScrollHints[0]}`);
5603
+ }
5398
5604
  sections.push("");
5399
5605
  const interactivesIntent = analyzePageIntent(page);
5400
5606
  if (interactivesIntent) {
@@ -5419,7 +5625,7 @@ function buildScopedContext(page, mode) {
5419
5625
  }
5420
5626
  if (page.overlays.length > 0) {
5421
5627
  sections.push("### Active Overlays");
5422
- sections.push(formatOverlays(page.overlays));
5628
+ sections.push(formatOverlays(page));
5423
5629
  sections.push("");
5424
5630
  }
5425
5631
  if (dialogFocus) {
@@ -5457,6 +5663,10 @@ function buildScopedContext(page, mode) {
5457
5663
  sections.push(`**URL:** ${page.url}`);
5458
5664
  sections.push(`**Title:** ${page.title}`);
5459
5665
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5666
+ const visibleScrollHints = getScrollHints(page);
5667
+ if (visibleScrollHints.length > 0) {
5668
+ sections.push(`**Scroll Hint:** ${visibleScrollHints[0]}`);
5669
+ }
5460
5670
  sections.push("");
5461
5671
  const formsHighlights = getHighlightsForPage(page.url);
5462
5672
  if (formsHighlights.length > 0) {
@@ -5476,7 +5686,7 @@ function buildScopedContext(page, mode) {
5476
5686
  }
5477
5687
  if (page.overlays.length > 0) {
5478
5688
  sections.push("### Active Overlays");
5479
- sections.push(formatOverlays(page.overlays));
5689
+ sections.push(formatOverlays(page));
5480
5690
  sections.push("");
5481
5691
  }
5482
5692
  if (page.dormantOverlays.length > 0) {
@@ -5559,7 +5769,7 @@ function buildScopedContext(page, mode) {
5559
5769
  }
5560
5770
  if (page.overlays.length > 0) {
5561
5771
  sections.push("### Active Overlays");
5562
- sections.push(formatOverlays(page.overlays));
5772
+ sections.push(formatOverlays(page));
5563
5773
  sections.push("");
5564
5774
  }
5565
5775
  if (dialogFocus) {
@@ -5744,6 +5954,10 @@ function buildStructuredContext(page) {
5744
5954
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5745
5955
  if (page.byline) sections.push(`**Author:** ${page.byline}`);
5746
5956
  if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
5957
+ const structuredScrollHints = getScrollHints(page);
5958
+ if (structuredScrollHints.length > 0) {
5959
+ sections.push(`**Scroll Hint:** ${structuredScrollHints[0]}`);
5960
+ }
5747
5961
  sections.push("");
5748
5962
  const pageIntent = analyzePageIntent(page);
5749
5963
  if (pageIntent) {
@@ -5782,7 +5996,7 @@ function buildStructuredContext(page) {
5782
5996
  sections.push(formatLandmarks(page.landmarks));
5783
5997
  sections.push("");
5784
5998
  sections.push("### Active Overlays / Modals");
5785
- sections.push(formatOverlays(page.overlays));
5999
+ sections.push(formatOverlays(page));
5786
6000
  sections.push("");
5787
6001
  sections.push("### Dormant Consent / Modal UI");
5788
6002
  sections.push(formatDormantOverlays(page.dormantOverlays));
@@ -5861,6 +6075,83 @@ function buildGeneralPrompt(query) {
5861
6075
  user: query
5862
6076
  };
5863
6077
  }
6078
+ const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
6079
+ function stripWrappingQuotes(value) {
6080
+ const trimmed = value.trim();
6081
+ if (trimmed.length < 2) return trimmed;
6082
+ const first = trimmed[0];
6083
+ const last = trimmed[trimmed.length - 1];
6084
+ if (first === last && WRAPPING_QUOTES.has(first)) {
6085
+ return trimmed.slice(1, -1).trim();
6086
+ }
6087
+ return trimmed;
6088
+ }
6089
+ function normalizeArrayItem(value) {
6090
+ if (typeof value === "string") {
6091
+ return stripWrappingQuotes(value).trim();
6092
+ }
6093
+ return String(value).trim();
6094
+ }
6095
+ function normalizeLooseString(value) {
6096
+ if (typeof value !== "string") return void 0;
6097
+ const normalized = stripWrappingQuotes(value);
6098
+ return normalized ? normalized : void 0;
6099
+ }
6100
+ function coerceOptionalNumber(value) {
6101
+ if (typeof value === "number" && Number.isFinite(value)) {
6102
+ return value;
6103
+ }
6104
+ const normalized = normalizeLooseString(value);
6105
+ if (!normalized) return void 0;
6106
+ const parsed = Number(normalized);
6107
+ return Number.isFinite(parsed) ? parsed : void 0;
6108
+ }
6109
+ function coerceStringArray(value) {
6110
+ if (Array.isArray(value)) {
6111
+ return value.map(normalizeArrayItem).filter(Boolean);
6112
+ }
6113
+ const normalized = normalizeLooseString(value);
6114
+ if (!normalized) return [];
6115
+ try {
6116
+ const parsed = JSON.parse(normalized);
6117
+ if (Array.isArray(parsed)) {
6118
+ return parsed.map(normalizeArrayItem).filter(Boolean);
6119
+ }
6120
+ } catch {
6121
+ }
6122
+ const lines = normalized.split(/\r?\n/).map((line) => line.replace(/^\s*[-*]\s+/, "").trim()).filter(Boolean);
6123
+ if (lines.length > 1) {
6124
+ return lines;
6125
+ }
6126
+ return [normalized];
6127
+ }
6128
+ function optionalNumberLikeSchema() {
6129
+ return zod.z.preprocess(
6130
+ (value) => {
6131
+ if (value == null) return void 0;
6132
+ return coerceOptionalNumber(value) ?? value;
6133
+ },
6134
+ zod.z.number().finite().optional()
6135
+ );
6136
+ }
6137
+ function normalizedOptionalStringSchema() {
6138
+ return zod.z.preprocess(
6139
+ (value) => {
6140
+ if (value == null) return void 0;
6141
+ return normalizeLooseString(value) ?? value;
6142
+ },
6143
+ zod.z.string().optional()
6144
+ );
6145
+ }
6146
+ function stringArrayLikeSchema() {
6147
+ return zod.z.preprocess(
6148
+ (value) => {
6149
+ if (value == null) return value;
6150
+ return coerceStringArray(value) ?? value;
6151
+ },
6152
+ zod.z.array(zod.z.string().min(1)).min(1)
6153
+ );
6154
+ }
5864
6155
  const TOOL_DEFINITIONS = [
5865
6156
  // --- Tab Management ---
5866
6157
  {
@@ -5989,7 +6280,9 @@ const TOOL_DEFINITIONS = [
5989
6280
  description: "Scroll the page up or down.",
5990
6281
  inputSchema: {
5991
6282
  direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
5992
- amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
6283
+ amount: optionalNumberLikeSchema().describe(
6284
+ "Pixels to scroll (default 500)"
6285
+ )
5993
6286
  },
5994
6287
  tier: 0,
5995
6288
  relevance: ["ARTICLE", "SEARCH_RESULTS", "PAGINATED_LIST"]
@@ -6034,6 +6327,17 @@ const TOOL_DEFINITIONS = [
6034
6327
  description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions.",
6035
6328
  tier: 1
6036
6329
  },
6330
+ {
6331
+ name: "clear_overlays",
6332
+ title: "Clear Overlays",
6333
+ description: "Work through blocking overlays and modals until the page is unblocked, using overlay-specific heuristics for consent banners and radio-selection dialogs.",
6334
+ inputSchema: {
6335
+ strategy: zod.z.enum(["auto", "interactive"]).optional().describe(
6336
+ 'How aggressively to clear overlays. "auto" uses heuristics; "interactive" stops earlier when human judgment may be needed.'
6337
+ )
6338
+ },
6339
+ tier: 1
6340
+ },
6037
6341
  {
6038
6342
  name: "inspect_element",
6039
6343
  title: "Inspect Element",
@@ -6233,7 +6537,9 @@ const TOOL_DEFINITIONS = [
6233
6537
  inputSchema: {
6234
6538
  index: zod.z.number().optional().describe("Element index from page content to highlight"),
6235
6539
  selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
6236
- text: zod.z.string().optional().describe("Text to find and highlight on the page (all occurrences)"),
6540
+ text: normalizedOptionalStringSchema().describe(
6541
+ "Text to find and highlight on the page (all occurrences)"
6542
+ ),
6237
6543
  label: zod.z.string().optional().describe("Annotation label to display near the highlight"),
6238
6544
  durationMs: zod.z.number().optional().describe(
6239
6545
  "Auto-clear after this many milliseconds (omit for permanent)"
@@ -6258,7 +6564,7 @@ const TOOL_DEFINITIONS = [
6258
6564
  goal: zod.z.string().describe(
6259
6565
  "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
6260
6566
  ),
6261
- steps: zod.z.array(zod.z.string()).describe(
6567
+ steps: stringArrayLikeSchema().describe(
6262
6568
  "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
6263
6569
  )
6264
6570
  },
@@ -6495,6 +6801,7 @@ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
6495
6801
  "search",
6496
6802
  "scroll",
6497
6803
  "dismiss_popup",
6804
+ "clear_overlays",
6498
6805
  "accept_cookies",
6499
6806
  "wait_for",
6500
6807
  "read_page",
@@ -6518,6 +6825,9 @@ function inferIntent(query) {
6518
6825
  }
6519
6826
  if (/\b(highlight|mark|annotate)\b/.test(lowered)) intents.add("highlight");
6520
6827
  if (/\b(table|csv|rows|columns)\b/.test(lowered)) intents.add("table");
6828
+ if (/\b(overlay|modal|popup|consent|cookie|blocking ui)\b/.test(lowered)) {
6829
+ intents.add("debug");
6830
+ }
6521
6831
  if (/\b(debug|diagnose|what should i do|stuck|inspect)\b/.test(lowered)) {
6522
6832
  intents.add("debug");
6523
6833
  }
@@ -8683,6 +8993,93 @@ async function dismissPopup$1(wc) {
8683
8993
  }
8684
8994
  return initialBlocking > 0 ? "Could not dismiss the blocking popup automatically" : initialDormant > 0 ? `No active blocking popup detected. Found ${initialDormant} dormant consent/modal surface(s) in the DOM, likely geo-gated or inactive in this session.` : "No blocking popup detected";
8685
8995
  }
8996
+ function describeOverlayState(page) {
8997
+ const inventory = buildOverlayInventory(page);
8998
+ return {
8999
+ inventory,
9000
+ blocking: inventory.filter((overlay) => overlay.blocksInteraction).length,
9001
+ total: inventory.length,
9002
+ signature: getBlockingOverlaySignature(inventory)
9003
+ };
9004
+ }
9005
+ async function clickOverlayCandidate(wc, action) {
9006
+ if (!action?.selector) return null;
9007
+ const result = await clickResolvedSelector$1(wc, action.selector);
9008
+ return `${action.label || action.selector}: ${result}`;
9009
+ }
9010
+ async function clearOverlays(wc, strategy = "auto") {
9011
+ const steps = [];
9012
+ let cleared = 0;
9013
+ const maxIterations = 8;
9014
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
9015
+ const before = await extractContent(wc);
9016
+ const beforeState = describeOverlayState(before);
9017
+ const blockingOverlays = beforeState.inventory.filter(
9018
+ (overlay2) => overlay2.blocksInteraction
9019
+ );
9020
+ if (blockingOverlays.length === 0) {
9021
+ if (cleared === 0) return "No blocking overlays detected";
9022
+ steps.push(`Overlays remaining: ${beforeState.total}`);
9023
+ steps.push("Page still blocked: false");
9024
+ return steps.join("\n");
9025
+ }
9026
+ const overlay = blockingOverlays[0];
9027
+ let actionMessage = null;
9028
+ if (overlay.kind === "cookie_consent") {
9029
+ actionMessage = await clickOverlayCandidate(
9030
+ wc,
9031
+ overlay.acceptAction || overlay.dismissAction || overlay.actions[0]
9032
+ );
9033
+ } else if (overlay.kind === "selection_modal") {
9034
+ if (!overlay.correctOption?.selector) {
9035
+ if (strategy === "interactive") {
9036
+ steps.push(
9037
+ "Stopped: selection modal needs human judgment because no likely-correct option was detected."
9038
+ );
9039
+ steps.push(`Overlays remaining: ${beforeState.total}`);
9040
+ steps.push("Page still blocked: true");
9041
+ return steps.join("\n");
9042
+ }
9043
+ } else {
9044
+ const optionResult = await clickOverlayCandidate(
9045
+ wc,
9046
+ overlay.correctOption
9047
+ );
9048
+ if (optionResult) {
9049
+ actionMessage = `Selected likely-correct option: ${optionResult}`;
9050
+ await sleep$1(120);
9051
+ const submitResult = await clickOverlayCandidate(
9052
+ wc,
9053
+ overlay.submitAction || overlay.acceptAction
9054
+ );
9055
+ if (submitResult) {
9056
+ actionMessage += `
9057
+ Submitted modal: ${submitResult}`;
9058
+ }
9059
+ }
9060
+ }
9061
+ }
9062
+ if (!actionMessage) {
9063
+ actionMessage = `Fallback popup handling: ${await dismissPopup$1(wc)}`;
9064
+ }
9065
+ steps.push(actionMessage);
9066
+ await sleep$1(250);
9067
+ const after = await extractContent(wc);
9068
+ const afterState = describeOverlayState(after);
9069
+ steps.push(`Overlays remaining: ${afterState.total}`);
9070
+ steps.push(`Page still blocked: ${afterState.blocking > 0}`);
9071
+ if (afterState.blocking === 0) {
9072
+ return steps.join("\n");
9073
+ }
9074
+ const progressMade = afterState.blocking < beforeState.blocking || afterState.total !== beforeState.total || afterState.signature !== beforeState.signature;
9075
+ if (progressMade) {
9076
+ cleared += 1;
9077
+ continue;
9078
+ }
9079
+ return steps.join("\n");
9080
+ }
9081
+ return steps.join("\n");
9082
+ }
8686
9083
  async function resolveSelector$1(wc, index, selector) {
8687
9084
  if (selector) return selector;
8688
9085
  if (index == null) return null;
@@ -9589,7 +9986,8 @@ async function getPostActionState$1(ctx, name) {
9589
9986
  "hover",
9590
9987
  "focus",
9591
9988
  "fill_form",
9592
- "inspect_element"
9989
+ "inspect_element",
9990
+ "clear_overlays"
9593
9991
  ];
9594
9992
  const tabActions = [
9595
9993
  "create_tab",
@@ -9639,6 +10037,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
9639
10037
  "focus",
9640
10038
  "set_ad_blocking",
9641
10039
  "dismiss_popup",
10040
+ "clear_overlays",
9642
10041
  "read_page",
9643
10042
  "wait_for",
9644
10043
  "create_checkpoint",
@@ -9866,7 +10265,7 @@ async function executeAction(name, args, ctx) {
9866
10265
  }
9867
10266
  case "scroll": {
9868
10267
  if (!wc) return "Error: No active tab";
9869
- const pixels = args.amount || 500;
10268
+ const pixels = coerceOptionalNumber(args.amount) ?? 500;
9870
10269
  const dir = args.direction === "up" ? -pixels : pixels;
9871
10270
  const result2 = await scrollPage$1(wc, dir);
9872
10271
  return `Scrolled ${args.direction} by ${pixels}px (moved ${Math.abs(result2.movedY)}px, now at y=${Math.round(result2.afterY)})`;
@@ -9911,6 +10310,11 @@ async function executeAction(name, args, ctx) {
9911
10310
  if (!wc) return "Error: No active tab";
9912
10311
  return dismissPopup$1(wc);
9913
10312
  }
10313
+ case "clear_overlays": {
10314
+ if (!wc) return "Error: No active tab";
10315
+ const strategy = args.strategy === "interactive" ? "interactive" : "auto";
10316
+ return clearOverlays(wc, strategy);
10317
+ }
9914
10318
  case "read_page": {
9915
10319
  if (!wc) return "Error: No active tab";
9916
10320
  console.log("[Vessel read_page] starting extraction with 6s timeout");
@@ -10277,9 +10681,9 @@ ${truncated}`;
10277
10681
  if (!wc) return "Error: No active tab";
10278
10682
  const selector = await resolveSelector$1(wc, args.index, args.selector);
10279
10683
  const highlightColor = args.color || "yellow";
10684
+ const highlightText = normalizeLooseString(args.text);
10280
10685
  const url = wc.getURL();
10281
10686
  if (url && url !== "about:blank") {
10282
- const highlightText = typeof args.text === "string" ? args.text : void 0;
10283
10687
  addHighlight(
10284
10688
  url,
10285
10689
  typeof selector === "string" ? selector : void 0,
@@ -10292,7 +10696,7 @@ ${truncated}`;
10292
10696
  return highlightOnPage(
10293
10697
  wc,
10294
10698
  selector,
10295
- args.text,
10699
+ highlightText,
10296
10700
  args.label,
10297
10701
  args.durationMs,
10298
10702
  highlightColor
@@ -10305,7 +10709,7 @@ ${truncated}`;
10305
10709
  // --- Speedee System ---
10306
10710
  case "flow_start": {
10307
10711
  const goal = typeof args.goal === "string" ? args.goal : "";
10308
- const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
10712
+ const steps = coerceStringArray(args.steps) ?? [];
10309
10713
  if (!goal || steps.length === 0)
10310
10714
  return "Error: goal and steps are required";
10311
10715
  const flow = ctx.runtime.startFlow(goal, steps, wc?.getURL());
@@ -10365,9 +10769,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
10365
10769
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
10366
10770
  if (hasOverlays) {
10367
10771
  suggestions.push("BLOCKING OVERLAY detected — dismiss it first:");
10368
- suggestions.push(
10369
- " → dismiss_popup or click on close/accept button"
10370
- );
10772
+ suggestions.push(" → clear_overlays for stacked modals");
10773
+ suggestions.push(" → or dismiss_popup for a single popup");
10371
10774
  suggestions.push("");
10372
10775
  }
10373
10776
  if (hasPasswordField) {
@@ -10931,6 +11334,8 @@ Instructions:
10931
11334
  - When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
10932
11335
  - Escalate page reads progressively: read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
10933
11336
  - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
11337
+ - VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
11338
+ - read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
10934
11339
  - After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
10935
11340
  - If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.
10936
11341
  - If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.
@@ -13995,7 +14400,9 @@ ${buildScopedContext(pageContent, mode)}`;
13995
14400
  description: "Scroll the page up or down.",
13996
14401
  inputSchema: {
13997
14402
  direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
13998
- amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
14403
+ amount: optionalNumberLikeSchema().describe(
14404
+ "Pixels to scroll (default 500)"
14405
+ )
13999
14406
  }
14000
14407
  },
14001
14408
  async ({ direction, amount }) => {
@@ -14007,7 +14414,7 @@ ${buildScopedContext(pageContent, mode)}`;
14007
14414
  "scroll",
14008
14415
  { direction, amount },
14009
14416
  async () => {
14010
- const pixels = amount || 500;
14417
+ const pixels = coerceOptionalNumber(amount) ?? 500;
14011
14418
  const dir = direction === "up" ? -pixels : pixels;
14012
14419
  const result = await scrollPage(tab.view.webContents, dir);
14013
14420
  return `Scrolled ${direction} by ${pixels}px (moved ${Math.abs(result.movedY)}px, now at y=${Math.round(result.afterY)})`;
@@ -14033,6 +14440,32 @@ ${buildScopedContext(pageContent, mode)}`;
14033
14440
  );
14034
14441
  }
14035
14442
  );
14443
+ server.registerTool(
14444
+ "vessel_clear_overlays",
14445
+ {
14446
+ title: "Clear Overlays",
14447
+ description: "Work through blocking overlays and modals until the page is unblocked, using overlay-specific heuristics for consent banners and radio-selection dialogs.",
14448
+ inputSchema: {
14449
+ strategy: zod.z.enum(["auto", "interactive"]).optional().describe(
14450
+ 'How aggressively to clear overlays. "auto" uses heuristics; "interactive" stops earlier when human judgment may be needed.'
14451
+ )
14452
+ }
14453
+ },
14454
+ async ({ strategy }) => {
14455
+ const tab = tabManager.getActiveTab();
14456
+ if (!tab) return asTextResponse("Error: No active tab");
14457
+ return withAction(
14458
+ runtime,
14459
+ tabManager,
14460
+ "clear_overlays",
14461
+ { strategy: strategy || "auto" },
14462
+ async () => clearOverlays(
14463
+ tab.view.webContents,
14464
+ strategy === "interactive" ? "interactive" : "auto"
14465
+ )
14466
+ );
14467
+ }
14468
+ );
14036
14469
  server.registerTool(
14037
14470
  "vessel_wait_for",
14038
14471
  {
@@ -14332,7 +14765,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14332
14765
  inputSchema: {
14333
14766
  index: zod.z.number().optional().describe("Element index from extracted content to highlight"),
14334
14767
  selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
14335
- text: zod.z.string().optional().describe(
14768
+ text: normalizedOptionalStringSchema().describe(
14336
14769
  "Text to find and highlight on the page (highlights all occurrences)"
14337
14770
  ),
14338
14771
  label: zod.z.string().optional().describe("Optional annotation label to display near the highlight"),
@@ -14350,18 +14783,27 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14350
14783
  async ({ index, selector, text, label, durationMs, persist, color }) => {
14351
14784
  const tab = tabManager.getActiveTab();
14352
14785
  if (!tab) return asTextResponse("Error: No active tab");
14786
+ const normalizedText = normalizeLooseString(text);
14353
14787
  return withAction(
14354
14788
  runtime,
14355
14789
  tabManager,
14356
14790
  "highlight",
14357
- { index, selector, text, label, durationMs, persist, color },
14791
+ {
14792
+ index,
14793
+ selector,
14794
+ text: normalizedText,
14795
+ label,
14796
+ durationMs,
14797
+ persist,
14798
+ color
14799
+ },
14358
14800
  async () => {
14359
14801
  const wc = tab.view.webContents;
14360
14802
  const resolvedSelector = await resolveSelector(wc, index, selector);
14361
14803
  const result = await highlightOnPage(
14362
14804
  wc,
14363
14805
  resolvedSelector,
14364
- text,
14806
+ normalizedText,
14365
14807
  label,
14366
14808
  durationMs,
14367
14809
  color
@@ -14371,7 +14813,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14371
14813
  addHighlight(
14372
14814
  url,
14373
14815
  resolvedSelector ?? void 0,
14374
- text,
14816
+ normalizedText,
14375
14817
  label,
14376
14818
  color,
14377
14819
  "agent"
@@ -15243,16 +15685,17 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15243
15685
  goal: zod.z.string().describe(
15244
15686
  "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
15245
15687
  ),
15246
- steps: zod.z.array(zod.z.string()).describe(
15688
+ steps: stringArrayLikeSchema().describe(
15247
15689
  "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
15248
15690
  )
15249
15691
  }
15250
15692
  },
15251
15693
  async ({ goal, steps }) => {
15694
+ const normalizedSteps = coerceStringArray(steps) ?? [];
15252
15695
  const tab = tabManager.getActiveTab();
15253
15696
  const flow = runtime.startFlow(
15254
15697
  goal,
15255
- steps,
15698
+ normalizedSteps,
15256
15699
  tab?.view.webContents.getURL()
15257
15700
  );
15258
15701
  return asTextResponse(
@@ -15348,9 +15791,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15348
15791
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
15349
15792
  if (hasOverlays) {
15350
15793
  suggestions.push("⚠ BLOCKING OVERLAY detected — dismiss it first:");
15351
- suggestions.push(
15352
- " → vessel_dismiss_popup or vessel_click on close/accept button"
15353
- );
15794
+ suggestions.push(" → vessel_clear_overlays for stacked modals");
15795
+ suggestions.push(" → or vessel_dismiss_popup for a single popup");
15354
15796
  suggestions.push("");
15355
15797
  }
15356
15798
  if (hasPasswordField) {