@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 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
- ` return String(value).replace(/[${q}${bs}${bs}]/g, ${q}${bs}${bs}$&${q});`,
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
- ` var candidate = el.tagName.toLowerCase() + ${q}[${q} + attribute + ${q}=${escQ}${q} + __escapeSelectorValue(value) + ${escQ}]${q};`,
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
- latestToolMessage ? String(latestToolMessage.content || "") : null
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.trim().toLowerCase();
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.trim().toLowerCase();
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 buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview) {
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, or call inspect_element on the intended element to verify its index and selector.`
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
- return `${prefix}${elementLabel(element)} (${kind})${href}`;
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 instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
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
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) return "Error[not-input]: Element is not a fillable input";
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
- return result2 === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result2 || "Error: Could not type into element";
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
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
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
- return result === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result || "Error: Could not type into element";
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
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
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 shortcut = buildDefaultEngineShortcut(query);
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
- "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='image']), select, textarea"
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.140",
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": {