@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 {
|
|
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
|
|
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
|
|
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
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9149
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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)
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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 }) => ({
|
|
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(
|
|
11689
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
12671
|
-
|
|
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(
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
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(
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
13091
|
-
|
|
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(
|
|
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(
|
|
13221
|
-
|
|
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(
|
|
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(
|
|
13579
|
-
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
13677
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
13737
|
-
|
|
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 =
|
|
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(
|
|
13774
|
-
|
|
13775
|
-
|
|
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 ({
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
"
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
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
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
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
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
13890
|
-
|
|
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(
|
|
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)
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
14139
|
-
|
|
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(
|
|
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
|
}
|