@quanta-intellect/vessel-browser 0.1.140 → 0.1.141
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 +671 -120
- package/out/preload/content-script.js +76 -4
- package/package.json +1 -1
package/out/main/index.js
CHANGED
|
@@ -5589,15 +5589,12 @@ const DEFAULT_SELECTOR_ATTRIBUTES = [
|
|
|
5589
5589
|
];
|
|
5590
5590
|
function selectorHelpersJS(attributes = DEFAULT_SELECTOR_ATTRIBUTES) {
|
|
5591
5591
|
const attrsExpr = JSON.stringify(attributes);
|
|
5592
|
-
const q = '"';
|
|
5593
|
-
const bs = "\\";
|
|
5594
|
-
const escQ = bs + q;
|
|
5595
5592
|
return [
|
|
5596
5593
|
"function __escapeSelectorValue(value) {",
|
|
5597
5594
|
' if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {',
|
|
5598
5595
|
" return CSS.escape(value);",
|
|
5599
5596
|
" }",
|
|
5600
|
-
|
|
5597
|
+
' return String(value).replace(/["\\\\]/g, "\\\\$&");',
|
|
5601
5598
|
"}",
|
|
5602
5599
|
"",
|
|
5603
5600
|
"function __uniqueSelector(candidate) {",
|
|
@@ -5613,7 +5610,7 @@ function selectorHelpersJS(attributes = DEFAULT_SELECTOR_ATTRIBUTES) {
|
|
|
5613
5610
|
' var value = (el.getAttribute && el.getAttribute(attribute)) || "";',
|
|
5614
5611
|
" value = String(value).trim();",
|
|
5615
5612
|
" if (!value) return null;",
|
|
5616
|
-
|
|
5613
|
+
' var candidate = el.tagName.toLowerCase() + "[" + attribute + "=\\"" + __escapeSelectorValue(value) + "\\"]";',
|
|
5617
5614
|
" return __uniqueSelector(candidate);",
|
|
5618
5615
|
"}",
|
|
5619
5616
|
"",
|
|
@@ -8754,6 +8751,42 @@ function recoverNarratedActionToolCalls(text, availableToolNames) {
|
|
|
8754
8751
|
}
|
|
8755
8752
|
return recovered;
|
|
8756
8753
|
}
|
|
8754
|
+
const DOLLAR_PRICE_RE = /\$\s?\d{2,4}(?:[,.]\d{2})?\b/;
|
|
8755
|
+
const FLIGHT_TASK_RE = /\b(?:flight|flights|airfare|air fare|plane ticket|airline|airport|google flights|pdx|sfo|san francisco|portland)\b/i;
|
|
8756
|
+
const SHOPPING_INTENT_RE = /\b(?:cheap|cheapest|price|prices|fare|fares|one[- ]?way|round[- ]?trip|depart|departure|arrive|arrival|from|to)\b/i;
|
|
8757
|
+
const FLIGHT_CLAIM_CONTEXT_RE = /\b(?:flight|flights|airline|airlines|departure|arrival|depart|arrive|nonstop|non-stop|stops?|duration|alaska|united|delta|american|southwest|frontier|spirit|jetblue|hawaiian)\b/i;
|
|
8758
|
+
const FLIGHT_RESULT_CONTEXT_RE = /\b(?:best departing flights|departing flights|returning flights|flight results|google flights|airline|airlines|duration|nonstop|non-stop|stops?|departure|arrival|depart|arrive|price|fare|fares|alaska|united|delta|american|southwest|frontier|spirit|jetblue|hawaiian)\b/i;
|
|
8759
|
+
const EMPTY_FORM_CONTEXT_RE = /\b(?:where to\?|search airports or cities|destination|from\?|departure date|calendar|google flights)\b/i;
|
|
8760
|
+
const TOOL_FAILURE_RE = /\b(?:error|failed|type-not-applied|did not type|could not type|not applied|unsupported|invalid args)\b/i;
|
|
8761
|
+
function normalizeEvidenceText(text) {
|
|
8762
|
+
return (text || "").replace(/\s+/g, " ").trim();
|
|
8763
|
+
}
|
|
8764
|
+
function looksLikeFlightShoppingTask(userMessage) {
|
|
8765
|
+
return FLIGHT_TASK_RE.test(userMessage) && SHOPPING_INTENT_RE.test(userMessage);
|
|
8766
|
+
}
|
|
8767
|
+
function answerIncludesFlightPriceClaims(assistantText) {
|
|
8768
|
+
return DOLLAR_PRICE_RE.test(assistantText) && FLIGHT_CLAIM_CONTEXT_RE.test(assistantText);
|
|
8769
|
+
}
|
|
8770
|
+
function toolResultHasFlightPriceEvidence(latestToolResultPreview) {
|
|
8771
|
+
const text = normalizeEvidenceText(latestToolResultPreview);
|
|
8772
|
+
if (!text) return false;
|
|
8773
|
+
if (TOOL_FAILURE_RE.test(text) && !DOLLAR_PRICE_RE.test(text)) return false;
|
|
8774
|
+
return DOLLAR_PRICE_RE.test(text) && FLIGHT_RESULT_CONTEXT_RE.test(text);
|
|
8775
|
+
}
|
|
8776
|
+
function shouldBlockUnsupportedFlightPriceAnswer(userMessage, assistantText, latestToolResultPreview) {
|
|
8777
|
+
return looksLikeFlightShoppingTask(userMessage) && answerIncludesFlightPriceClaims(assistantText) && !toolResultHasFlightPriceEvidence(latestToolResultPreview);
|
|
8778
|
+
}
|
|
8779
|
+
function buildFlightPriceEvidenceRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
|
|
8780
|
+
const latest = normalizeEvidenceText(latestToolResultPreview);
|
|
8781
|
+
const maybeFormState = latest && EMPTY_FORM_CONTEXT_RE.test(latest) ? " The latest page state still looks like a flight search form, so verify the destination, route, and date before reading results." : "";
|
|
8782
|
+
return [
|
|
8783
|
+
`The user asked for live flight prices: ${userMessage}`,
|
|
8784
|
+
`Your last answer included specific flight prices, but the latest browser/tool evidence does not show visible flight-result rows with prices.`,
|
|
8785
|
+
`Erase that answer and continue with browser tools until the current page evidence shows the route/date and visible priced flight results.${maybeFormState}`,
|
|
8786
|
+
`Do not report airline names, times, or prices unless they are visible in the latest browser/page evidence.`,
|
|
8787
|
+
`Last unsupported answer: ${assistantText.replace(/\s+/g, " ").trim().slice(0, 500) || "(empty)"}`
|
|
8788
|
+
].join("\n");
|
|
8789
|
+
}
|
|
8757
8790
|
const logger$v = createLogger("OpenAIProvider");
|
|
8758
8791
|
function shouldDebugAgentLoop() {
|
|
8759
8792
|
const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
|
|
@@ -9137,6 +9170,7 @@ class OpenAICompatProvider {
|
|
|
9137
9170
|
const maxIterations = getEffectiveMaxIterations();
|
|
9138
9171
|
let iterationsUsed = 0;
|
|
9139
9172
|
let compactRecoveryCount = 0;
|
|
9173
|
+
let flightPriceEvidenceRecoveryCount = 0;
|
|
9140
9174
|
let compactCorrectionCount = 0;
|
|
9141
9175
|
const recentCompactToolSignatures = [];
|
|
9142
9176
|
const recentToolNames = [];
|
|
@@ -9273,6 +9307,24 @@ class OpenAICompatProvider {
|
|
|
9273
9307
|
};
|
|
9274
9308
|
messages.push(assistantMsg);
|
|
9275
9309
|
if (toolCalls.length === 0) {
|
|
9310
|
+
const latestToolResultContent = latestToolMessage ? String(latestToolMessage.content || "") : null;
|
|
9311
|
+
if (flightPriceEvidenceRecoveryCount < 2 && shouldBlockUnsupportedFlightPriceAnswer(
|
|
9312
|
+
userMessage,
|
|
9313
|
+
textAccum,
|
|
9314
|
+
latestToolResultContent
|
|
9315
|
+
)) {
|
|
9316
|
+
flightPriceEvidenceRecoveryCount += 1;
|
|
9317
|
+
if (textAccum.trim()) onChunk("<<erase_prev>>");
|
|
9318
|
+
messages.push({
|
|
9319
|
+
role: "user",
|
|
9320
|
+
content: `[System] ${buildFlightPriceEvidenceRecoveryPrompt(
|
|
9321
|
+
userMessage,
|
|
9322
|
+
textAccum,
|
|
9323
|
+
latestToolResultContent
|
|
9324
|
+
)}`
|
|
9325
|
+
});
|
|
9326
|
+
continue;
|
|
9327
|
+
}
|
|
9276
9328
|
if (compactRecoveryCount < 2 && shouldRetryCompactToolLoop(
|
|
9277
9329
|
this.agentToolProfile,
|
|
9278
9330
|
textAccum,
|
|
@@ -9285,7 +9337,7 @@ class OpenAICompatProvider {
|
|
|
9285
9337
|
content: `[System] ${buildCompactRecoveryPrompt(
|
|
9286
9338
|
userMessage,
|
|
9287
9339
|
textAccum,
|
|
9288
|
-
|
|
9340
|
+
latestToolResultContent
|
|
9289
9341
|
)}`
|
|
9290
9342
|
});
|
|
9291
9343
|
continue;
|
|
@@ -9293,6 +9345,7 @@ class OpenAICompatProvider {
|
|
|
9293
9345
|
break;
|
|
9294
9346
|
}
|
|
9295
9347
|
compactRecoveryCount = 0;
|
|
9348
|
+
flightPriceEvidenceRecoveryCount = 0;
|
|
9296
9349
|
const iterationToolResultPreviews = [];
|
|
9297
9350
|
for (const tc of toolCalls) {
|
|
9298
9351
|
if (!availableToolNames.has(tc.name)) {
|
|
@@ -9837,10 +9890,17 @@ function summarizeToolArg(args) {
|
|
|
9837
9890
|
const index = typeof args.index === "number" ? `#${args.index}` : typeof args.index === "string" && args.index.trim() ? `#${args.index.trim()}` : "";
|
|
9838
9891
|
return [args.url, args.query, args.text, args.selector, index, args.direction].map((value) => typeof value === "string" ? value : "").find((value) => value.length > 0) ?? "";
|
|
9839
9892
|
}
|
|
9893
|
+
function normalizeCodexText(text) {
|
|
9894
|
+
return text.trim().toLowerCase().replace(/[‘’]/g, "'").replace(/[“”]/g, '"');
|
|
9895
|
+
}
|
|
9840
9896
|
function looksLikeFailedToolOutput(output) {
|
|
9841
|
-
const normalized = output
|
|
9897
|
+
const normalized = normalizeCodexText(output);
|
|
9842
9898
|
return normalized.startsWith("error") || normalized.startsWith("warning") || normalized.startsWith("target") || normalized.startsWith("no active tab") || normalized.includes("same page — results may have loaded dynamically") || normalized.includes("could not ") || normalized.includes("did not ");
|
|
9843
9899
|
}
|
|
9900
|
+
function looksLikeTravelFareContext(text) {
|
|
9901
|
+
const normalized = normalizeCodexText(text);
|
|
9902
|
+
return /\b(?:flight|flights|fare|fares|airline|airlines|one-way|roundtrip|nonstop|layover|departure|arrival)\b/.test(normalized) || /\b(?:pdx|sfo|oak|sjc)\b/.test(normalized) || /\$[\d,]+/.test(normalized);
|
|
9903
|
+
}
|
|
9844
9904
|
function emitCodexToolChunk(onChunk, name, args, output) {
|
|
9845
9905
|
const summary = summarizeToolArg(args);
|
|
9846
9906
|
const argSummary = looksLikeFailedToolOutput(output) ? ["⚠ failed", summary].filter(Boolean).join(" ") : summary;
|
|
@@ -10056,7 +10116,7 @@ const REAL_PROGRESS_TOOLS = /* @__PURE__ */ new Set([
|
|
|
10056
10116
|
]);
|
|
10057
10117
|
function shouldRetryCodexToolLoop(text, hasToolHistory) {
|
|
10058
10118
|
if (!hasToolHistory) return false;
|
|
10059
|
-
const trimmed = text
|
|
10119
|
+
const trimmed = normalizeCodexText(text);
|
|
10060
10120
|
if (!trimmed) return true;
|
|
10061
10121
|
if (trimmed.length <= 160 && trimmed.includes("?")) return true;
|
|
10062
10122
|
const askingUserForDirection = [
|
|
@@ -10072,6 +10132,26 @@ function shouldRetryCodexToolLoop(text, hasToolHistory) {
|
|
|
10072
10132
|
if (askingUserForDirection.some((signal) => trimmed.includes(signal))) {
|
|
10073
10133
|
return true;
|
|
10074
10134
|
}
|
|
10135
|
+
const askingUserToContinue = [
|
|
10136
|
+
"please let me",
|
|
10137
|
+
"let me inspect",
|
|
10138
|
+
"let me click",
|
|
10139
|
+
"need to continue from",
|
|
10140
|
+
"i need to continue",
|
|
10141
|
+
"allow me to",
|
|
10142
|
+
"can't access or reuse",
|
|
10143
|
+
"cant access or reuse",
|
|
10144
|
+
"can't access",
|
|
10145
|
+
"cant access",
|
|
10146
|
+
"visible context",
|
|
10147
|
+
"surface it",
|
|
10148
|
+
"send me the current",
|
|
10149
|
+
"send me the results",
|
|
10150
|
+
"send me the pricing"
|
|
10151
|
+
];
|
|
10152
|
+
if (askingUserToContinue.some((signal) => trimmed.includes(signal))) {
|
|
10153
|
+
return true;
|
|
10154
|
+
}
|
|
10075
10155
|
const cannotProceed = [
|
|
10076
10156
|
"i cannot",
|
|
10077
10157
|
"i can't",
|
|
@@ -10105,12 +10185,39 @@ function buildCodexRecoveryInput(userMessage, assistantText, latestToolResultPre
|
|
|
10105
10185
|
content: [{ type: "input_text", text: lines.join("\n") }]
|
|
10106
10186
|
};
|
|
10107
10187
|
}
|
|
10108
|
-
function
|
|
10188
|
+
function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText, latestToolResultPreview) {
|
|
10189
|
+
return {
|
|
10190
|
+
type: "message",
|
|
10191
|
+
role: "user",
|
|
10192
|
+
content: [
|
|
10193
|
+
{
|
|
10194
|
+
type: "input_text",
|
|
10195
|
+
text: `[System] ${buildFlightPriceEvidenceRecoveryPrompt(
|
|
10196
|
+
userMessage,
|
|
10197
|
+
assistantText,
|
|
10198
|
+
latestToolResultPreview
|
|
10199
|
+
)}`
|
|
10200
|
+
}
|
|
10201
|
+
]
|
|
10202
|
+
};
|
|
10203
|
+
}
|
|
10204
|
+
function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
|
|
10109
10205
|
const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
|
|
10110
10206
|
const lines = [
|
|
10111
10207
|
`[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`,
|
|
10112
|
-
`Take the next step: try a different target, refresh the page state with read_page,
|
|
10208
|
+
`Take the next step yourself: try a different target, refresh the page state with read_page, call inspect_element on the intended element, or answer from the results already visible in the conversation. Do not ask the user to inspect or click the result for you.`
|
|
10113
10209
|
];
|
|
10210
|
+
if (failedClickCount >= 2) {
|
|
10211
|
+
lines.push(
|
|
10212
|
+
`You have already had multiple failed clicks without making page progress. Do not keep clicking similar search result titles. Use the latest read_page/search result text to answer, or inspect a specific indexed result/control only if essential.`
|
|
10213
|
+
);
|
|
10214
|
+
}
|
|
10215
|
+
if (looksLikeTravelFareContext(`${attemptedTarget}
|
|
10216
|
+
${latestToolResultPreview || ""}`)) {
|
|
10217
|
+
lines.push(
|
|
10218
|
+
`For flight-price tasks, visible fare snippets are enough to compare candidates. If the results show prices, stops, airlines, or route names, report the cheapest visible option with caveats instead of trying to open every aggregator result.`
|
|
10219
|
+
);
|
|
10220
|
+
}
|
|
10114
10221
|
if (stateReminder) {
|
|
10115
10222
|
lines.push(stateReminder);
|
|
10116
10223
|
}
|
|
@@ -10307,11 +10414,13 @@ class CodexProvider {
|
|
|
10307
10414
|
let turnState = null;
|
|
10308
10415
|
let toolHistoryCount = 0;
|
|
10309
10416
|
let recoveryCount = 0;
|
|
10417
|
+
let flightPriceEvidenceRecoveryCount = 0;
|
|
10310
10418
|
let correctionCount = 0;
|
|
10311
10419
|
const recentToolSignatures = [];
|
|
10312
10420
|
const recentToolNames = [];
|
|
10313
10421
|
let clickReadLoopNudged = false;
|
|
10314
10422
|
let latestToolResultPreview = null;
|
|
10423
|
+
let failedClickCountSinceProgress = 0;
|
|
10315
10424
|
const recentSuccessfulSearchQueries = [];
|
|
10316
10425
|
const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
|
|
10317
10426
|
let lastSuccessfulWebSearchQuery = null;
|
|
@@ -10351,6 +10460,22 @@ class CodexProvider {
|
|
|
10351
10460
|
}
|
|
10352
10461
|
}
|
|
10353
10462
|
if (functionCalls.length === 0) {
|
|
10463
|
+
if (flightPriceEvidenceRecoveryCount < 2 && shouldBlockUnsupportedFlightPriceAnswer(
|
|
10464
|
+
userMessage,
|
|
10465
|
+
result.text,
|
|
10466
|
+
latestToolResultPreview
|
|
10467
|
+
)) {
|
|
10468
|
+
flightPriceEvidenceRecoveryCount += 1;
|
|
10469
|
+
if (result.text.trim()) onChunk("<<erase_prev>>");
|
|
10470
|
+
currentInput = [
|
|
10471
|
+
buildCodexFlightPriceEvidenceRecoveryInput(
|
|
10472
|
+
userMessage,
|
|
10473
|
+
result.text,
|
|
10474
|
+
latestToolResultPreview
|
|
10475
|
+
)
|
|
10476
|
+
];
|
|
10477
|
+
continue;
|
|
10478
|
+
}
|
|
10354
10479
|
if (recoveryCount < 1 && shouldRetryCodexToolLoop(result.text, toolHistoryCount > 0)) {
|
|
10355
10480
|
recoveryCount += 1;
|
|
10356
10481
|
if (result.text.trim()) onChunk("<<erase_prev>>");
|
|
@@ -10366,6 +10491,7 @@ class CodexProvider {
|
|
|
10366
10491
|
break;
|
|
10367
10492
|
}
|
|
10368
10493
|
recoveryCount = 0;
|
|
10494
|
+
flightPriceEvidenceRecoveryCount = 0;
|
|
10369
10495
|
currentInput = [];
|
|
10370
10496
|
for (const fc of functionCalls) {
|
|
10371
10497
|
const functionCallInput = createCodexFunctionCallInput(fc);
|
|
@@ -10467,6 +10593,7 @@ ${latestToolResultPreview || ""}`
|
|
|
10467
10593
|
if (!looksLikeFailedToolOutput(outputText)) {
|
|
10468
10594
|
if (isRealProgressTool(prepared.prepared.name)) {
|
|
10469
10595
|
lastSuccessfulWebSearchQuery = null;
|
|
10596
|
+
failedClickCountSinceProgress = 0;
|
|
10470
10597
|
}
|
|
10471
10598
|
}
|
|
10472
10599
|
if (searchToolQuery && !looksLikeFailedToolOutput(outputText) && !recentSuccessfulSearchQueries.includes(searchToolQuery)) {
|
|
@@ -10488,10 +10615,12 @@ ${latestToolResultPreview || ""}`
|
|
|
10488
10615
|
}
|
|
10489
10616
|
}
|
|
10490
10617
|
if (prepared.prepared.name === "click" && looksLikeFailedToolOutput(outputText)) {
|
|
10618
|
+
failedClickCountSinceProgress += 1;
|
|
10491
10619
|
currentInput.push(
|
|
10492
10620
|
buildCodexFailedClickRecoveryInput(
|
|
10493
10621
|
summarizeToolArg(prepared.prepared.args),
|
|
10494
|
-
latestToolResultPreview
|
|
10622
|
+
latestToolResultPreview,
|
|
10623
|
+
failedClickCountSinceProgress
|
|
10495
10624
|
)
|
|
10496
10625
|
);
|
|
10497
10626
|
}
|
|
@@ -12195,6 +12324,12 @@ function formatElementMeta(el) {
|
|
|
12195
12324
|
if (el.checked !== void 0) {
|
|
12196
12325
|
meta.push(el.checked ? "checked" : "unchecked");
|
|
12197
12326
|
}
|
|
12327
|
+
if (el.focused) {
|
|
12328
|
+
meta.push("focused");
|
|
12329
|
+
}
|
|
12330
|
+
if (el.hasValue && !shouldRenderFieldValue(el)) {
|
|
12331
|
+
meta.push("has-value");
|
|
12332
|
+
}
|
|
12198
12333
|
if (el.ariaExpanded !== void 0) {
|
|
12199
12334
|
meta.push(`expanded=${el.ariaExpanded}`);
|
|
12200
12335
|
}
|
|
@@ -12256,6 +12391,49 @@ function summarizeElementValue(el) {
|
|
|
12256
12391
|
}
|
|
12257
12392
|
return null;
|
|
12258
12393
|
}
|
|
12394
|
+
function isTextEntryControl$1(el) {
|
|
12395
|
+
if (el.disabled) return false;
|
|
12396
|
+
if (el.type !== "input" && el.type !== "textarea") return false;
|
|
12397
|
+
const inputType = (el.inputType || "text").toLowerCase();
|
|
12398
|
+
return ![
|
|
12399
|
+
"button",
|
|
12400
|
+
"checkbox",
|
|
12401
|
+
"file",
|
|
12402
|
+
"hidden",
|
|
12403
|
+
"image",
|
|
12404
|
+
"radio",
|
|
12405
|
+
"reset",
|
|
12406
|
+
"submit"
|
|
12407
|
+
].includes(inputType);
|
|
12408
|
+
}
|
|
12409
|
+
function hasRenderedValue(el) {
|
|
12410
|
+
return Boolean(summarizeElementValue(el));
|
|
12411
|
+
}
|
|
12412
|
+
function hasAnyFieldValue(el) {
|
|
12413
|
+
return el.hasValue === true || typeof el.value === "string" && el.value.trim().length > 0;
|
|
12414
|
+
}
|
|
12415
|
+
function formatFillHint(el) {
|
|
12416
|
+
if (el.index == null || el.disabled) return null;
|
|
12417
|
+
if (el.focused && isTextEntryControl$1(el)) {
|
|
12418
|
+
return `cursor is here; type_text(text="...") or type_text(index=${el.index})`;
|
|
12419
|
+
}
|
|
12420
|
+
if (hasAnyFieldValue(el) && isTextEntryControl$1(el)) {
|
|
12421
|
+
return `already has value; use type_text(index=${el.index}) only to change it`;
|
|
12422
|
+
}
|
|
12423
|
+
if (isTextEntryControl$1(el)) return `use type_text(index=${el.index})`;
|
|
12424
|
+
if (el.type === "select") return `use select_option(index=${el.index})`;
|
|
12425
|
+
return null;
|
|
12426
|
+
}
|
|
12427
|
+
function appendFieldAffordances(parts, el) {
|
|
12428
|
+
if (isTextEntryControl$1(el)) {
|
|
12429
|
+
parts.push("fillable");
|
|
12430
|
+
if (!hasAnyFieldValue(el)) parts.push("empty");
|
|
12431
|
+
} else if (el.type === "select") {
|
|
12432
|
+
if (!hasRenderedValue(el) && !hasAnyFieldValue(el)) parts.push("not-selected");
|
|
12433
|
+
}
|
|
12434
|
+
const hint = formatFillHint(el);
|
|
12435
|
+
if (hint) parts.push(hint);
|
|
12436
|
+
}
|
|
12259
12437
|
function isQuantityLike(el) {
|
|
12260
12438
|
const text = [
|
|
12261
12439
|
el.label,
|
|
@@ -12535,12 +12713,14 @@ function formatInteractiveElements(elements) {
|
|
|
12535
12713
|
parts.push("input");
|
|
12536
12714
|
const summary = summarizeElementValue(el);
|
|
12537
12715
|
if (summary) parts.push(`${summary.label}="${summary.value}"`);
|
|
12716
|
+
if (!isQuantityLike(el)) appendFieldAffordances(parts, el);
|
|
12538
12717
|
if (el.required) parts.push("(required)");
|
|
12539
12718
|
} else if (el.type === "select") {
|
|
12540
12719
|
parts.push(`[${el.label || "Select"}]`);
|
|
12541
12720
|
parts.push("dropdown");
|
|
12542
12721
|
const summary = summarizeElementValue(el);
|
|
12543
12722
|
if (summary) parts.push(`${summary.label}="${summary.value}"`);
|
|
12723
|
+
appendFieldAffordances(parts, el);
|
|
12544
12724
|
if (el.options?.length) {
|
|
12545
12725
|
parts.push(
|
|
12546
12726
|
`options=${el.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
|
|
@@ -12551,6 +12731,7 @@ function formatInteractiveElements(elements) {
|
|
|
12551
12731
|
parts.push("textarea");
|
|
12552
12732
|
const summary = summarizeElementValue(el);
|
|
12553
12733
|
if (summary) parts.push(`${summary.label}="${summary.value}"`);
|
|
12734
|
+
appendFieldAffordances(parts, el);
|
|
12554
12735
|
}
|
|
12555
12736
|
const meta = formatElementMeta(el);
|
|
12556
12737
|
if (meta.length > 0) parts.push(`(${meta.join(", ")})`);
|
|
@@ -12595,12 +12776,14 @@ function formatForms(forms) {
|
|
|
12595
12776
|
fieldParts.push(field.inputType || "text");
|
|
12596
12777
|
const summary = summarizeElementValue(field);
|
|
12597
12778
|
if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
|
|
12779
|
+
if (!isQuantityLike(field)) appendFieldAffordances(fieldParts, field);
|
|
12598
12780
|
if (field.required) fieldParts.push("(required)");
|
|
12599
12781
|
} else if (field.type === "select") {
|
|
12600
12782
|
fieldParts.push(`[${field.label || "Select"}]`);
|
|
12601
12783
|
fieldParts.push("dropdown");
|
|
12602
12784
|
const summary = summarizeElementValue(field);
|
|
12603
12785
|
if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
|
|
12786
|
+
appendFieldAffordances(fieldParts, field);
|
|
12604
12787
|
if (field.options?.length) {
|
|
12605
12788
|
fieldParts.push(
|
|
12606
12789
|
`options=${field.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
|
|
@@ -12611,6 +12794,7 @@ function formatForms(forms) {
|
|
|
12611
12794
|
fieldParts.push("textarea");
|
|
12612
12795
|
const summary = summarizeElementValue(field);
|
|
12613
12796
|
if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
|
|
12797
|
+
appendFieldAffordances(fieldParts, field);
|
|
12614
12798
|
}
|
|
12615
12799
|
const meta = formatElementMeta(field);
|
|
12616
12800
|
if (meta.length > 0) fieldParts.push(`(${meta.join(", ")})`);
|
|
@@ -13221,6 +13405,7 @@ function buildScopedContext(page, mode) {
|
|
|
13221
13405
|
const cartSnapshot = formatCartSnapshot(visiblePage);
|
|
13222
13406
|
const visibleForms = visiblePage.forms;
|
|
13223
13407
|
const dialogFocus = formatDialogFocus(page);
|
|
13408
|
+
const flightBookingFormHint = getFlightBookingFormHint(page);
|
|
13224
13409
|
const sections = [];
|
|
13225
13410
|
sections.push(`**URL:** ${page.url}`);
|
|
13226
13411
|
sections.push(`**Title:** ${page.title}`);
|
|
@@ -13242,6 +13427,11 @@ function buildScopedContext(page, mode) {
|
|
|
13242
13427
|
sections.push(formatPageIssues(page.pageIssues ?? []));
|
|
13243
13428
|
sections.push("");
|
|
13244
13429
|
}
|
|
13430
|
+
if (flightBookingFormHint) {
|
|
13431
|
+
sections.push("### Flight Booking Hint");
|
|
13432
|
+
sections.push(flightBookingFormHint);
|
|
13433
|
+
sections.push("");
|
|
13434
|
+
}
|
|
13245
13435
|
if (page.overlays.length > 0) {
|
|
13246
13436
|
sections.push("### Active Overlays");
|
|
13247
13437
|
sections.push(formatOverlays(page));
|
|
@@ -13379,9 +13569,41 @@ function detectPageType(page) {
|
|
|
13379
13569
|
return "ARTICLE";
|
|
13380
13570
|
return "GENERAL";
|
|
13381
13571
|
}
|
|
13572
|
+
function isFlightBookingPage(page) {
|
|
13573
|
+
const pageText = [
|
|
13574
|
+
page.url,
|
|
13575
|
+
page.title,
|
|
13576
|
+
page.excerpt,
|
|
13577
|
+
page.headings.map((heading) => heading.text).join(" ")
|
|
13578
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
13579
|
+
if (/google\.com\/travel\/flights|google flights/.test(pageText)) {
|
|
13580
|
+
return true;
|
|
13581
|
+
}
|
|
13582
|
+
const controlsText = [
|
|
13583
|
+
...page.interactiveElements,
|
|
13584
|
+
...page.forms.flatMap((form) => form.fields)
|
|
13585
|
+
].map(
|
|
13586
|
+
(el) => [el.label, el.placeholder, el.name, el.text, el.role, el.inputType].filter(Boolean).join(" ")
|
|
13587
|
+
).join(" ").toLowerCase();
|
|
13588
|
+
const hasRouteControls = /\b(?:where from|where to|origin|destination|from|to)\b/.test(
|
|
13589
|
+
controlsText
|
|
13590
|
+
);
|
|
13591
|
+
const hasDateControls = /\b(?:departure|depart|return|date)\b/.test(
|
|
13592
|
+
controlsText
|
|
13593
|
+
);
|
|
13594
|
+
const hasFlightLanguage = /\b(?:flight|flights|airfare|airline)\b/.test(
|
|
13595
|
+
`${pageText} ${controlsText}`
|
|
13596
|
+
);
|
|
13597
|
+
return hasFlightLanguage && hasRouteControls && hasDateControls;
|
|
13598
|
+
}
|
|
13599
|
+
function getFlightBookingFormHint(page) {
|
|
13600
|
+
if (!isFlightBookingPage(page)) return null;
|
|
13601
|
+
return "Flight booking form: use the visible route, destination, and date controls with type_text/select_option/click/submit_form before constructing direct Google Flights or travel-search URLs. Direct travel URLs are a fallback only after visible controls fail or the page is unusable.";
|
|
13602
|
+
}
|
|
13382
13603
|
function analyzePageIntent(page) {
|
|
13383
13604
|
const hints = [];
|
|
13384
13605
|
const pageType = detectPageType(page);
|
|
13606
|
+
const flightBookingFormHint = getFlightBookingFormHint(page);
|
|
13385
13607
|
const hasPagination = page.interactiveElements.some(
|
|
13386
13608
|
(el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»" || (el.label || "").toLowerCase().includes("next page")
|
|
13387
13609
|
);
|
|
@@ -13409,6 +13631,7 @@ function analyzePageIntent(page) {
|
|
|
13409
13631
|
hints.push(
|
|
13410
13632
|
"Treat the visible site search box as the primary navigation control before jumping to direct URLs."
|
|
13411
13633
|
);
|
|
13634
|
+
if (flightBookingFormHint) hints.push(flightBookingFormHint);
|
|
13412
13635
|
break;
|
|
13413
13636
|
case "SEARCH_RESULTS":
|
|
13414
13637
|
hints.push("Page type: SEARCH RESULTS");
|
|
@@ -13429,6 +13652,7 @@ function analyzePageIntent(page) {
|
|
|
13429
13652
|
`Page type: FORM (${formCount} form${formCount > 1 ? "s" : ""}, ${totalFields} fields)`
|
|
13430
13653
|
);
|
|
13431
13654
|
hints.push("Suggested: vessel_fill_form → fill all fields in one call");
|
|
13655
|
+
if (flightBookingFormHint) hints.push(flightBookingFormHint);
|
|
13432
13656
|
break;
|
|
13433
13657
|
}
|
|
13434
13658
|
case "PAGINATED_LIST":
|
|
@@ -13440,7 +13664,10 @@ function analyzePageIntent(page) {
|
|
|
13440
13664
|
hints.push("Suggested: vessel_extract_content for readable text");
|
|
13441
13665
|
break;
|
|
13442
13666
|
}
|
|
13443
|
-
if (hints.length === 0) return "";
|
|
13667
|
+
if (hints.length === 0 && !flightBookingFormHint) return "";
|
|
13668
|
+
if (flightBookingFormHint && pageType !== "SEARCH_READY" && pageType !== "FORM") {
|
|
13669
|
+
hints.push(flightBookingFormHint);
|
|
13670
|
+
}
|
|
13444
13671
|
return `### Page Intent (Speedee)
|
|
13445
13672
|
${hints.join("\n")}`;
|
|
13446
13673
|
}
|
|
@@ -13712,11 +13939,45 @@ function elementLabel(element) {
|
|
|
13712
13939
|
96
|
|
13713
13940
|
) || "Element";
|
|
13714
13941
|
}
|
|
13942
|
+
function isTextEntryControl(element) {
|
|
13943
|
+
if (element.disabled) return false;
|
|
13944
|
+
if (element.type !== "input" && element.type !== "textarea") return false;
|
|
13945
|
+
const inputType = (element.inputType || "text").toLowerCase();
|
|
13946
|
+
return ![
|
|
13947
|
+
"button",
|
|
13948
|
+
"checkbox",
|
|
13949
|
+
"file",
|
|
13950
|
+
"hidden",
|
|
13951
|
+
"image",
|
|
13952
|
+
"radio",
|
|
13953
|
+
"reset",
|
|
13954
|
+
"submit"
|
|
13955
|
+
].includes(inputType);
|
|
13956
|
+
}
|
|
13957
|
+
function formatFieldAffordance(element) {
|
|
13958
|
+
if (element.index == null || element.disabled) return "";
|
|
13959
|
+
const hasValue = element.hasValue === true || typeof element.value === "string" && element.value.trim();
|
|
13960
|
+
const empty = hasValue ? "" : "; empty";
|
|
13961
|
+
if (isTextEntryControl(element)) {
|
|
13962
|
+
if (element.focused) {
|
|
13963
|
+
return `${empty}; focused; type_text(text="...") targets this`;
|
|
13964
|
+
}
|
|
13965
|
+
if (hasValue) {
|
|
13966
|
+
return `; has value; type_text(index=${element.index}) changes it`;
|
|
13967
|
+
}
|
|
13968
|
+
return `${empty}; use type_text(index=${element.index})`;
|
|
13969
|
+
}
|
|
13970
|
+
if (element.type === "select") {
|
|
13971
|
+
return `; use select_option(index=${element.index})`;
|
|
13972
|
+
}
|
|
13973
|
+
return "";
|
|
13974
|
+
}
|
|
13715
13975
|
function formatElement(element) {
|
|
13716
13976
|
const prefix = element.index != null ? `[#${element.index}] ` : "";
|
|
13717
13977
|
const kind = element.type === "input" ? `${element.inputType || "text"} input` : element.type === "select" ? "select" : element.type;
|
|
13718
13978
|
const href = element.type === "link" && element.href ? ` -> ${element.href}` : "";
|
|
13719
|
-
|
|
13979
|
+
const affordance = formatFieldAffordance(element);
|
|
13980
|
+
return `${prefix}${elementLabel(element)} (${kind}${affordance})${href}`;
|
|
13720
13981
|
}
|
|
13721
13982
|
function uniqueElements(elements) {
|
|
13722
13983
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14676,6 +14937,26 @@ function buildDefaultEngineShortcut(rawQuery) {
|
|
|
14676
14937
|
appliedFilters: []
|
|
14677
14938
|
};
|
|
14678
14939
|
}
|
|
14940
|
+
function looksLikeFlightSearchText(text) {
|
|
14941
|
+
const lower = text.toLowerCase();
|
|
14942
|
+
return /\b(flight|flights|airfare|air fare|plane ticket|plane tickets)\b/.test(
|
|
14943
|
+
lower
|
|
14944
|
+
) || /\b(one-way|one way|roundtrip|round trip)\b/.test(lower) && /\b(to|from)\b/.test(lower);
|
|
14945
|
+
}
|
|
14946
|
+
function buildFlightSearchShortcut(rawQuery, taskGoal) {
|
|
14947
|
+
const goalQuery = normalizeSearchQuery(taskGoal || "");
|
|
14948
|
+
const toolQuery = normalizeSearchQuery(rawQuery);
|
|
14949
|
+
const combined = normalizeSearchQuery(`${goalQuery} ${toolQuery}`);
|
|
14950
|
+
if (!looksLikeFlightSearchText(combined)) return null;
|
|
14951
|
+
const searchQuery = looksLikeFlightSearchText(goalQuery) ? goalQuery : toolQuery;
|
|
14952
|
+
if (!searchQuery) return null;
|
|
14953
|
+
return {
|
|
14954
|
+
url: `https://www.google.com/travel/flights?q=${encodeURIComponent(searchQuery)}`,
|
|
14955
|
+
source: "Google Flights",
|
|
14956
|
+
section: "flight search",
|
|
14957
|
+
appliedFilters: []
|
|
14958
|
+
};
|
|
14959
|
+
}
|
|
14679
14960
|
function buildSearchEngineLandingShortcut(currentUrl, rawQuery) {
|
|
14680
14961
|
let url;
|
|
14681
14962
|
try {
|
|
@@ -14794,6 +15075,304 @@ function describeFillField(field) {
|
|
|
14794
15075
|
if (field.placeholder) return `placeholder=${field.placeholder}`;
|
|
14795
15076
|
return "field";
|
|
14796
15077
|
}
|
|
15078
|
+
const FILLABLE_CONTROL_HELPERS = `
|
|
15079
|
+
function vesselText(value) {
|
|
15080
|
+
return value == null ? "" : String(value).trim();
|
|
15081
|
+
}
|
|
15082
|
+
|
|
15083
|
+
function vesselControlLabel(el, original) {
|
|
15084
|
+
var source = el || original;
|
|
15085
|
+
return vesselText(source && source.getAttribute && source.getAttribute("aria-label")) ||
|
|
15086
|
+
vesselText(source && source.getAttribute && source.getAttribute("placeholder")) ||
|
|
15087
|
+
vesselText(source && source.getAttribute && source.getAttribute("name")) ||
|
|
15088
|
+
vesselText(original && original.getAttribute && original.getAttribute("aria-label")) ||
|
|
15089
|
+
vesselText(original && original.textContent).slice(0, 80) ||
|
|
15090
|
+
"input";
|
|
15091
|
+
}
|
|
15092
|
+
|
|
15093
|
+
function vesselIsVisible(el) {
|
|
15094
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
15095
|
+
var style = window.getComputedStyle(el);
|
|
15096
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
15097
|
+
if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") return false;
|
|
15098
|
+
var rect = el.getBoundingClientRect();
|
|
15099
|
+
return rect.width > 0 && rect.height > 0;
|
|
15100
|
+
}
|
|
15101
|
+
|
|
15102
|
+
function vesselIsDisabled(el) {
|
|
15103
|
+
return !!(el && (el.disabled || el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true"));
|
|
15104
|
+
}
|
|
15105
|
+
|
|
15106
|
+
function vesselIsNativeField(el) {
|
|
15107
|
+
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
|
|
15108
|
+
}
|
|
15109
|
+
|
|
15110
|
+
function vesselIsEditableElement(el) {
|
|
15111
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
15112
|
+
var role = (el.getAttribute("role") || "").toLowerCase();
|
|
15113
|
+
return el.isContentEditable ||
|
|
15114
|
+
el.getAttribute("contenteditable") === "true" ||
|
|
15115
|
+
role === "textbox" ||
|
|
15116
|
+
role === "searchbox";
|
|
15117
|
+
}
|
|
15118
|
+
|
|
15119
|
+
function vesselResolveFillableControl(el) {
|
|
15120
|
+
if (!el) return null;
|
|
15121
|
+
if (vesselIsNativeField(el) || vesselIsEditableElement(el)) return el;
|
|
15122
|
+
if (!(el instanceof Element)) return null;
|
|
15123
|
+
var nested = el.querySelector("input:not([type='hidden']):not([type='submit']):not([type='button']), textarea, select, [contenteditable='true'], [role='textbox'], [role='searchbox']");
|
|
15124
|
+
return nested instanceof HTMLElement ? nested : null;
|
|
15125
|
+
}
|
|
15126
|
+
|
|
15127
|
+
function vesselIsTextEntryActivator(el) {
|
|
15128
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
15129
|
+
var role = (el.getAttribute("role") || "").toLowerCase();
|
|
15130
|
+
return role === "combobox" ||
|
|
15131
|
+
el.getAttribute("aria-haspopup") === "listbox" ||
|
|
15132
|
+
!!el.getAttribute("aria-controls");
|
|
15133
|
+
}
|
|
15134
|
+
|
|
15135
|
+
function vesselFindVisibleFillableControl(original) {
|
|
15136
|
+
var active = vesselResolveFillableControl(document.activeElement);
|
|
15137
|
+
if (active && vesselIsVisible(active) && !vesselIsDisabled(active)) return active;
|
|
15138
|
+
|
|
15139
|
+
var scopes = Array.from(document.querySelectorAll("dialog[open], [role='dialog'], [role='alertdialog'], [aria-modal='true'], [role='listbox'], [role='combobox'][aria-expanded='true']"));
|
|
15140
|
+
scopes.push(document.body);
|
|
15141
|
+
var selector = "input:not([type='hidden']):not([type='submit']):not([type='button']), textarea, select, [contenteditable='true'], [role='textbox'], [role='searchbox']";
|
|
15142
|
+
for (var i = 0; i < scopes.length; i += 1) {
|
|
15143
|
+
var scope = scopes[i];
|
|
15144
|
+
if (!scope || !(scope instanceof Element)) continue;
|
|
15145
|
+
var candidates = Array.from(scope.querySelectorAll(selector));
|
|
15146
|
+
for (var j = 0; j < candidates.length; j += 1) {
|
|
15147
|
+
var candidate = vesselResolveFillableControl(candidates[j]);
|
|
15148
|
+
if (candidate && candidate !== original && vesselIsVisible(candidate) && !vesselIsDisabled(candidate)) {
|
|
15149
|
+
return candidate;
|
|
15150
|
+
}
|
|
15151
|
+
}
|
|
15152
|
+
}
|
|
15153
|
+
return null;
|
|
15154
|
+
}
|
|
15155
|
+
|
|
15156
|
+
async function vesselFindFillTarget(original) {
|
|
15157
|
+
var direct = vesselResolveFillableControl(original);
|
|
15158
|
+
if (direct && !vesselIsDisabled(direct)) return { el: direct, activated: false };
|
|
15159
|
+
|
|
15160
|
+
if (vesselIsTextEntryActivator(original)) {
|
|
15161
|
+
original.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
15162
|
+
original.focus();
|
|
15163
|
+
original.click();
|
|
15164
|
+
await new Promise(function(resolve) { setTimeout(resolve, 180); });
|
|
15165
|
+
var opened = vesselFindVisibleFillableControl(original);
|
|
15166
|
+
if (opened) return { el: opened, activated: true };
|
|
15167
|
+
}
|
|
15168
|
+
|
|
15169
|
+
return null;
|
|
15170
|
+
}
|
|
15171
|
+
|
|
15172
|
+
function vesselSetNativeValue(el, value) {
|
|
15173
|
+
var proto = el instanceof HTMLTextAreaElement
|
|
15174
|
+
? HTMLTextAreaElement.prototype
|
|
15175
|
+
: el instanceof HTMLSelectElement
|
|
15176
|
+
? HTMLSelectElement.prototype
|
|
15177
|
+
: HTMLInputElement.prototype;
|
|
15178
|
+
var desc = Object.getOwnPropertyDescriptor(proto, "value");
|
|
15179
|
+
if (desc && desc.set) {
|
|
15180
|
+
desc.set.call(el, value);
|
|
15181
|
+
} else {
|
|
15182
|
+
el.value = value;
|
|
15183
|
+
}
|
|
15184
|
+
}
|
|
15185
|
+
|
|
15186
|
+
function vesselDispatchTextEvents(el, value) {
|
|
15187
|
+
try {
|
|
15188
|
+
el.dispatchEvent(new InputEvent("beforeinput", {
|
|
15189
|
+
bubbles: true,
|
|
15190
|
+
cancelable: true,
|
|
15191
|
+
data: value,
|
|
15192
|
+
inputType: "insertText",
|
|
15193
|
+
}));
|
|
15194
|
+
} catch {}
|
|
15195
|
+
try {
|
|
15196
|
+
el.dispatchEvent(new InputEvent("input", {
|
|
15197
|
+
bubbles: true,
|
|
15198
|
+
cancelable: true,
|
|
15199
|
+
data: value,
|
|
15200
|
+
inputType: "insertText",
|
|
15201
|
+
}));
|
|
15202
|
+
} catch {
|
|
15203
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
15204
|
+
}
|
|
15205
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
15206
|
+
}
|
|
15207
|
+
|
|
15208
|
+
function vesselReadFillableControlValue(el) {
|
|
15209
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
15210
|
+
return el.value || "";
|
|
15211
|
+
}
|
|
15212
|
+
return el.textContent || "";
|
|
15213
|
+
}
|
|
15214
|
+
|
|
15215
|
+
function vesselValueWasApplied(el, value) {
|
|
15216
|
+
var actual = vesselReadFillableControlValue(el);
|
|
15217
|
+
if (!value) return !actual;
|
|
15218
|
+
return actual === value || actual.toLowerCase().indexOf(value.toLowerCase()) >= 0;
|
|
15219
|
+
}
|
|
15220
|
+
|
|
15221
|
+
async function vesselSetFillableControlValue(original, value) {
|
|
15222
|
+
var target = await vesselFindFillTarget(original);
|
|
15223
|
+
if (!target || !target.el) {
|
|
15224
|
+
return "Error[not-input]: Element is not text-editable. Click it first, then type into the opened text field.";
|
|
15225
|
+
}
|
|
15226
|
+
var el = target.el;
|
|
15227
|
+
if (vesselIsDisabled(el)) return "Error[disabled]: Input is disabled";
|
|
15228
|
+
|
|
15229
|
+
if (el instanceof HTMLSelectElement) {
|
|
15230
|
+
var requested = value.trim().toLowerCase();
|
|
15231
|
+
var option = Array.from(el.options).find(function(item) {
|
|
15232
|
+
return item.value.trim().toLowerCase() === requested ||
|
|
15233
|
+
(item.textContent || "").trim().toLowerCase() === requested;
|
|
15234
|
+
});
|
|
15235
|
+
if (!option) return "Error[option-not-found]: Option not found";
|
|
15236
|
+
el.value = option.value;
|
|
15237
|
+
el.focus();
|
|
15238
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
15239
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
15240
|
+
return "Selected: " + ((option.textContent || option.value).trim().slice(0, 100));
|
|
15241
|
+
}
|
|
15242
|
+
|
|
15243
|
+
el.focus();
|
|
15244
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
15245
|
+
vesselSetNativeValue(el, value);
|
|
15246
|
+
} else {
|
|
15247
|
+
el.textContent = value;
|
|
15248
|
+
}
|
|
15249
|
+
vesselDispatchTextEvents(el, value);
|
|
15250
|
+
await new Promise(function(resolve) { setTimeout(resolve, 80); });
|
|
15251
|
+
if (!vesselValueWasApplied(el, value)) {
|
|
15252
|
+
return "Error[type-not-applied]: The page did not keep the typed text. Retry with type_text(index=..., text=..., mode=\\"keystroke\\") or click the focused text box and type again.";
|
|
15253
|
+
}
|
|
15254
|
+
|
|
15255
|
+
var label = vesselControlLabel(el, original);
|
|
15256
|
+
var displayValue = el instanceof HTMLInputElement && el.type === "password"
|
|
15257
|
+
? "[hidden]"
|
|
15258
|
+
: value.slice(0, 80);
|
|
15259
|
+
return "Typed into: " + label + " = " + displayValue + (target.activated ? " (opened field)" : "");
|
|
15260
|
+
}
|
|
15261
|
+
|
|
15262
|
+
async function vesselTypeFillableControlKeystrokes(original, value) {
|
|
15263
|
+
var target = await vesselFindFillTarget(original);
|
|
15264
|
+
if (!target || !target.el) {
|
|
15265
|
+
return "Error[not-input]: Element is not text-editable. Click it first, then type into the opened text field.";
|
|
15266
|
+
}
|
|
15267
|
+
var el = target.el;
|
|
15268
|
+
if (vesselIsDisabled(el)) return "Error[disabled]: Input is disabled";
|
|
15269
|
+
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || vesselIsEditableElement(el))) {
|
|
15270
|
+
return "Error[not-input]: Element is not a text input";
|
|
15271
|
+
}
|
|
15272
|
+
|
|
15273
|
+
el.focus();
|
|
15274
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
15275
|
+
vesselSetNativeValue(el, "");
|
|
15276
|
+
} else {
|
|
15277
|
+
el.textContent = "";
|
|
15278
|
+
}
|
|
15279
|
+
vesselDispatchTextEvents(el, "");
|
|
15280
|
+
|
|
15281
|
+
var chars = value.split("");
|
|
15282
|
+
for (var i = 0; i < chars.length; i += 1) {
|
|
15283
|
+
var ch = chars[i];
|
|
15284
|
+
el.dispatchEvent(new KeyboardEvent("keydown", { key: ch, bubbles: true, cancelable: true }));
|
|
15285
|
+
el.dispatchEvent(new KeyboardEvent("keypress", { key: ch, bubbles: true, cancelable: true }));
|
|
15286
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
15287
|
+
vesselSetNativeValue(el, el.value + ch);
|
|
15288
|
+
} else {
|
|
15289
|
+
el.textContent = (el.textContent || "") + ch;
|
|
15290
|
+
}
|
|
15291
|
+
el.dispatchEvent(new InputEvent("input", {
|
|
15292
|
+
bubbles: true,
|
|
15293
|
+
cancelable: true,
|
|
15294
|
+
data: ch,
|
|
15295
|
+
inputType: "insertText",
|
|
15296
|
+
}));
|
|
15297
|
+
el.dispatchEvent(new KeyboardEvent("keyup", { key: ch, bubbles: true, cancelable: true }));
|
|
15298
|
+
}
|
|
15299
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
15300
|
+
await new Promise(function(resolve) { setTimeout(resolve, 80); });
|
|
15301
|
+
if (!vesselValueWasApplied(el, value)) {
|
|
15302
|
+
return "Error[type-not-applied]: The page did not keep the typed text after keystrokes. Click the text box again or use press_key for the active field.";
|
|
15303
|
+
}
|
|
15304
|
+
return "Typed into: " + vesselControlLabel(el, original) + " = " + value.slice(0, 80) + (target.activated ? " (opened field)" : "");
|
|
15305
|
+
}
|
|
15306
|
+
`;
|
|
15307
|
+
function shouldRetryWithNativeTyping(result) {
|
|
15308
|
+
return typeof result === "string" && result.startsWith("Error[type-not-applied]");
|
|
15309
|
+
}
|
|
15310
|
+
async function typeTextWithNativeInput(wc, selector, value) {
|
|
15311
|
+
const focusResult = await executePageScript(
|
|
15312
|
+
wc,
|
|
15313
|
+
`
|
|
15314
|
+
(async function() {
|
|
15315
|
+
${FILLABLE_CONTROL_HELPERS}
|
|
15316
|
+
const el = ${selector.includes(" >>> ") ? `window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)})` : `document.querySelector(${JSON.stringify(selector)})`};
|
|
15317
|
+
if (!el) return { error: 'Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh.' };
|
|
15318
|
+
const target = await vesselFindFillTarget(el);
|
|
15319
|
+
if (!target || !target.el) {
|
|
15320
|
+
return { error: "Error[not-input]: Element is not text-editable. Click it first, then type into the opened text field." };
|
|
15321
|
+
}
|
|
15322
|
+
target.el.focus();
|
|
15323
|
+
return { label: vesselControlLabel(target.el, el) };
|
|
15324
|
+
})()
|
|
15325
|
+
`,
|
|
15326
|
+
{
|
|
15327
|
+
timeoutMs: 2500,
|
|
15328
|
+
label: "prepare native typing"
|
|
15329
|
+
}
|
|
15330
|
+
);
|
|
15331
|
+
if (focusResult === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
|
|
15332
|
+
if (!focusResult || typeof focusResult !== "object") {
|
|
15333
|
+
return "Error: Could not prepare native typing";
|
|
15334
|
+
}
|
|
15335
|
+
if (typeof focusResult.error === "string") return focusResult.error;
|
|
15336
|
+
wc.focus();
|
|
15337
|
+
const selectModifier = process.platform === "darwin" ? "meta" : "control";
|
|
15338
|
+
wc.sendInputEvent({ type: "keyDown", keyCode: "A", modifiers: [selectModifier] });
|
|
15339
|
+
await sleep(8);
|
|
15340
|
+
wc.sendInputEvent({ type: "keyUp", keyCode: "A", modifiers: [selectModifier] });
|
|
15341
|
+
await sleep(8);
|
|
15342
|
+
wc.sendInputEvent({ type: "keyDown", keyCode: "Backspace" });
|
|
15343
|
+
await sleep(4);
|
|
15344
|
+
wc.sendInputEvent({ type: "keyUp", keyCode: "Backspace" });
|
|
15345
|
+
for (const char of value) {
|
|
15346
|
+
wc.sendInputEvent({ type: "keyDown", keyCode: char });
|
|
15347
|
+
wc.sendInputEvent({ type: "char", keyCode: char });
|
|
15348
|
+
wc.sendInputEvent({ type: "keyUp", keyCode: char });
|
|
15349
|
+
await sleep(2);
|
|
15350
|
+
}
|
|
15351
|
+
await sleep(120);
|
|
15352
|
+
const verify = await executePageScript(
|
|
15353
|
+
wc,
|
|
15354
|
+
`
|
|
15355
|
+
(function() {
|
|
15356
|
+
${FILLABLE_CONTROL_HELPERS}
|
|
15357
|
+
const active = vesselResolveFillableControl(document.activeElement);
|
|
15358
|
+
if (!active) return { ok: false, actual: "" };
|
|
15359
|
+
const actual = vesselReadFillableControlValue(active);
|
|
15360
|
+
return { ok: vesselValueWasApplied(active, ${JSON.stringify(value)}), actual: actual.slice(0, 80) };
|
|
15361
|
+
})()
|
|
15362
|
+
`,
|
|
15363
|
+
{
|
|
15364
|
+
timeoutMs: 1500,
|
|
15365
|
+
label: "verify native typing"
|
|
15366
|
+
}
|
|
15367
|
+
);
|
|
15368
|
+
if (verify === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
|
|
15369
|
+
if (!verify || verify.ok !== true) {
|
|
15370
|
+
const actual = verify && typeof verify.actual === "string" && verify.actual ? ` Current field text: "${verify.actual}".` : "";
|
|
15371
|
+
return `Error[type-not-applied]: The page did not accept the typed text.${actual} Click the field again or use a different indexed text box.`;
|
|
15372
|
+
}
|
|
15373
|
+
const label = typeof focusResult.label === "string" && focusResult.label ? focusResult.label : "input";
|
|
15374
|
+
return `Typed into: ${label} = ${value.slice(0, 80)} (native input)`;
|
|
15375
|
+
}
|
|
14797
15376
|
async function resolveFieldSelector(wc, field) {
|
|
14798
15377
|
const directSelector = await resolveSelector(wc, field.index, field.selector);
|
|
14799
15378
|
if (directSelector) return directSelector;
|
|
@@ -14849,8 +15428,22 @@ async function resolveFieldSelector(wc, field) {
|
|
|
14849
15428
|
return normalize(parts.join(" "));
|
|
14850
15429
|
}
|
|
14851
15430
|
|
|
15431
|
+
function isNativeField(el) {
|
|
15432
|
+
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
|
|
15433
|
+
}
|
|
15434
|
+
|
|
15435
|
+
function isCustomTextField(el) {
|
|
15436
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
15437
|
+
var role = (el.getAttribute("role") || "").toLowerCase();
|
|
15438
|
+
return el.isContentEditable ||
|
|
15439
|
+
el.getAttribute("contenteditable") === "true" ||
|
|
15440
|
+
role === "textbox" ||
|
|
15441
|
+
role === "searchbox" ||
|
|
15442
|
+
role === "combobox";
|
|
15443
|
+
}
|
|
15444
|
+
|
|
14852
15445
|
function scoreField(el) {
|
|
14853
|
-
if (!(el
|
|
15446
|
+
if (!isNativeField(el) && !isCustomTextField(el)) {
|
|
14854
15447
|
return -1;
|
|
14855
15448
|
}
|
|
14856
15449
|
if (!isVisible(el) || el.disabled || el.getAttribute("aria-disabled") === "true") {
|
|
@@ -14879,10 +15472,11 @@ async function resolveFieldSelector(wc, field) {
|
|
|
14879
15472
|
|
|
14880
15473
|
if (score === 0) return -1;
|
|
14881
15474
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) score += 5;
|
|
15475
|
+
if (isCustomTextField(el)) score += 3;
|
|
14882
15476
|
return score;
|
|
14883
15477
|
}
|
|
14884
15478
|
|
|
14885
|
-
const candidates = Array.from(document.querySelectorAll("input, textarea, select"));
|
|
15479
|
+
const candidates = Array.from(document.querySelectorAll("input, textarea, select, [contenteditable='true'], [role='textbox'], [role='searchbox'], [role='combobox']"));
|
|
14886
15480
|
let best = null;
|
|
14887
15481
|
let bestScore = -1;
|
|
14888
15482
|
for (const el of candidates) {
|
|
@@ -14932,136 +15526,73 @@ async function setElementValue(wc, selector, value) {
|
|
|
14932
15526
|
const result2 = await executePageScript(
|
|
14933
15527
|
wc,
|
|
14934
15528
|
`
|
|
14935
|
-
(function() {
|
|
15529
|
+
(async function() {
|
|
15530
|
+
${FILLABLE_CONTROL_HELPERS}
|
|
14936
15531
|
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
14937
15532
|
if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
14938
|
-
|
|
14939
|
-
if (el.disabled || el.getAttribute("aria-disabled") === "true") return "Error[disabled]: Input is disabled";
|
|
14940
|
-
if (el instanceof HTMLSelectElement) {
|
|
14941
|
-
var requested = ${JSON.stringify(value)}.trim().toLowerCase();
|
|
14942
|
-
var option = Array.from(el.options).find(function(item) {
|
|
14943
|
-
return item.value.trim().toLowerCase() === requested ||
|
|
14944
|
-
(item.textContent || "").trim().toLowerCase() === requested;
|
|
14945
|
-
});
|
|
14946
|
-
if (!option) return "Error[option-not-found]: Option not found";
|
|
14947
|
-
el.value = option.value;
|
|
14948
|
-
el.focus();
|
|
14949
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
14950
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
14951
|
-
return "Selected: " + ((option.textContent || option.value).trim().slice(0, 100));
|
|
14952
|
-
}
|
|
14953
|
-
var proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
14954
|
-
var desc = Object.getOwnPropertyDescriptor(proto, "value");
|
|
14955
|
-
if (desc && desc.set) { desc.set.call(el, ${JSON.stringify(value)}); } else { el.value = ${JSON.stringify(value)}; }
|
|
14956
|
-
el.focus();
|
|
14957
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
14958
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
14959
|
-
return "Typed into: " + (el.getAttribute("aria-label") || el.placeholder || el.name || "input");
|
|
15533
|
+
return vesselSetFillableControlValue(el, ${JSON.stringify(value)});
|
|
14960
15534
|
})()
|
|
14961
15535
|
`,
|
|
14962
15536
|
{
|
|
14963
15537
|
label: "type text in shadow input"
|
|
14964
15538
|
}
|
|
14965
15539
|
);
|
|
14966
|
-
|
|
15540
|
+
if (result2 === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
|
|
15541
|
+
if (shouldRetryWithNativeTyping(result2)) {
|
|
15542
|
+
return typeTextWithNativeInput(wc, selector, value);
|
|
15543
|
+
}
|
|
15544
|
+
return result2 || "Error: Could not type into element";
|
|
14967
15545
|
}
|
|
14968
15546
|
const result = await executePageScript(
|
|
14969
15547
|
wc,
|
|
14970
15548
|
`
|
|
14971
|
-
(function() {
|
|
15549
|
+
(async function() {
|
|
15550
|
+
${FILLABLE_CONTROL_HELPERS}
|
|
14972
15551
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
14973
15552
|
if (!el) return 'Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh.';
|
|
14974
|
-
|
|
14975
|
-
return 'Error[not-input]: Element is not a fillable input';
|
|
14976
|
-
}
|
|
14977
|
-
if (el.disabled || el.getAttribute('aria-disabled') === 'true') {
|
|
14978
|
-
return 'Error[disabled]: Input is disabled';
|
|
14979
|
-
}
|
|
14980
|
-
|
|
14981
|
-
if (el instanceof HTMLSelectElement) {
|
|
14982
|
-
const requested = ${JSON.stringify(value)}.trim().toLowerCase();
|
|
14983
|
-
const option = Array.from(el.options).find((item) => {
|
|
14984
|
-
const label = (item.textContent || '').trim().toLowerCase();
|
|
14985
|
-
return label === requested || item.value.trim().toLowerCase() === requested;
|
|
14986
|
-
});
|
|
14987
|
-
if (!option) {
|
|
14988
|
-
return 'Error[option-not-found]: Option not found';
|
|
14989
|
-
}
|
|
14990
|
-
el.value = option.value;
|
|
14991
|
-
el.focus();
|
|
14992
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
14993
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
14994
|
-
return 'Selected: ' + ((option.textContent || option.value).trim().slice(0, 100));
|
|
14995
|
-
}
|
|
14996
|
-
|
|
14997
|
-
const prototype = el instanceof HTMLTextAreaElement
|
|
14998
|
-
? HTMLTextAreaElement.prototype
|
|
14999
|
-
: HTMLInputElement.prototype;
|
|
15000
|
-
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
15001
|
-
if (descriptor && descriptor.set) {
|
|
15002
|
-
descriptor.set.call(el, ${JSON.stringify(value)});
|
|
15003
|
-
} else {
|
|
15004
|
-
el.value = ${JSON.stringify(value)};
|
|
15005
|
-
}
|
|
15006
|
-
|
|
15007
|
-
el.focus();
|
|
15008
|
-
el.dispatchEvent(new InputEvent('input', {
|
|
15009
|
-
bubbles: true,
|
|
15010
|
-
cancelable: true,
|
|
15011
|
-
data: ${JSON.stringify(value)},
|
|
15012
|
-
inputType: 'insertText',
|
|
15013
|
-
}));
|
|
15014
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
15015
|
-
return 'Typed into: ' +
|
|
15016
|
-
(el.getAttribute('aria-label') || el.placeholder || el.name || 'input') +
|
|
15017
|
-
' = ' + (el.type === 'password' ? '[hidden]' : String(el.value).slice(0, 80));
|
|
15553
|
+
return vesselSetFillableControlValue(el, ${JSON.stringify(value)});
|
|
15018
15554
|
})()
|
|
15019
15555
|
`,
|
|
15020
15556
|
{
|
|
15021
15557
|
label: "type text"
|
|
15022
15558
|
}
|
|
15023
15559
|
);
|
|
15024
|
-
|
|
15560
|
+
if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
|
|
15561
|
+
if (shouldRetryWithNativeTyping(result)) {
|
|
15562
|
+
return typeTextWithNativeInput(wc, selector, value);
|
|
15563
|
+
}
|
|
15564
|
+
return result || "Error: Could not type into element";
|
|
15025
15565
|
}
|
|
15026
15566
|
async function typeKeystroke(wc, selector, value) {
|
|
15567
|
+
if (selector.startsWith("__vessel_idx:")) {
|
|
15568
|
+
return setElementValue(wc, selector, value);
|
|
15569
|
+
}
|
|
15570
|
+
if (selector.includes(" >>> ")) {
|
|
15571
|
+
const result2 = await executePageScript(
|
|
15572
|
+
wc,
|
|
15573
|
+
`
|
|
15574
|
+
(async function() {
|
|
15575
|
+
${FILLABLE_CONTROL_HELPERS}
|
|
15576
|
+
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
15577
|
+
if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
15578
|
+
return vesselTypeFillableControlKeystrokes(el, ${JSON.stringify(value)});
|
|
15579
|
+
})()
|
|
15580
|
+
`,
|
|
15581
|
+
{
|
|
15582
|
+
timeoutMs: 2500,
|
|
15583
|
+
label: "type keystrokes in shadow input"
|
|
15584
|
+
}
|
|
15585
|
+
);
|
|
15586
|
+
return result2 === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result2 || "Error: Could not type into element";
|
|
15587
|
+
}
|
|
15027
15588
|
const result = await executePageScript(
|
|
15028
15589
|
wc,
|
|
15029
15590
|
`
|
|
15030
15591
|
(async function() {
|
|
15592
|
+
${FILLABLE_CONTROL_HELPERS}
|
|
15031
15593
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
15032
15594
|
if (!el) return 'Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh.';
|
|
15033
|
-
|
|
15034
|
-
return 'Error[not-input]: Element is not a text input';
|
|
15035
|
-
}
|
|
15036
|
-
if (el.disabled || el.getAttribute('aria-disabled') === 'true') {
|
|
15037
|
-
return 'Error[disabled]: Input is disabled';
|
|
15038
|
-
}
|
|
15039
|
-
el.focus();
|
|
15040
|
-
const prototype = el instanceof HTMLTextAreaElement
|
|
15041
|
-
? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
15042
|
-
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
15043
|
-
if (descriptor && descriptor.set) {
|
|
15044
|
-
descriptor.set.call(el, '');
|
|
15045
|
-
} else {
|
|
15046
|
-
el.value = '';
|
|
15047
|
-
}
|
|
15048
|
-
el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data: '', inputType: 'deleteContentBackward' }));
|
|
15049
|
-
const chars = ${JSON.stringify(value)}.split('');
|
|
15050
|
-
for (const ch of chars) {
|
|
15051
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true, cancelable: true }));
|
|
15052
|
-
el.dispatchEvent(new KeyboardEvent('keypress', { key: ch, bubbles: true, cancelable: true }));
|
|
15053
|
-
if (descriptor && descriptor.set) {
|
|
15054
|
-
descriptor.set.call(el, el.value + ch);
|
|
15055
|
-
} else {
|
|
15056
|
-
el.value += ch;
|
|
15057
|
-
}
|
|
15058
|
-
el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data: ch, inputType: 'insertText' }));
|
|
15059
|
-
el.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true, cancelable: true }));
|
|
15060
|
-
}
|
|
15061
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
15062
|
-
return 'Typed into: ' +
|
|
15063
|
-
(el.getAttribute('aria-label') || el.placeholder || el.name || 'input') +
|
|
15064
|
-
' = ' + (el.type === 'password' ? '[hidden]' : String(el.value).slice(0, 80));
|
|
15595
|
+
return vesselTypeFillableControlKeystrokes(el, ${JSON.stringify(value)});
|
|
15065
15596
|
})()
|
|
15066
15597
|
`,
|
|
15067
15598
|
{
|
|
@@ -17427,8 +17958,25 @@ async function locateImplicitTextTarget(wc) {
|
|
|
17427
17958
|
}
|
|
17428
17959
|
|
|
17429
17960
|
function isFillable(el) {
|
|
17961
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
17962
|
+
if (
|
|
17963
|
+
el.getAttribute("aria-disabled") === "true" ||
|
|
17964
|
+
(el instanceof HTMLInputElement && (el.disabled || el.readOnly)) ||
|
|
17965
|
+
(el instanceof HTMLTextAreaElement && (el.disabled || el.readOnly))
|
|
17966
|
+
) {
|
|
17967
|
+
return false;
|
|
17968
|
+
}
|
|
17969
|
+
const role = normalize(el.getAttribute("role"));
|
|
17970
|
+
if (
|
|
17971
|
+
el.isContentEditable ||
|
|
17972
|
+
el.getAttribute("contenteditable") === "true" ||
|
|
17973
|
+
role === "textbox" ||
|
|
17974
|
+
role === "searchbox" ||
|
|
17975
|
+
role === "combobox"
|
|
17976
|
+
) {
|
|
17977
|
+
return true;
|
|
17978
|
+
}
|
|
17430
17979
|
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return false;
|
|
17431
|
-
if (el.disabled || el.readOnly || el.getAttribute("aria-disabled") === "true") return false;
|
|
17432
17980
|
const type = el instanceof HTMLTextAreaElement ? "text" : normalize(el.getAttribute("type") || el.type || "text");
|
|
17433
17981
|
return ["", "search", "text", "email", "url", "tel", "number", "password"].includes(type);
|
|
17434
17982
|
}
|
|
@@ -17445,7 +17993,7 @@ async function locateImplicitTextTarget(wc) {
|
|
|
17445
17993
|
}
|
|
17446
17994
|
|
|
17447
17995
|
const candidates = Array.from(
|
|
17448
|
-
document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]), textarea')
|
|
17996
|
+
document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]), textarea, [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"]')
|
|
17449
17997
|
).filter((el) => isFillable(el) && isVisible(el));
|
|
17450
17998
|
|
|
17451
17999
|
let best = null;
|
|
@@ -17697,7 +18245,8 @@ async function handleWebSearch(ctx, tabId, args) {
|
|
|
17697
18245
|
if (!tab || !tabId) return "Error: No active tab";
|
|
17698
18246
|
const query = String(args.query || "").trim();
|
|
17699
18247
|
if (!query) return "Error: No web search query provided.";
|
|
17700
|
-
const
|
|
18248
|
+
const taskGoal = ctx.runtime.getState().taskTracker?.goal;
|
|
18249
|
+
const shortcut = buildFlightSearchShortcut(query, taskGoal) ?? buildDefaultEngineShortcut(query);
|
|
17701
18250
|
if (!shortcut) {
|
|
17702
18251
|
return "Error: No default search engine is configured. Navigate to a search engine or set a default search engine first.";
|
|
17703
18252
|
}
|
|
@@ -22402,6 +22951,8 @@ const SHARED_NAVIGATION_INSTRUCTIONS = [
|
|
|
22402
22951
|
"Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.",
|
|
22403
22952
|
"After navigating to a new site, do not call read_page immediately unless you are genuinely stuck. Prefer the site's search box, known navigation patterns, or clicking a visible section first.",
|
|
22404
22953
|
"For open-web discovery, current facts, prices, flights, news, or search-engine home pages, call web_search(query). Use search(query) only to search within the current site or app.",
|
|
22954
|
+
"For flight price-shopping, include the route, date, and trip type in web_search(query); do not send vague or partial flight queries.",
|
|
22955
|
+
"On flight/travel booking pages with visible route, destination, or date fields, use those visible controls before constructing direct Google Flights or travel-search URLs. Direct travel URLs are a fallback only after the visible controls fail or the page is unusable.",
|
|
22405
22956
|
"On retail and marketplace sites, prefer the site's visible search box, filters, and result pages over direct product URLs.",
|
|
22406
22957
|
"For broad discovery tasks, prefer direct sources and site-specific search over generic search engines."
|
|
22407
22958
|
];
|
|
@@ -2195,6 +2195,7 @@ let pageDiffActivityThrottleTimer = null;
|
|
|
2195
2195
|
let lastPageDiffSignature = "";
|
|
2196
2196
|
const PAGE_DIFF_ACTIVITY_THROTTLE_MS = 350;
|
|
2197
2197
|
const PAGE_DIFF_MUTATION_DEBOUNCE_MS = 1200;
|
|
2198
|
+
const CUSTOM_TEXT_FIELD_SELECTOR = '[contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"]';
|
|
2198
2199
|
function normalizeSignatureText(value) {
|
|
2199
2200
|
return (value || "").replace(/\s+/g, " ").trim();
|
|
2200
2201
|
}
|
|
@@ -2938,6 +2939,37 @@ function getInputLabelWithSource(el) {
|
|
|
2938
2939
|
if (placeholder) return { label: placeholder, source: "placeholder" };
|
|
2939
2940
|
return {};
|
|
2940
2941
|
}
|
|
2942
|
+
function getCustomTextFieldLabelWithSource(el) {
|
|
2943
|
+
const ariaLabel = getTrimmedText(el.getAttribute("aria-label"));
|
|
2944
|
+
if (ariaLabel) return { label: ariaLabel, source: "aria-label" };
|
|
2945
|
+
const labelledBy = getNodeTextByIds(el.getAttribute("aria-labelledby"));
|
|
2946
|
+
if (labelledBy) return { label: labelledBy, source: "label" };
|
|
2947
|
+
const placeholder = getTrimmedText(el.getAttribute("placeholder"));
|
|
2948
|
+
if (placeholder) return { label: placeholder, source: "placeholder" };
|
|
2949
|
+
const text = getTrimmedText(el.textContent);
|
|
2950
|
+
if (text) return { label: text, source: "text" };
|
|
2951
|
+
return {};
|
|
2952
|
+
}
|
|
2953
|
+
function isNativeFormField(el) {
|
|
2954
|
+
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
|
|
2955
|
+
}
|
|
2956
|
+
function shouldExposeCustomTextField(el) {
|
|
2957
|
+
if (!(el instanceof HTMLElement) || isNativeFormField(el)) return false;
|
|
2958
|
+
if (!isElementVisible(el) || isElementDisabled(el)) return false;
|
|
2959
|
+
const role = getElementRole(el);
|
|
2960
|
+
return el.isContentEditable || el.getAttribute("contenteditable") === "true" || role === "textbox" || role === "searchbox" || role === "combobox";
|
|
2961
|
+
}
|
|
2962
|
+
function getCustomTextFieldInputType(el) {
|
|
2963
|
+
const role = getElementRole(el);
|
|
2964
|
+
if (role === "searchbox") return "search";
|
|
2965
|
+
if (role === "combobox") return "combobox";
|
|
2966
|
+
if (role === "textbox") return "text";
|
|
2967
|
+
return el.getAttribute("contenteditable") === "true" ? "text" : void 0;
|
|
2968
|
+
}
|
|
2969
|
+
function getCustomTextFieldHasValue(el) {
|
|
2970
|
+
const value = getTrimmedText(el.textContent) || getTrimmedText(el.getAttribute("aria-valuetext")) || getTrimmedText(el.getAttribute("value"));
|
|
2971
|
+
return value ? true : void 0;
|
|
2972
|
+
}
|
|
2941
2973
|
function getButtonTextWithSource(el) {
|
|
2942
2974
|
const textContent = getTrimmedText(el.textContent);
|
|
2943
2975
|
if (textContent) return { text: textContent, source: "text" };
|
|
@@ -2986,6 +3018,18 @@ function getElementValue(el) {
|
|
|
2986
3018
|
}
|
|
2987
3019
|
return void 0;
|
|
2988
3020
|
}
|
|
3021
|
+
function getElementHasValue(el) {
|
|
3022
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
3023
|
+
if (el.type === "password" || el.type === "checkbox" || el.type === "radio") {
|
|
3024
|
+
return void 0;
|
|
3025
|
+
}
|
|
3026
|
+
return getTrimmedText(el.value) ? true : void 0;
|
|
3027
|
+
}
|
|
3028
|
+
if (el instanceof HTMLSelectElement) {
|
|
3029
|
+
return getTrimmedText(el.value) ? true : void 0;
|
|
3030
|
+
}
|
|
3031
|
+
return void 0;
|
|
3032
|
+
}
|
|
2989
3033
|
function getSelectOptions(el) {
|
|
2990
3034
|
const options = Array.from(el.options).map((option) => ({
|
|
2991
3035
|
label: option.textContent?.trim() || option.value.trim(),
|
|
@@ -2999,6 +3043,13 @@ function getAriaBoolean(el, attr) {
|
|
|
2999
3043
|
if (val === "false") return false;
|
|
3000
3044
|
return void 0;
|
|
3001
3045
|
}
|
|
3046
|
+
function getDeepActiveElement() {
|
|
3047
|
+
let active = document.activeElement;
|
|
3048
|
+
while (active instanceof HTMLElement && active.shadowRoot?.activeElement) {
|
|
3049
|
+
active = active.shadowRoot.activeElement;
|
|
3050
|
+
}
|
|
3051
|
+
return active;
|
|
3052
|
+
}
|
|
3002
3053
|
function buildBaseMetadata(el) {
|
|
3003
3054
|
return {
|
|
3004
3055
|
context: getElementContext(el),
|
|
@@ -3009,6 +3060,7 @@ function buildBaseMetadata(el) {
|
|
|
3009
3060
|
description: getElementDescription(el),
|
|
3010
3061
|
...getVisibilityState(el),
|
|
3011
3062
|
disabled: isElementDisabled(el),
|
|
3063
|
+
focused: getDeepActiveElement() === el || void 0,
|
|
3012
3064
|
ariaExpanded: getAriaBoolean(el, "aria-expanded"),
|
|
3013
3065
|
ariaPressed: getAriaBoolean(el, "aria-pressed"),
|
|
3014
3066
|
ariaSelected: getAriaBoolean(el, "aria-selected")
|
|
@@ -3120,6 +3172,7 @@ function extractInteractiveElements() {
|
|
|
3120
3172
|
placeholder: element.getAttribute("placeholder") || void 0,
|
|
3121
3173
|
required: element.hasAttribute("required") || void 0,
|
|
3122
3174
|
value: getElementValue(element),
|
|
3175
|
+
hasValue: getElementHasValue(element),
|
|
3123
3176
|
options: element instanceof HTMLSelectElement ? getSelectOptions(element) : void 0,
|
|
3124
3177
|
...buildBaseMetadata(input),
|
|
3125
3178
|
role,
|
|
@@ -3128,6 +3181,20 @@ function extractInteractiveElements() {
|
|
|
3128
3181
|
...getFieldMetadata(element)
|
|
3129
3182
|
});
|
|
3130
3183
|
});
|
|
3184
|
+
deepQuerySelectorAll(CUSTOM_TEXT_FIELD_SELECTOR).forEach((field) => {
|
|
3185
|
+
if (!shouldExposeCustomTextField(field)) return;
|
|
3186
|
+
const label = getCustomTextFieldLabelWithSource(field);
|
|
3187
|
+
const role = getElementRole(field);
|
|
3188
|
+
elements.push({
|
|
3189
|
+
type: "input",
|
|
3190
|
+
label: label.label?.slice(0, MAX_LABEL_LENGTH),
|
|
3191
|
+
labelSource: label.source,
|
|
3192
|
+
inputType: getCustomTextFieldInputType(field),
|
|
3193
|
+
hasValue: getCustomTextFieldHasValue(field),
|
|
3194
|
+
...buildBaseMetadata(field),
|
|
3195
|
+
role
|
|
3196
|
+
});
|
|
3197
|
+
});
|
|
3131
3198
|
return elements;
|
|
3132
3199
|
}
|
|
3133
3200
|
function extractForms() {
|
|
@@ -3143,23 +3210,28 @@ function extractForms() {
|
|
|
3143
3210
|
const form = formEl;
|
|
3144
3211
|
const fields = [];
|
|
3145
3212
|
form.querySelectorAll(
|
|
3146
|
-
|
|
3213
|
+
`input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='image']), select, textarea, ${CUSTOM_TEXT_FIELD_SELECTOR}`
|
|
3147
3214
|
).forEach((input) => {
|
|
3215
|
+
if (!isNativeFormField(input) && !shouldExposeCustomTextField(input)) {
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3148
3218
|
const element = input;
|
|
3149
3219
|
const tag = input.tagName.toLowerCase();
|
|
3150
3220
|
const label = getInputLabelWithSource(element);
|
|
3221
|
+
const customLabel = isNativeFormField(input) ? {} : getCustomTextFieldLabelWithSource(input);
|
|
3151
3222
|
const role = getElementRole(input);
|
|
3152
3223
|
const radioText = role === "radio" || element instanceof HTMLInputElement && element.type === "radio" ? getTrimmedText(
|
|
3153
3224
|
element.getAttribute("value") || element.getAttribute("aria-label") || label.label
|
|
3154
3225
|
) : void 0;
|
|
3155
3226
|
fields.push({
|
|
3156
3227
|
type: tag === "select" ? "select" : tag === "textarea" ? "textarea" : "input",
|
|
3157
|
-
label: label.label?.slice(0, MAX_LABEL_LENGTH),
|
|
3158
|
-
labelSource: label.source,
|
|
3159
|
-
inputType: element.getAttribute("type") || void 0,
|
|
3228
|
+
label: (label.label || customLabel.label)?.slice(0, MAX_LABEL_LENGTH),
|
|
3229
|
+
labelSource: label.source || customLabel.source,
|
|
3230
|
+
inputType: element.getAttribute("type") || getCustomTextFieldInputType(input) || void 0,
|
|
3160
3231
|
placeholder: element.getAttribute("placeholder") || void 0,
|
|
3161
3232
|
required: element.hasAttribute("required") || void 0,
|
|
3162
3233
|
value: getElementValue(element),
|
|
3234
|
+
hasValue: isNativeFormField(input) ? getElementHasValue(element) : getCustomTextFieldHasValue(input),
|
|
3163
3235
|
options: element instanceof HTMLSelectElement ? getSelectOptions(element) : void 0,
|
|
3164
3236
|
...buildBaseMetadata(input),
|
|
3165
3237
|
role,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quanta-intellect/vessel-browser",
|
|
3
3
|
"mcpName": "io.github.unmodeled-tyler/vessel-browser",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.141",
|
|
5
5
|
"description": "AI-native web browser runtime for autonomous agents with human supervision",
|
|
6
6
|
"main": "./out/main/index.js",
|
|
7
7
|
"bin": {
|