@quanta-intellect/vessel-browser 0.1.140 → 0.1.143

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
@@ -2221,6 +2221,19 @@ async function scrollToHighlight(wc, index) {
2221
2221
  })()
2222
2222
  `);
2223
2223
  }
2224
+ async function getHighlightTextAtIndex(wc, index) {
2225
+ const safeIndex = Math.floor(Number(index));
2226
+ return wc.executeJavaScript(`
2227
+ (function() {
2228
+ var highlights = document.querySelectorAll(${HIGHLIGHT_SELECTOR});
2229
+ if (${safeIndex} < 0 || ${safeIndex} >= highlights.length) return null;
2230
+ var el = highlights[${safeIndex}];
2231
+ var text = el.getAttribute && el.getAttribute('data-vessel-highlight-text');
2232
+ if (!text && el.textContent) text = el.textContent;
2233
+ return text ? text.trim() : null;
2234
+ })()
2235
+ `);
2236
+ }
2224
2237
  async function removeHighlightAtIndex(wc, index) {
2225
2238
  const safeIndex = Math.floor(Number(index));
2226
2239
  return wc.executeJavaScript(`
@@ -5589,15 +5602,12 @@ const DEFAULT_SELECTOR_ATTRIBUTES = [
5589
5602
  ];
5590
5603
  function selectorHelpersJS(attributes = DEFAULT_SELECTOR_ATTRIBUTES) {
5591
5604
  const attrsExpr = JSON.stringify(attributes);
5592
- const q = '"';
5593
- const bs = "\\";
5594
- const escQ = bs + q;
5595
5605
  return [
5596
5606
  "function __escapeSelectorValue(value) {",
5597
5607
  ' if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {',
5598
5608
  " return CSS.escape(value);",
5599
5609
  " }",
5600
- ` return String(value).replace(/[${q}${bs}${bs}]/g, ${q}${bs}${bs}$&${q});`,
5610
+ ' return String(value).replace(/["\\\\]/g, "\\\\$&");',
5601
5611
  "}",
5602
5612
  "",
5603
5613
  "function __uniqueSelector(candidate) {",
@@ -5613,7 +5623,7 @@ function selectorHelpersJS(attributes = DEFAULT_SELECTOR_ATTRIBUTES) {
5613
5623
  ' var value = (el.getAttribute && el.getAttribute(attribute)) || "";',
5614
5624
  " value = String(value).trim();",
5615
5625
  " if (!value) return null;",
5616
- ` var candidate = el.tagName.toLowerCase() + ${q}[${q} + attribute + ${q}=${escQ}${q} + __escapeSelectorValue(value) + ${escQ}]${q};`,
5626
+ ' var candidate = el.tagName.toLowerCase() + "[" + attribute + "=\\"" + __escapeSelectorValue(value) + "\\"]";',
5617
5627
  " return __uniqueSelector(candidate);",
5618
5628
  "}",
5619
5629
  "",
@@ -8754,6 +8764,59 @@ function recoverNarratedActionToolCalls(text, availableToolNames) {
8754
8764
  }
8755
8765
  return recovered;
8756
8766
  }
8767
+ function shouldRetryUnexecutedHighlightCompletion(userMessage, assistantText, successfulToolNames) {
8768
+ const userAskedForHighlights = /\b(?:highlight|highlights|mark|annotate)\b/i.test(
8769
+ userMessage
8770
+ );
8771
+ if (!userAskedForHighlights || successfulToolNames.includes("highlight")) {
8772
+ return false;
8773
+ }
8774
+ const normalizedAssistant = assistantText.toLowerCase();
8775
+ return /\b(?:highlighted|marked|annotated)\b/.test(normalizedAssistant) || /\b(?:green|yellow|red|blue|purple|orange)\s+highlights?\b/.test(
8776
+ normalizedAssistant
8777
+ ) || /\bhighlights?\s+(?:added|shown|applied|visible|on the page)\b/.test(
8778
+ normalizedAssistant
8779
+ );
8780
+ }
8781
+ function buildHighlightToolCompletionPrompt() {
8782
+ return `The user asked you to highlight items on the page, but no highlight tool call succeeded. Do not claim visual highlights are present until you call the supported highlight tool. Use read_page only if you need current page text, then call highlight with {"text":"exact visible title or passage"} for each item you want to mark. Use an element index only when the latest read_page result gives the exact current index for that same item.`;
8783
+ }
8784
+ const DOLLAR_PRICE_RE = /\$\s?(?:\d{2,4}|\d{1,3}(?:,\d{3})+)(?:\.\d{2})?\b/;
8785
+ const FLIGHT_TASK_RE = /\b(?:flight|flights|airfare|air fare|plane ticket|airline|airport|google flights|pdx|sfo|san francisco|portland)\b/i;
8786
+ const SHOPPING_INTENT_RE = /\b(?:cheap|cheapest|price|prices|fare|fares|one[- ]?way|round[- ]?trip|depart|departure|arrive|arrival|from|to)\b/i;
8787
+ 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;
8788
+ 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;
8789
+ const EMPTY_FORM_CONTEXT_RE = /\b(?:where to\?|search airports or cities|destination|from\?|departure date|calendar|google flights)\b/i;
8790
+ const TOOL_FAILURE_RE = /\b(?:error|failed|type-not-applied|did not type|could not type|not applied|unsupported|invalid args)\b/i;
8791
+ function normalizeEvidenceText(text) {
8792
+ return (text || "").replace(/\s+/g, " ").trim();
8793
+ }
8794
+ function looksLikeFlightShoppingTask(userMessage) {
8795
+ return FLIGHT_TASK_RE.test(userMessage) && SHOPPING_INTENT_RE.test(userMessage);
8796
+ }
8797
+ function answerIncludesFlightPriceClaims(assistantText) {
8798
+ return DOLLAR_PRICE_RE.test(assistantText) && FLIGHT_CLAIM_CONTEXT_RE.test(assistantText);
8799
+ }
8800
+ function toolResultHasFlightPriceEvidence(latestToolResultPreview) {
8801
+ const text = normalizeEvidenceText(latestToolResultPreview);
8802
+ if (!text) return false;
8803
+ if (TOOL_FAILURE_RE.test(text) && !DOLLAR_PRICE_RE.test(text)) return false;
8804
+ return DOLLAR_PRICE_RE.test(text) && FLIGHT_RESULT_CONTEXT_RE.test(text);
8805
+ }
8806
+ function shouldBlockUnsupportedFlightPriceAnswer(userMessage, assistantText, latestToolResultPreview) {
8807
+ return looksLikeFlightShoppingTask(userMessage) && answerIncludesFlightPriceClaims(assistantText) && !toolResultHasFlightPriceEvidence(latestToolResultPreview);
8808
+ }
8809
+ function buildFlightPriceEvidenceRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
8810
+ const latest = normalizeEvidenceText(latestToolResultPreview);
8811
+ 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." : "";
8812
+ return [
8813
+ `The user asked for live flight prices: ${userMessage}`,
8814
+ `Your last answer included specific flight prices, but the latest browser/tool evidence does not show visible flight-result rows with prices.`,
8815
+ `Erase that answer and continue with browser tools until the current page evidence shows the route/date and visible priced flight results.${maybeFormState}`,
8816
+ `Do not report airline names, times, or prices unless they are visible in the latest browser/page evidence.`,
8817
+ `Last unsupported answer: ${assistantText.replace(/\s+/g, " ").trim().slice(0, 500) || "(empty)"}`
8818
+ ].join("\n");
8819
+ }
8757
8820
  const logger$v = createLogger("OpenAIProvider");
8758
8821
  function shouldDebugAgentLoop() {
8759
8822
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
@@ -8804,6 +8867,22 @@ function toOpenAIReasoningEffort(effort, providerId, model) {
8804
8867
  return void 0;
8805
8868
  }
8806
8869
  }
8870
+ function openRouterRoutingOptions(providerId) {
8871
+ if (providerId !== "openrouter") return {};
8872
+ return {
8873
+ provider: {
8874
+ require_parameters: true,
8875
+ sort: "latency"
8876
+ }
8877
+ };
8878
+ }
8879
+ function buildOpenRouterAttributionHeaders() {
8880
+ return {
8881
+ "HTTP-Referer": "https://github.com/unmodeled-tyler/vessel-browser",
8882
+ "X-OpenRouter-Title": "Vessel Browser",
8883
+ "X-OpenRouter-Categories": "personal-agent,general-chat"
8884
+ };
8885
+ }
8807
8886
  function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
8808
8887
  if (profile !== "compact") return null;
8809
8888
  const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
@@ -8945,6 +9024,37 @@ function buildLatestStateReminder(toolResultPreview) {
8945
9024
  }
8946
9025
  return "";
8947
9026
  }
9027
+ function normalizedSearchToolQuery$1(name, args) {
9028
+ if (name !== "search" && name !== "web_search") return null;
9029
+ const raw = typeof args.query === "string" ? args.query : typeof args.text === "string" ? args.text : typeof args.term === "string" ? args.term : "";
9030
+ const normalized = raw.replace(/\s+/g, " ").trim().toLowerCase();
9031
+ return normalized || null;
9032
+ }
9033
+ function isOpenAIRealProgressToolForSearch(name) {
9034
+ return ![
9035
+ "read_page",
9036
+ "current_tab",
9037
+ "list_tabs",
9038
+ "screenshot",
9039
+ "clear_overlays",
9040
+ "accept_cookies",
9041
+ "dismiss_popup",
9042
+ "web_search",
9043
+ "search"
9044
+ ].includes(name);
9045
+ }
9046
+ function buildOpenAIRepeatedSearchError(previousTool, previousQuery, latestToolResultPreview, mode) {
9047
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
9048
+ const lines = [
9049
+ mode === "drifted" ? `Error: You already performed ${previousTool} successfully for this task.` : `Error: You already searched for "${previousQuery}" successfully with ${previousTool}.`,
9050
+ mode === "drifted" ? `Do not rewrite or broaden the query with another ${previousTool}; use the current search results instead.` : `Do not search the same query again with ${previousTool} or its search/web_search alias; use the current search results instead.`,
9051
+ `For named venues, businesses, organizations, schools, or local places, prefer opening the official site or clearly direct result from the current results page before answering. Do not switch to a site: restricted web_search when an official or direct result is already available. Next action: click a result, inspect a specific result, or answer from the result you already opened. Do not call any search tool again as preparation.`
9052
+ ];
9053
+ if (stateReminder) {
9054
+ lines.push(stateReminder);
9055
+ }
9056
+ return lines.join(" ");
9057
+ }
8948
9058
  function shouldRecoverCompactStall(text, userMessage) {
8949
9059
  const trimmed = text.trim().toLowerCase();
8950
9060
  if (!trimmed) return true;
@@ -9028,6 +9138,11 @@ function logAgentLoopDebug(payload) {
9028
9138
  }
9029
9139
  }
9030
9140
  function formatOpenAICompatErrorMessage(providerId, message) {
9141
+ if (providerId === "openrouter" && /(timed out after \d+(?:\.\d+)? seconds|request timed out|returned none after all retries|no content|empty response)/i.test(
9142
+ message
9143
+ )) {
9144
+ return `${message} OpenRouter reported an upstream model timeout/no-content failure. If this persists, retry or pin a specific low-latency tool-calling model instead of the free router.`;
9145
+ }
9031
9146
  if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
9032
9147
  message
9033
9148
  )) {
@@ -9050,10 +9165,7 @@ class OpenAICompatProvider {
9050
9165
  apiKey: config.apiKey || "ollama",
9051
9166
  baseURL,
9052
9167
  ...isOpenRouter && {
9053
- defaultHeaders: {
9054
- "HTTP-Referer": "https://github.com/unmodeled/vessel-browser",
9055
- "X-Title": "Vessel"
9056
- }
9168
+ defaultHeaders: buildOpenRouterAttributionHeaders()
9057
9169
  }
9058
9170
  });
9059
9171
  this.providerId = config.id;
@@ -9080,6 +9192,7 @@ class OpenAICompatProvider {
9080
9192
  max_tokens: 4096,
9081
9193
  stream: true,
9082
9194
  messages,
9195
+ ...openRouterRoutingOptions(this.providerId),
9083
9196
  ...openAIPromptCacheOptions({
9084
9197
  providerId: this.providerId,
9085
9198
  model: this.model,
@@ -9137,9 +9250,15 @@ class OpenAICompatProvider {
9137
9250
  const maxIterations = getEffectiveMaxIterations();
9138
9251
  let iterationsUsed = 0;
9139
9252
  let compactRecoveryCount = 0;
9253
+ let flightPriceEvidenceRecoveryCount = 0;
9254
+ let highlightCompletionRecoveryCount = 0;
9140
9255
  let compactCorrectionCount = 0;
9141
9256
  const recentCompactToolSignatures = [];
9142
9257
  const recentToolNames = [];
9258
+ const successfulToolNames = [];
9259
+ const recentSuccessfulSearchQueries = [];
9260
+ const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
9261
+ let lastSuccessfulWebSearchQuery = null;
9143
9262
  let clickReadLoopNudged = false;
9144
9263
  for (let i = 0; i < maxIterations; i++) {
9145
9264
  iterationsUsed = i + 1;
@@ -9161,6 +9280,7 @@ class OpenAICompatProvider {
9161
9280
  tools: openAITools,
9162
9281
  tool_choice: "auto",
9163
9282
  temperature: agentTemperatureForProfile(this.agentToolProfile),
9283
+ ...openRouterRoutingOptions(this.providerId),
9164
9284
  ...openAIPromptCacheOptions({
9165
9285
  providerId: this.providerId,
9166
9286
  model: this.model,
@@ -9273,6 +9393,24 @@ class OpenAICompatProvider {
9273
9393
  };
9274
9394
  messages.push(assistantMsg);
9275
9395
  if (toolCalls.length === 0) {
9396
+ const latestToolResultContent = latestToolMessage ? String(latestToolMessage.content || "") : null;
9397
+ if (flightPriceEvidenceRecoveryCount < 2 && shouldBlockUnsupportedFlightPriceAnswer(
9398
+ userMessage,
9399
+ textAccum,
9400
+ latestToolResultContent
9401
+ )) {
9402
+ flightPriceEvidenceRecoveryCount += 1;
9403
+ if (textAccum.trim()) onChunk("<<erase_prev>>");
9404
+ messages.push({
9405
+ role: "user",
9406
+ content: `[System] ${buildFlightPriceEvidenceRecoveryPrompt(
9407
+ userMessage,
9408
+ textAccum,
9409
+ latestToolResultContent
9410
+ )}`
9411
+ });
9412
+ continue;
9413
+ }
9276
9414
  if (compactRecoveryCount < 2 && shouldRetryCompactToolLoop(
9277
9415
  this.agentToolProfile,
9278
9416
  textAccum,
@@ -9285,14 +9423,28 @@ class OpenAICompatProvider {
9285
9423
  content: `[System] ${buildCompactRecoveryPrompt(
9286
9424
  userMessage,
9287
9425
  textAccum,
9288
- latestToolMessage ? String(latestToolMessage.content || "") : null
9426
+ latestToolResultContent
9289
9427
  )}`
9290
9428
  });
9291
9429
  continue;
9292
9430
  }
9431
+ if (highlightCompletionRecoveryCount < 1 && shouldRetryUnexecutedHighlightCompletion(
9432
+ userMessage,
9433
+ textAccum,
9434
+ successfulToolNames
9435
+ )) {
9436
+ highlightCompletionRecoveryCount += 1;
9437
+ if (textAccum.trim()) onChunk("<<erase_prev>>");
9438
+ messages.push({
9439
+ role: "user",
9440
+ content: `[System] ${buildHighlightToolCompletionPrompt()}`
9441
+ });
9442
+ continue;
9443
+ }
9293
9444
  break;
9294
9445
  }
9295
9446
  compactRecoveryCount = 0;
9447
+ flightPriceEvidenceRecoveryCount = 0;
9296
9448
  const iterationToolResultPreviews = [];
9297
9449
  for (const tc of toolCalls) {
9298
9450
  if (!availableToolNames.has(tc.name)) {
@@ -9341,6 +9493,29 @@ class OpenAICompatProvider {
9341
9493
  args = repairedArgs.args;
9342
9494
  args = coerceToolArgsForExecution(tc.name, args);
9343
9495
  const toolSignature = stableToolSignature(tc.name, args);
9496
+ const searchToolQuery = normalizedSearchToolQuery$1(tc.name, args);
9497
+ const isRepeatedSearchAcrossTools = searchToolQuery !== null && recentSuccessfulSearchQueries.includes(searchToolQuery);
9498
+ const isQueryDriftedWebSearch = tc.name === "web_search" && lastSuccessfulWebSearchQuery !== null && searchToolQuery !== null && searchToolQuery !== lastSuccessfulWebSearchQuery;
9499
+ if (isRepeatedSearchAcrossTools || isQueryDriftedWebSearch) {
9500
+ onChunk(`
9501
+ <<tool:${tc.name}:↻ duplicate suppressed>>
9502
+ `);
9503
+ const previousTool = isRepeatedSearchAcrossTools ? recentSuccessfulSearchToolByQuery.get(searchToolQuery ?? "") ?? (tc.name === "web_search" ? "search" : "web_search") : "web_search";
9504
+ const previousQuery = isRepeatedSearchAcrossTools ? searchToolQuery ?? "" : lastSuccessfulWebSearchQuery ?? "";
9505
+ const mode = isRepeatedSearchAcrossTools ? "repeated" : "drifted";
9506
+ messages.push({
9507
+ role: "tool",
9508
+ tool_call_id: tc.id,
9509
+ content: buildOpenAIRepeatedSearchError(
9510
+ previousTool,
9511
+ previousQuery,
9512
+ latestToolMessage ? String(latestToolMessage.content || "") : null,
9513
+ mode
9514
+ )
9515
+ });
9516
+ compactCorrectionCount += 1;
9517
+ continue;
9518
+ }
9344
9519
  if (this.agentToolProfile === "compact" && tc.name === "click" && isTargetlessClickArgs(args)) {
9345
9520
  onChunk(`
9346
9521
  <<tool:${tc.name}:⚠ missing target>>
@@ -9415,6 +9590,25 @@ class OpenAICompatProvider {
9415
9590
  recentCompactToolSignatures.shift();
9416
9591
  }
9417
9592
  }
9593
+ if (!/^Error:/i.test(toolContent.trim())) {
9594
+ successfulToolNames.push(tc.name);
9595
+ if (isOpenAIRealProgressToolForSearch(tc.name)) {
9596
+ lastSuccessfulWebSearchQuery = null;
9597
+ }
9598
+ if (searchToolQuery && !recentSuccessfulSearchQueries.includes(searchToolQuery)) {
9599
+ recentSuccessfulSearchQueries.push(searchToolQuery);
9600
+ recentSuccessfulSearchToolByQuery.set(searchToolQuery, tc.name);
9601
+ if (recentSuccessfulSearchQueries.length > 4) {
9602
+ const dropped = recentSuccessfulSearchQueries.shift();
9603
+ if (dropped) {
9604
+ recentSuccessfulSearchToolByQuery.delete(dropped);
9605
+ }
9606
+ }
9607
+ }
9608
+ if (tc.name === "web_search" && searchToolQuery) {
9609
+ lastSuccessfulWebSearchQuery = searchToolQuery;
9610
+ }
9611
+ }
9418
9612
  recentToolNames.push(tc.name);
9419
9613
  if (recentToolNames.length > 8) recentToolNames.shift();
9420
9614
  if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
@@ -9837,10 +10031,17 @@ function summarizeToolArg(args) {
9837
10031
  const index = typeof args.index === "number" ? `#${args.index}` : typeof args.index === "string" && args.index.trim() ? `#${args.index.trim()}` : "";
9838
10032
  return [args.url, args.query, args.text, args.selector, index, args.direction].map((value) => typeof value === "string" ? value : "").find((value) => value.length > 0) ?? "";
9839
10033
  }
10034
+ function normalizeCodexText(text) {
10035
+ return text.trim().toLowerCase().replace(/[‘’]/g, "'").replace(/[“”]/g, '"');
10036
+ }
9840
10037
  function looksLikeFailedToolOutput(output) {
9841
- const normalized = output.trim().toLowerCase();
10038
+ const normalized = normalizeCodexText(output);
9842
10039
  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
10040
  }
10041
+ function looksLikeTravelFareContext(text) {
10042
+ const normalized = normalizeCodexText(text);
10043
+ 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);
10044
+ }
9844
10045
  function emitCodexToolChunk(onChunk, name, args, output) {
9845
10046
  const summary = summarizeToolArg(args);
9846
10047
  const argSummary = looksLikeFailedToolOutput(output) ? ["⚠ failed", summary].filter(Boolean).join(" ") : summary;
@@ -10056,7 +10257,7 @@ const REAL_PROGRESS_TOOLS = /* @__PURE__ */ new Set([
10056
10257
  ]);
10057
10258
  function shouldRetryCodexToolLoop(text, hasToolHistory) {
10058
10259
  if (!hasToolHistory) return false;
10059
- const trimmed = text.trim().toLowerCase();
10260
+ const trimmed = normalizeCodexText(text);
10060
10261
  if (!trimmed) return true;
10061
10262
  if (trimmed.length <= 160 && trimmed.includes("?")) return true;
10062
10263
  const askingUserForDirection = [
@@ -10072,6 +10273,26 @@ function shouldRetryCodexToolLoop(text, hasToolHistory) {
10072
10273
  if (askingUserForDirection.some((signal) => trimmed.includes(signal))) {
10073
10274
  return true;
10074
10275
  }
10276
+ const askingUserToContinue = [
10277
+ "please let me",
10278
+ "let me inspect",
10279
+ "let me click",
10280
+ "need to continue from",
10281
+ "i need to continue",
10282
+ "allow me to",
10283
+ "can't access or reuse",
10284
+ "cant access or reuse",
10285
+ "can't access",
10286
+ "cant access",
10287
+ "visible context",
10288
+ "surface it",
10289
+ "send me the current",
10290
+ "send me the results",
10291
+ "send me the pricing"
10292
+ ];
10293
+ if (askingUserToContinue.some((signal) => trimmed.includes(signal))) {
10294
+ return true;
10295
+ }
10075
10296
  const cannotProceed = [
10076
10297
  "i cannot",
10077
10298
  "i can't",
@@ -10105,12 +10326,39 @@ function buildCodexRecoveryInput(userMessage, assistantText, latestToolResultPre
10105
10326
  content: [{ type: "input_text", text: lines.join("\n") }]
10106
10327
  };
10107
10328
  }
10108
- function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview) {
10329
+ function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText, latestToolResultPreview) {
10330
+ return {
10331
+ type: "message",
10332
+ role: "user",
10333
+ content: [
10334
+ {
10335
+ type: "input_text",
10336
+ text: `[System] ${buildFlightPriceEvidenceRecoveryPrompt(
10337
+ userMessage,
10338
+ assistantText,
10339
+ latestToolResultPreview
10340
+ )}`
10341
+ }
10342
+ ]
10343
+ };
10344
+ }
10345
+ function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
10109
10346
  const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10110
10347
  const lines = [
10111
10348
  `[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.`
10349
+ `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
10350
  ];
10351
+ if (failedClickCount >= 2) {
10352
+ lines.push(
10353
+ `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.`
10354
+ );
10355
+ }
10356
+ if (looksLikeTravelFareContext(`${attemptedTarget}
10357
+ ${latestToolResultPreview || ""}`)) {
10358
+ lines.push(
10359
+ `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.`
10360
+ );
10361
+ }
10114
10362
  if (stateReminder) {
10115
10363
  lines.push(stateReminder);
10116
10364
  }
@@ -10307,11 +10555,13 @@ class CodexProvider {
10307
10555
  let turnState = null;
10308
10556
  let toolHistoryCount = 0;
10309
10557
  let recoveryCount = 0;
10558
+ let flightPriceEvidenceRecoveryCount = 0;
10310
10559
  let correctionCount = 0;
10311
10560
  const recentToolSignatures = [];
10312
10561
  const recentToolNames = [];
10313
10562
  let clickReadLoopNudged = false;
10314
10563
  let latestToolResultPreview = null;
10564
+ let failedClickCountSinceProgress = 0;
10315
10565
  const recentSuccessfulSearchQueries = [];
10316
10566
  const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
10317
10567
  let lastSuccessfulWebSearchQuery = null;
@@ -10351,6 +10601,22 @@ class CodexProvider {
10351
10601
  }
10352
10602
  }
10353
10603
  if (functionCalls.length === 0) {
10604
+ if (flightPriceEvidenceRecoveryCount < 2 && shouldBlockUnsupportedFlightPriceAnswer(
10605
+ userMessage,
10606
+ result.text,
10607
+ latestToolResultPreview
10608
+ )) {
10609
+ flightPriceEvidenceRecoveryCount += 1;
10610
+ if (result.text.trim()) onChunk("<<erase_prev>>");
10611
+ currentInput = [
10612
+ buildCodexFlightPriceEvidenceRecoveryInput(
10613
+ userMessage,
10614
+ result.text,
10615
+ latestToolResultPreview
10616
+ )
10617
+ ];
10618
+ continue;
10619
+ }
10354
10620
  if (recoveryCount < 1 && shouldRetryCodexToolLoop(result.text, toolHistoryCount > 0)) {
10355
10621
  recoveryCount += 1;
10356
10622
  if (result.text.trim()) onChunk("<<erase_prev>>");
@@ -10366,6 +10632,7 @@ class CodexProvider {
10366
10632
  break;
10367
10633
  }
10368
10634
  recoveryCount = 0;
10635
+ flightPriceEvidenceRecoveryCount = 0;
10369
10636
  currentInput = [];
10370
10637
  for (const fc of functionCalls) {
10371
10638
  const functionCallInput = createCodexFunctionCallInput(fc);
@@ -10467,6 +10734,7 @@ ${latestToolResultPreview || ""}`
10467
10734
  if (!looksLikeFailedToolOutput(outputText)) {
10468
10735
  if (isRealProgressTool(prepared.prepared.name)) {
10469
10736
  lastSuccessfulWebSearchQuery = null;
10737
+ failedClickCountSinceProgress = 0;
10470
10738
  }
10471
10739
  }
10472
10740
  if (searchToolQuery && !looksLikeFailedToolOutput(outputText) && !recentSuccessfulSearchQueries.includes(searchToolQuery)) {
@@ -10488,10 +10756,12 @@ ${latestToolResultPreview || ""}`
10488
10756
  }
10489
10757
  }
10490
10758
  if (prepared.prepared.name === "click" && looksLikeFailedToolOutput(outputText)) {
10759
+ failedClickCountSinceProgress += 1;
10491
10760
  currentInput.push(
10492
10761
  buildCodexFailedClickRecoveryInput(
10493
10762
  summarizeToolArg(prepared.prepared.args),
10494
- latestToolResultPreview
10763
+ latestToolResultPreview,
10764
+ failedClickCountSinceProgress
10495
10765
  )
10496
10766
  );
10497
10767
  }
@@ -11400,13 +11670,15 @@ const TOOL_DEFINITIONS = [
11400
11670
  {
11401
11671
  name: "highlight",
11402
11672
  title: "Highlight Element",
11403
- description: "Visually highlight an element or text on the page for the user. Use to draw attention to specific content. Highlights persist until cleared.",
11673
+ description: "Visually highlight page content for the user. For named items like story titles, result titles, links, headings, or article passages, prefer text with the exact visible title/text. Use index only when you have a current read_page element index for that exact item. Highlights persist until cleared.",
11404
11674
  inputSchema: {
11405
- index: zod.z.number().optional().describe("Element index from page content to highlight"),
11406
- selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
11407
11675
  text: normalizedOptionalStringSchema().describe(
11408
- "Text to find and highlight on the page (all occurrences)"
11676
+ "Exact visible text/title to find and highlight on the page. Preferred for story titles, result titles, links, headings, and passages."
11409
11677
  ),
11678
+ index: zod.z.number().optional().describe(
11679
+ "Element index from the latest page content listing. Use only when it identifies the exact item to highlight."
11680
+ ),
11681
+ selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
11410
11682
  label: zod.z.string().optional().describe("Annotation label to display near the highlight"),
11411
11683
  durationMs: zod.z.number().optional().describe(
11412
11684
  "Auto-clear after this many milliseconds (omit for permanent)"
@@ -11518,7 +11790,7 @@ const TOOL_DEFINITIONS = [
11518
11790
  {
11519
11791
  name: "web_search",
11520
11792
  title: "Web Search",
11521
- description: "Search the open web using the configured default search engine. Use this for broad discovery tasks instead of typing into the current page.",
11793
+ description: "Search the open web using the configured default search engine. Use this for broad discovery tasks instead of typing into the current page. For named venues, businesses, organizations, schools, or local places, use web search to find the official/direct result, then click that result; do not use site: queries as a substitute for opening an available official result.",
11522
11794
  inputSchema: {
11523
11795
  query: zod.z.string().describe("Web search query text")
11524
11796
  },
@@ -12195,6 +12467,12 @@ function formatElementMeta(el) {
12195
12467
  if (el.checked !== void 0) {
12196
12468
  meta.push(el.checked ? "checked" : "unchecked");
12197
12469
  }
12470
+ if (el.focused) {
12471
+ meta.push("focused");
12472
+ }
12473
+ if (el.hasValue && !shouldRenderFieldValue(el)) {
12474
+ meta.push("has-value");
12475
+ }
12198
12476
  if (el.ariaExpanded !== void 0) {
12199
12477
  meta.push(`expanded=${el.ariaExpanded}`);
12200
12478
  }
@@ -12256,6 +12534,49 @@ function summarizeElementValue(el) {
12256
12534
  }
12257
12535
  return null;
12258
12536
  }
12537
+ function isTextEntryControl$1(el) {
12538
+ if (el.disabled) return false;
12539
+ if (el.type !== "input" && el.type !== "textarea") return false;
12540
+ const inputType = (el.inputType || "text").toLowerCase();
12541
+ return ![
12542
+ "button",
12543
+ "checkbox",
12544
+ "file",
12545
+ "hidden",
12546
+ "image",
12547
+ "radio",
12548
+ "reset",
12549
+ "submit"
12550
+ ].includes(inputType);
12551
+ }
12552
+ function hasRenderedValue(el) {
12553
+ return Boolean(summarizeElementValue(el));
12554
+ }
12555
+ function hasAnyFieldValue(el) {
12556
+ return el.hasValue === true || typeof el.value === "string" && el.value.trim().length > 0;
12557
+ }
12558
+ function formatFillHint(el) {
12559
+ if (el.index == null || el.disabled) return null;
12560
+ if (el.focused && isTextEntryControl$1(el)) {
12561
+ return `cursor is here; type_text(text="...") or type_text(index=${el.index})`;
12562
+ }
12563
+ if (hasAnyFieldValue(el) && isTextEntryControl$1(el)) {
12564
+ return `already has value; use type_text(index=${el.index}) only to change it`;
12565
+ }
12566
+ if (isTextEntryControl$1(el)) return `use type_text(index=${el.index})`;
12567
+ if (el.type === "select") return `use select_option(index=${el.index})`;
12568
+ return null;
12569
+ }
12570
+ function appendFieldAffordances(parts, el) {
12571
+ if (isTextEntryControl$1(el)) {
12572
+ parts.push("fillable");
12573
+ if (!hasAnyFieldValue(el)) parts.push("empty");
12574
+ } else if (el.type === "select") {
12575
+ if (!hasRenderedValue(el) && !hasAnyFieldValue(el)) parts.push("not-selected");
12576
+ }
12577
+ const hint = formatFillHint(el);
12578
+ if (hint) parts.push(hint);
12579
+ }
12259
12580
  function isQuantityLike(el) {
12260
12581
  const text = [
12261
12582
  el.label,
@@ -12411,6 +12732,37 @@ function purchaseActionPriority(el) {
12411
12732
  }
12412
12733
  return Number.POSITIVE_INFINITY;
12413
12734
  }
12735
+ function dateOrShowtimeControlPriority(el) {
12736
+ const haystack = normalizeComparable(
12737
+ [
12738
+ el.text,
12739
+ el.label,
12740
+ el.name,
12741
+ el.placeholder,
12742
+ el.description,
12743
+ el.href,
12744
+ el.role
12745
+ ].filter(Boolean).join(" ")
12746
+ );
12747
+ if (!haystack) return Number.POSITIVE_INFINITY;
12748
+ if (/\b(today|tomorrow|mon(?:day)?|tue(?:s|sday)?|wed(?:nesday)?|thu(?:rs|rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/.test(
12749
+ haystack
12750
+ )) {
12751
+ return 0;
12752
+ }
12753
+ if (/\b(showtimes?|showings?|screenings?|movie times?|date|calendar)\b/.test(
12754
+ haystack
12755
+ )) {
12756
+ return 1;
12757
+ }
12758
+ if (/\b(ticketing|tickets?|formovietickets|seat selection)\b/.test(haystack)) {
12759
+ return 2;
12760
+ }
12761
+ return Number.POSITIVE_INFINITY;
12762
+ }
12763
+ function isDateOrShowtimeControl(el) {
12764
+ return Number.isFinite(dateOrShowtimeControlPriority(el));
12765
+ }
12414
12766
  function isPurchaseActionElement(el) {
12415
12767
  if (el.type !== "button" && el.type !== "link" && !(el.type === "input" && (el.inputType === "submit" || el.inputType === "button"))) {
12416
12768
  return false;
@@ -12508,6 +12860,10 @@ function formatInteractiveElements(elements) {
12508
12860
  if (Number.isFinite(purchasePriority)) {
12509
12861
  s -= 25 - purchasePriority * 5;
12510
12862
  }
12863
+ const datePriority = dateOrShowtimeControlPriority(el);
12864
+ if (Number.isFinite(datePriority)) {
12865
+ s -= 18 - datePriority * 4;
12866
+ }
12511
12867
  if (el.visible === false) s += 100;
12512
12868
  if (el.inViewport === false) s += 50;
12513
12869
  if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
@@ -12535,15 +12891,18 @@ function formatInteractiveElements(elements) {
12535
12891
  parts.push("input");
12536
12892
  const summary = summarizeElementValue(el);
12537
12893
  if (summary) parts.push(`${summary.label}="${summary.value}"`);
12894
+ if (!isQuantityLike(el)) appendFieldAffordances(parts, el);
12538
12895
  if (el.required) parts.push("(required)");
12539
12896
  } else if (el.type === "select") {
12540
12897
  parts.push(`[${el.label || "Select"}]`);
12541
12898
  parts.push("dropdown");
12542
12899
  const summary = summarizeElementValue(el);
12543
12900
  if (summary) parts.push(`${summary.label}="${summary.value}"`);
12901
+ appendFieldAffordances(parts, el);
12544
12902
  if (el.options?.length) {
12903
+ const maxOptions = isDateOrShowtimeControl(el) ? 10 : 5;
12545
12904
  parts.push(
12546
- `options=${el.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12905
+ `options=${el.options.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12547
12906
  );
12548
12907
  }
12549
12908
  } else if (el.type === "textarea") {
@@ -12551,6 +12910,7 @@ function formatInteractiveElements(elements) {
12551
12910
  parts.push("textarea");
12552
12911
  const summary = summarizeElementValue(el);
12553
12912
  if (summary) parts.push(`${summary.label}="${summary.value}"`);
12913
+ appendFieldAffordances(parts, el);
12554
12914
  }
12555
12915
  const meta = formatElementMeta(el);
12556
12916
  if (meta.length > 0) parts.push(`(${meta.join(", ")})`);
@@ -12595,15 +12955,18 @@ function formatForms(forms) {
12595
12955
  fieldParts.push(field.inputType || "text");
12596
12956
  const summary = summarizeElementValue(field);
12597
12957
  if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
12958
+ if (!isQuantityLike(field)) appendFieldAffordances(fieldParts, field);
12598
12959
  if (field.required) fieldParts.push("(required)");
12599
12960
  } else if (field.type === "select") {
12600
12961
  fieldParts.push(`[${field.label || "Select"}]`);
12601
12962
  fieldParts.push("dropdown");
12602
12963
  const summary = summarizeElementValue(field);
12603
12964
  if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
12965
+ appendFieldAffordances(fieldParts, field);
12604
12966
  if (field.options?.length) {
12967
+ const maxOptions = isDateOrShowtimeControl(field) ? 10 : 5;
12605
12968
  fieldParts.push(
12606
- `options=${field.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12969
+ `options=${field.options.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12607
12970
  );
12608
12971
  }
12609
12972
  } else if (field.type === "textarea") {
@@ -12611,6 +12974,7 @@ function formatForms(forms) {
12611
12974
  fieldParts.push("textarea");
12612
12975
  const summary = summarizeElementValue(field);
12613
12976
  if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
12977
+ appendFieldAffordances(fieldParts, field);
12614
12978
  }
12615
12979
  const meta = formatElementMeta(field);
12616
12980
  if (meta.length > 0) fieldParts.push(`(${meta.join(", ")})`);
@@ -12868,6 +13232,7 @@ function chooseAgentReadMode(page) {
12868
13232
  }
12869
13233
  }
12870
13234
  function isSearchOrListingPage(page) {
13235
+ if (isHackerNewsListingPage(page.url)) return true;
12871
13236
  const haystack = normalizeComparable(
12872
13237
  [
12873
13238
  page.url,
@@ -12880,6 +13245,48 @@ function isSearchOrListingPage(page) {
12880
13245
  haystack
12881
13246
  );
12882
13247
  }
13248
+ function isHackerNewsListingPage(url) {
13249
+ try {
13250
+ const parsed = new URL(url);
13251
+ if (parsed.hostname !== "news.ycombinator.com") return false;
13252
+ const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
13253
+ return [
13254
+ "/",
13255
+ "/news",
13256
+ "/newest",
13257
+ "/front",
13258
+ "/ask",
13259
+ "/show",
13260
+ "/jobs",
13261
+ "/best",
13262
+ "/active",
13263
+ "/classic",
13264
+ "/noobstories"
13265
+ ].includes(pathname);
13266
+ } catch {
13267
+ return false;
13268
+ }
13269
+ }
13270
+ function isHackerNewsUtilityLink(element) {
13271
+ if (!element.href) return false;
13272
+ let url;
13273
+ try {
13274
+ url = new URL(element.href);
13275
+ } catch {
13276
+ return false;
13277
+ }
13278
+ if (url.hostname !== "news.ycombinator.com") return false;
13279
+ const text = normalizeComparable(element.text || "");
13280
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
13281
+ if (/^(hide|past|favorite|unfavorite|flag|unflag|discuss|reply|parent|more)$/.test(
13282
+ text
13283
+ )) {
13284
+ return true;
13285
+ }
13286
+ if (/^\d+\s+(?:comments?|points?)$/.test(text)) return true;
13287
+ if (pathname === "/hide" || pathname === "/user") return true;
13288
+ return pathname === "/item" && /^(?:discuss|\d+\s+comments?)$/.test(text);
13289
+ }
12883
13290
  function collectJsonLdEntityItems(input, results = []) {
12884
13291
  if (!input) return results;
12885
13292
  if (Array.isArray(input)) {
@@ -12919,7 +13326,7 @@ function getResultCandidates(page) {
12919
13326
  const pageHost = normalizeUrlForMatch(page.url);
12920
13327
  const searchOrListingPage = isSearchOrListingPage(page);
12921
13328
  const scored = page.interactiveElements.filter(
12922
- (element) => element.type === "link" && element.text?.trim() && element.href
13329
+ (element) => element.type === "link" && element.text?.trim() && element.href && !isHackerNewsUtilityLink(element)
12923
13330
  ).map((element) => {
12924
13331
  const text = element.text?.trim() || "";
12925
13332
  const comparableText = normalizeComparable(text);
@@ -13221,6 +13628,7 @@ function buildScopedContext(page, mode) {
13221
13628
  const cartSnapshot = formatCartSnapshot(visiblePage);
13222
13629
  const visibleForms = visiblePage.forms;
13223
13630
  const dialogFocus = formatDialogFocus(page);
13631
+ const flightBookingFormHint = getFlightBookingFormHint(page);
13224
13632
  const sections = [];
13225
13633
  sections.push(`**URL:** ${page.url}`);
13226
13634
  sections.push(`**Title:** ${page.title}`);
@@ -13242,6 +13650,11 @@ function buildScopedContext(page, mode) {
13242
13650
  sections.push(formatPageIssues(page.pageIssues ?? []));
13243
13651
  sections.push("");
13244
13652
  }
13653
+ if (flightBookingFormHint) {
13654
+ sections.push("### Flight Booking Hint");
13655
+ sections.push(flightBookingFormHint);
13656
+ sections.push("");
13657
+ }
13245
13658
  if (page.overlays.length > 0) {
13246
13659
  sections.push("### Active Overlays");
13247
13660
  sections.push(formatOverlays(page));
@@ -13373,15 +13786,48 @@ function detectPageType(page) {
13373
13786
  if (hasResults && hasSearchInput && listingLike) return "SEARCH_RESULTS";
13374
13787
  if (hasCart) return "SHOPPING";
13375
13788
  if (formCount > 0 && !hasPasswordField) return "FORM";
13789
+ if (isHackerNewsListingPage(page.url)) return "PAGINATED_LIST";
13376
13790
  if (hasPagination && listingLike) return "PAGINATED_LIST";
13377
13791
  if (hasSearchInput && !listingLike) return "SEARCH_READY";
13378
13792
  if (page.content.length > 3e3 && page.interactiveElements.length < 10)
13379
13793
  return "ARTICLE";
13380
13794
  return "GENERAL";
13381
13795
  }
13796
+ function isFlightBookingPage(page) {
13797
+ const pageText = [
13798
+ page.url,
13799
+ page.title,
13800
+ page.excerpt,
13801
+ page.headings.map((heading) => heading.text).join(" ")
13802
+ ].filter(Boolean).join(" ").toLowerCase();
13803
+ if (/google\.com\/travel\/flights|google flights/.test(pageText)) {
13804
+ return true;
13805
+ }
13806
+ const controlsText = [
13807
+ ...page.interactiveElements,
13808
+ ...page.forms.flatMap((form) => form.fields)
13809
+ ].map(
13810
+ (el) => [el.label, el.placeholder, el.name, el.text, el.role, el.inputType].filter(Boolean).join(" ")
13811
+ ).join(" ").toLowerCase();
13812
+ const hasRouteControls = /\b(?:where from|where to|origin|destination|from|to)\b/.test(
13813
+ controlsText
13814
+ );
13815
+ const hasDateControls = /\b(?:departure|depart|return|date)\b/.test(
13816
+ controlsText
13817
+ );
13818
+ const hasFlightLanguage = /\b(?:flight|flights|airfare|airline)\b/.test(
13819
+ `${pageText} ${controlsText}`
13820
+ );
13821
+ return hasFlightLanguage && hasRouteControls && hasDateControls;
13822
+ }
13823
+ function getFlightBookingFormHint(page) {
13824
+ if (!isFlightBookingPage(page)) return null;
13825
+ 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.";
13826
+ }
13382
13827
  function analyzePageIntent(page) {
13383
13828
  const hints = [];
13384
13829
  const pageType = detectPageType(page);
13830
+ const flightBookingFormHint = getFlightBookingFormHint(page);
13385
13831
  const hasPagination = page.interactiveElements.some(
13386
13832
  (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»" || (el.label || "").toLowerCase().includes("next page")
13387
13833
  );
@@ -13409,6 +13855,7 @@ function analyzePageIntent(page) {
13409
13855
  hints.push(
13410
13856
  "Treat the visible site search box as the primary navigation control before jumping to direct URLs."
13411
13857
  );
13858
+ if (flightBookingFormHint) hints.push(flightBookingFormHint);
13412
13859
  break;
13413
13860
  case "SEARCH_RESULTS":
13414
13861
  hints.push("Page type: SEARCH RESULTS");
@@ -13429,6 +13876,7 @@ function analyzePageIntent(page) {
13429
13876
  `Page type: FORM (${formCount} form${formCount > 1 ? "s" : ""}, ${totalFields} fields)`
13430
13877
  );
13431
13878
  hints.push("Suggested: vessel_fill_form → fill all fields in one call");
13879
+ if (flightBookingFormHint) hints.push(flightBookingFormHint);
13432
13880
  break;
13433
13881
  }
13434
13882
  case "PAGINATED_LIST":
@@ -13440,7 +13888,10 @@ function analyzePageIntent(page) {
13440
13888
  hints.push("Suggested: vessel_extract_content for readable text");
13441
13889
  break;
13442
13890
  }
13443
- if (hints.length === 0) return "";
13891
+ if (hints.length === 0 && !flightBookingFormHint) return "";
13892
+ if (flightBookingFormHint && pageType !== "SEARCH_READY" && pageType !== "FORM") {
13893
+ hints.push(flightBookingFormHint);
13894
+ }
13444
13895
  return `### Page Intent (Speedee)
13445
13896
  ${hints.join("\n")}`;
13446
13897
  }
@@ -13712,11 +14163,45 @@ function elementLabel(element) {
13712
14163
  96
13713
14164
  ) || "Element";
13714
14165
  }
14166
+ function isTextEntryControl(element) {
14167
+ if (element.disabled) return false;
14168
+ if (element.type !== "input" && element.type !== "textarea") return false;
14169
+ const inputType = (element.inputType || "text").toLowerCase();
14170
+ return ![
14171
+ "button",
14172
+ "checkbox",
14173
+ "file",
14174
+ "hidden",
14175
+ "image",
14176
+ "radio",
14177
+ "reset",
14178
+ "submit"
14179
+ ].includes(inputType);
14180
+ }
14181
+ function formatFieldAffordance(element) {
14182
+ if (element.index == null || element.disabled) return "";
14183
+ const hasValue = element.hasValue === true || typeof element.value === "string" && element.value.trim();
14184
+ const empty = hasValue ? "" : "; empty";
14185
+ if (isTextEntryControl(element)) {
14186
+ if (element.focused) {
14187
+ return `${empty}; focused; type_text(text="...") targets this`;
14188
+ }
14189
+ if (hasValue) {
14190
+ return `; has value; type_text(index=${element.index}) changes it`;
14191
+ }
14192
+ return `${empty}; use type_text(index=${element.index})`;
14193
+ }
14194
+ if (element.type === "select") {
14195
+ return `; use select_option(index=${element.index})`;
14196
+ }
14197
+ return "";
14198
+ }
13715
14199
  function formatElement(element) {
13716
14200
  const prefix = element.index != null ? `[#${element.index}] ` : "";
13717
14201
  const kind = element.type === "input" ? `${element.inputType || "text"} input` : element.type === "select" ? "select" : element.type;
13718
14202
  const href = element.type === "link" && element.href ? ` -> ${element.href}` : "";
13719
- return `${prefix}${elementLabel(element)} (${kind})${href}`;
14203
+ const affordance = formatFieldAffordance(element);
14204
+ return `${prefix}${elementLabel(element)} (${kind}${affordance})${href}`;
13720
14205
  }
13721
14206
  function uniqueElements(elements) {
13722
14207
  const seen = /* @__PURE__ */ new Set();
@@ -14676,6 +15161,26 @@ function buildDefaultEngineShortcut(rawQuery) {
14676
15161
  appliedFilters: []
14677
15162
  };
14678
15163
  }
15164
+ function looksLikeFlightSearchText(text) {
15165
+ const lower = text.toLowerCase();
15166
+ return /\b(flight|flights|airfare|air fare|plane ticket|plane tickets)\b/.test(
15167
+ lower
15168
+ ) || /\b(one-way|one way|roundtrip|round trip)\b/.test(lower) && /\b(to|from)\b/.test(lower);
15169
+ }
15170
+ function buildFlightSearchShortcut(rawQuery, taskGoal) {
15171
+ const goalQuery = normalizeSearchQuery(taskGoal || "");
15172
+ const toolQuery = normalizeSearchQuery(rawQuery);
15173
+ const combined = normalizeSearchQuery(`${goalQuery} ${toolQuery}`);
15174
+ if (!looksLikeFlightSearchText(combined)) return null;
15175
+ const searchQuery = looksLikeFlightSearchText(goalQuery) ? goalQuery : toolQuery;
15176
+ if (!searchQuery) return null;
15177
+ return {
15178
+ url: `https://www.google.com/travel/flights?q=${encodeURIComponent(searchQuery)}`,
15179
+ source: "Google Flights",
15180
+ section: "flight search",
15181
+ appliedFilters: []
15182
+ };
15183
+ }
14679
15184
  function buildSearchEngineLandingShortcut(currentUrl, rawQuery) {
14680
15185
  let url;
14681
15186
  try {
@@ -14794,6 +15299,304 @@ function describeFillField(field) {
14794
15299
  if (field.placeholder) return `placeholder=${field.placeholder}`;
14795
15300
  return "field";
14796
15301
  }
15302
+ const FILLABLE_CONTROL_HELPERS = `
15303
+ function vesselText(value) {
15304
+ return value == null ? "" : String(value).trim();
15305
+ }
15306
+
15307
+ function vesselControlLabel(el, original) {
15308
+ var source = el || original;
15309
+ return vesselText(source && source.getAttribute && source.getAttribute("aria-label")) ||
15310
+ vesselText(source && source.getAttribute && source.getAttribute("placeholder")) ||
15311
+ vesselText(source && source.getAttribute && source.getAttribute("name")) ||
15312
+ vesselText(original && original.getAttribute && original.getAttribute("aria-label")) ||
15313
+ vesselText(original && original.textContent).slice(0, 80) ||
15314
+ "input";
15315
+ }
15316
+
15317
+ function vesselIsVisible(el) {
15318
+ if (!(el instanceof HTMLElement)) return false;
15319
+ var style = window.getComputedStyle(el);
15320
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
15321
+ if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") return false;
15322
+ var rect = el.getBoundingClientRect();
15323
+ return rect.width > 0 && rect.height > 0;
15324
+ }
15325
+
15326
+ function vesselIsDisabled(el) {
15327
+ return !!(el && (el.disabled || el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true"));
15328
+ }
15329
+
15330
+ function vesselIsNativeField(el) {
15331
+ return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
15332
+ }
15333
+
15334
+ function vesselIsEditableElement(el) {
15335
+ if (!(el instanceof HTMLElement)) return false;
15336
+ var role = (el.getAttribute("role") || "").toLowerCase();
15337
+ return el.isContentEditable ||
15338
+ (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") ||
15339
+ role === "textbox" ||
15340
+ role === "searchbox";
15341
+ }
15342
+
15343
+ function vesselResolveFillableControl(el) {
15344
+ if (!el) return null;
15345
+ if (vesselIsNativeField(el) || vesselIsEditableElement(el)) return el;
15346
+ if (!(el instanceof Element)) return null;
15347
+ var nested = el.querySelector("input:not([type='hidden']):not([type='submit']):not([type='button']), textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox'], [role='searchbox']");
15348
+ return nested instanceof HTMLElement ? nested : null;
15349
+ }
15350
+
15351
+ function vesselIsTextEntryActivator(el) {
15352
+ if (!(el instanceof HTMLElement)) return false;
15353
+ var role = (el.getAttribute("role") || "").toLowerCase();
15354
+ return role === "combobox" ||
15355
+ el.getAttribute("aria-haspopup") === "listbox" ||
15356
+ !!el.getAttribute("aria-controls");
15357
+ }
15358
+
15359
+ function vesselFindVisibleFillableControl(original) {
15360
+ var active = vesselResolveFillableControl(document.activeElement);
15361
+ if (active && vesselIsVisible(active) && !vesselIsDisabled(active)) return active;
15362
+
15363
+ var scopes = Array.from(document.querySelectorAll("dialog[open], [role='dialog'], [role='alertdialog'], [aria-modal='true'], [role='listbox'], [role='combobox'][aria-expanded='true']"));
15364
+ scopes.push(document.body);
15365
+ var selector = "input:not([type='hidden']):not([type='submit']):not([type='button']), textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox'], [role='searchbox']";
15366
+ for (var i = 0; i < scopes.length; i += 1) {
15367
+ var scope = scopes[i];
15368
+ if (!scope || !(scope instanceof Element)) continue;
15369
+ var candidates = Array.from(scope.querySelectorAll(selector));
15370
+ for (var j = 0; j < candidates.length; j += 1) {
15371
+ var candidate = vesselResolveFillableControl(candidates[j]);
15372
+ if (candidate && candidate !== original && vesselIsVisible(candidate) && !vesselIsDisabled(candidate)) {
15373
+ return candidate;
15374
+ }
15375
+ }
15376
+ }
15377
+ return null;
15378
+ }
15379
+
15380
+ async function vesselFindFillTarget(original) {
15381
+ var direct = vesselResolveFillableControl(original);
15382
+ if (direct && !vesselIsDisabled(direct)) return { el: direct, activated: false };
15383
+
15384
+ if (vesselIsTextEntryActivator(original)) {
15385
+ original.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
15386
+ original.focus();
15387
+ original.click();
15388
+ await new Promise(function(resolve) { setTimeout(resolve, 180); });
15389
+ var opened = vesselFindVisibleFillableControl(original);
15390
+ if (opened) return { el: opened, activated: true };
15391
+ }
15392
+
15393
+ return null;
15394
+ }
15395
+
15396
+ function vesselSetNativeValue(el, value) {
15397
+ var proto = el instanceof HTMLTextAreaElement
15398
+ ? HTMLTextAreaElement.prototype
15399
+ : el instanceof HTMLSelectElement
15400
+ ? HTMLSelectElement.prototype
15401
+ : HTMLInputElement.prototype;
15402
+ var desc = Object.getOwnPropertyDescriptor(proto, "value");
15403
+ if (desc && desc.set) {
15404
+ desc.set.call(el, value);
15405
+ } else {
15406
+ el.value = value;
15407
+ }
15408
+ }
15409
+
15410
+ function vesselDispatchTextEvents(el, value) {
15411
+ try {
15412
+ el.dispatchEvent(new InputEvent("beforeinput", {
15413
+ bubbles: true,
15414
+ cancelable: true,
15415
+ data: value,
15416
+ inputType: "insertText",
15417
+ }));
15418
+ } catch {}
15419
+ try {
15420
+ el.dispatchEvent(new InputEvent("input", {
15421
+ bubbles: true,
15422
+ cancelable: true,
15423
+ data: value,
15424
+ inputType: "insertText",
15425
+ }));
15426
+ } catch {
15427
+ el.dispatchEvent(new Event("input", { bubbles: true }));
15428
+ }
15429
+ el.dispatchEvent(new Event("change", { bubbles: true }));
15430
+ }
15431
+
15432
+ function vesselReadFillableControlValue(el) {
15433
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
15434
+ return el.value || "";
15435
+ }
15436
+ return el.textContent || "";
15437
+ }
15438
+
15439
+ function vesselValueWasApplied(el, value) {
15440
+ var actual = vesselReadFillableControlValue(el);
15441
+ if (!value) return !actual;
15442
+ return actual === value || actual.toLowerCase().indexOf(value.toLowerCase()) >= 0;
15443
+ }
15444
+
15445
+ async function vesselSetFillableControlValue(original, value) {
15446
+ var target = await vesselFindFillTarget(original);
15447
+ if (!target || !target.el) {
15448
+ return "Error[not-input]: Element is not text-editable. Click it first, then type into the opened text field.";
15449
+ }
15450
+ var el = target.el;
15451
+ if (vesselIsDisabled(el)) return "Error[disabled]: Input is disabled";
15452
+
15453
+ if (el instanceof HTMLSelectElement) {
15454
+ var requested = value.trim().toLowerCase();
15455
+ var option = Array.from(el.options).find(function(item) {
15456
+ return item.value.trim().toLowerCase() === requested ||
15457
+ (item.textContent || "").trim().toLowerCase() === requested;
15458
+ });
15459
+ if (!option) return "Error[option-not-found]: Option not found";
15460
+ el.value = option.value;
15461
+ el.focus();
15462
+ el.dispatchEvent(new Event("input", { bubbles: true }));
15463
+ el.dispatchEvent(new Event("change", { bubbles: true }));
15464
+ return "Selected: " + ((option.textContent || option.value).trim().slice(0, 100));
15465
+ }
15466
+
15467
+ el.focus();
15468
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
15469
+ vesselSetNativeValue(el, value);
15470
+ } else {
15471
+ el.textContent = value;
15472
+ }
15473
+ vesselDispatchTextEvents(el, value);
15474
+ await new Promise(function(resolve) { setTimeout(resolve, 80); });
15475
+ if (!vesselValueWasApplied(el, value)) {
15476
+ 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.";
15477
+ }
15478
+
15479
+ var label = vesselControlLabel(el, original);
15480
+ var displayValue = el instanceof HTMLInputElement && el.type === "password"
15481
+ ? "[hidden]"
15482
+ : value.slice(0, 80);
15483
+ return "Typed into: " + label + " = " + displayValue + (target.activated ? " (opened field)" : "");
15484
+ }
15485
+
15486
+ async function vesselTypeFillableControlKeystrokes(original, value) {
15487
+ var target = await vesselFindFillTarget(original);
15488
+ if (!target || !target.el) {
15489
+ return "Error[not-input]: Element is not text-editable. Click it first, then type into the opened text field.";
15490
+ }
15491
+ var el = target.el;
15492
+ if (vesselIsDisabled(el)) return "Error[disabled]: Input is disabled";
15493
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || vesselIsEditableElement(el))) {
15494
+ return "Error[not-input]: Element is not a text input";
15495
+ }
15496
+
15497
+ el.focus();
15498
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
15499
+ vesselSetNativeValue(el, "");
15500
+ } else {
15501
+ el.textContent = "";
15502
+ }
15503
+ vesselDispatchTextEvents(el, "");
15504
+
15505
+ var chars = value.split("");
15506
+ for (var i = 0; i < chars.length; i += 1) {
15507
+ var ch = chars[i];
15508
+ el.dispatchEvent(new KeyboardEvent("keydown", { key: ch, bubbles: true, cancelable: true }));
15509
+ el.dispatchEvent(new KeyboardEvent("keypress", { key: ch, bubbles: true, cancelable: true }));
15510
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
15511
+ vesselSetNativeValue(el, el.value + ch);
15512
+ } else {
15513
+ el.textContent = (el.textContent || "") + ch;
15514
+ }
15515
+ el.dispatchEvent(new InputEvent("input", {
15516
+ bubbles: true,
15517
+ cancelable: true,
15518
+ data: ch,
15519
+ inputType: "insertText",
15520
+ }));
15521
+ el.dispatchEvent(new KeyboardEvent("keyup", { key: ch, bubbles: true, cancelable: true }));
15522
+ }
15523
+ el.dispatchEvent(new Event("change", { bubbles: true }));
15524
+ await new Promise(function(resolve) { setTimeout(resolve, 80); });
15525
+ if (!vesselValueWasApplied(el, value)) {
15526
+ 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.";
15527
+ }
15528
+ return "Typed into: " + vesselControlLabel(el, original) + " = " + value.slice(0, 80) + (target.activated ? " (opened field)" : "");
15529
+ }
15530
+ `;
15531
+ function shouldRetryWithNativeTyping(result) {
15532
+ return typeof result === "string" && result.startsWith("Error[type-not-applied]");
15533
+ }
15534
+ async function typeTextWithNativeInput(wc, selector, value) {
15535
+ const focusResult = await executePageScript(
15536
+ wc,
15537
+ `
15538
+ (async function() {
15539
+ ${FILLABLE_CONTROL_HELPERS}
15540
+ const el = ${selector.includes(" >>> ") ? `window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)})` : `document.querySelector(${JSON.stringify(selector)})`};
15541
+ if (!el) return { error: 'Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh.' };
15542
+ const target = await vesselFindFillTarget(el);
15543
+ if (!target || !target.el) {
15544
+ return { error: "Error[not-input]: Element is not text-editable. Click it first, then type into the opened text field." };
15545
+ }
15546
+ target.el.focus();
15547
+ return { label: vesselControlLabel(target.el, el) };
15548
+ })()
15549
+ `,
15550
+ {
15551
+ timeoutMs: 2500,
15552
+ label: "prepare native typing"
15553
+ }
15554
+ );
15555
+ if (focusResult === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
15556
+ if (!focusResult || typeof focusResult !== "object") {
15557
+ return "Error: Could not prepare native typing";
15558
+ }
15559
+ if (typeof focusResult.error === "string") return focusResult.error;
15560
+ wc.focus();
15561
+ const selectModifier = process.platform === "darwin" ? "meta" : "control";
15562
+ wc.sendInputEvent({ type: "keyDown", keyCode: "A", modifiers: [selectModifier] });
15563
+ await sleep(8);
15564
+ wc.sendInputEvent({ type: "keyUp", keyCode: "A", modifiers: [selectModifier] });
15565
+ await sleep(8);
15566
+ wc.sendInputEvent({ type: "keyDown", keyCode: "Backspace" });
15567
+ await sleep(4);
15568
+ wc.sendInputEvent({ type: "keyUp", keyCode: "Backspace" });
15569
+ for (const char of value) {
15570
+ wc.sendInputEvent({ type: "keyDown", keyCode: char });
15571
+ wc.sendInputEvent({ type: "char", keyCode: char });
15572
+ wc.sendInputEvent({ type: "keyUp", keyCode: char });
15573
+ await sleep(2);
15574
+ }
15575
+ await sleep(120);
15576
+ const verify = await executePageScript(
15577
+ wc,
15578
+ `
15579
+ (function() {
15580
+ ${FILLABLE_CONTROL_HELPERS}
15581
+ const active = vesselResolveFillableControl(document.activeElement);
15582
+ if (!active) return { ok: false, actual: "" };
15583
+ const actual = vesselReadFillableControlValue(active);
15584
+ return { ok: vesselValueWasApplied(active, ${JSON.stringify(value)}), actual: actual.slice(0, 80) };
15585
+ })()
15586
+ `,
15587
+ {
15588
+ timeoutMs: 1500,
15589
+ label: "verify native typing"
15590
+ }
15591
+ );
15592
+ if (verify === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
15593
+ if (!verify || verify.ok !== true) {
15594
+ const actual = verify && typeof verify.actual === "string" && verify.actual ? ` Current field text: "${verify.actual}".` : "";
15595
+ 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.`;
15596
+ }
15597
+ const label = typeof focusResult.label === "string" && focusResult.label ? focusResult.label : "input";
15598
+ return `Typed into: ${label} = ${value.slice(0, 80)} (native input)`;
15599
+ }
14797
15600
  async function resolveFieldSelector(wc, field) {
14798
15601
  const directSelector = await resolveSelector(wc, field.index, field.selector);
14799
15602
  if (directSelector) return directSelector;
@@ -14849,8 +15652,22 @@ async function resolveFieldSelector(wc, field) {
14849
15652
  return normalize(parts.join(" "));
14850
15653
  }
14851
15654
 
15655
+ function isNativeField(el) {
15656
+ return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
15657
+ }
15658
+
15659
+ function isCustomTextField(el) {
15660
+ if (!(el instanceof HTMLElement)) return false;
15661
+ var role = (el.getAttribute("role") || "").toLowerCase();
15662
+ return el.isContentEditable ||
15663
+ (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") ||
15664
+ role === "textbox" ||
15665
+ role === "searchbox" ||
15666
+ role === "combobox";
15667
+ }
15668
+
14852
15669
  function scoreField(el) {
14853
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
15670
+ if (!isNativeField(el) && !isCustomTextField(el)) {
14854
15671
  return -1;
14855
15672
  }
14856
15673
  if (!isVisible(el) || el.disabled || el.getAttribute("aria-disabled") === "true") {
@@ -14879,10 +15696,11 @@ async function resolveFieldSelector(wc, field) {
14879
15696
 
14880
15697
  if (score === 0) return -1;
14881
15698
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) score += 5;
15699
+ if (isCustomTextField(el)) score += 3;
14882
15700
  return score;
14883
15701
  }
14884
15702
 
14885
- const candidates = Array.from(document.querySelectorAll("input, textarea, select"));
15703
+ const candidates = Array.from(document.querySelectorAll("input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox'], [role='searchbox'], [role='combobox']"));
14886
15704
  let best = null;
14887
15705
  let bestScore = -1;
14888
15706
  for (const el of candidates) {
@@ -14932,136 +15750,73 @@ async function setElementValue(wc, selector, value) {
14932
15750
  const result2 = await executePageScript(
14933
15751
  wc,
14934
15752
  `
14935
- (function() {
15753
+ (async function() {
15754
+ ${FILLABLE_CONTROL_HELPERS}
14936
15755
  var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
14937
15756
  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");
15757
+ return vesselSetFillableControlValue(el, ${JSON.stringify(value)});
14960
15758
  })()
14961
15759
  `,
14962
15760
  {
14963
15761
  label: "type text in shadow input"
14964
15762
  }
14965
15763
  );
14966
- return result2 === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result2 || "Error: Could not type into element";
15764
+ if (result2 === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
15765
+ if (shouldRetryWithNativeTyping(result2)) {
15766
+ return typeTextWithNativeInput(wc, selector, value);
15767
+ }
15768
+ return result2 || "Error: Could not type into element";
14967
15769
  }
14968
15770
  const result = await executePageScript(
14969
15771
  wc,
14970
15772
  `
14971
- (function() {
15773
+ (async function() {
15774
+ ${FILLABLE_CONTROL_HELPERS}
14972
15775
  const el = document.querySelector(${JSON.stringify(selector)});
14973
15776
  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));
15777
+ return vesselSetFillableControlValue(el, ${JSON.stringify(value)});
15018
15778
  })()
15019
15779
  `,
15020
15780
  {
15021
15781
  label: "type text"
15022
15782
  }
15023
15783
  );
15024
- return result === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result || "Error: Could not type into element";
15784
+ if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("type_text");
15785
+ if (shouldRetryWithNativeTyping(result)) {
15786
+ return typeTextWithNativeInput(wc, selector, value);
15787
+ }
15788
+ return result || "Error: Could not type into element";
15025
15789
  }
15026
15790
  async function typeKeystroke(wc, selector, value) {
15791
+ if (selector.startsWith("__vessel_idx:")) {
15792
+ return setElementValue(wc, selector, value);
15793
+ }
15794
+ if (selector.includes(" >>> ")) {
15795
+ const result2 = await executePageScript(
15796
+ wc,
15797
+ `
15798
+ (async function() {
15799
+ ${FILLABLE_CONTROL_HELPERS}
15800
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
15801
+ if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
15802
+ return vesselTypeFillableControlKeystrokes(el, ${JSON.stringify(value)});
15803
+ })()
15804
+ `,
15805
+ {
15806
+ timeoutMs: 2500,
15807
+ label: "type keystrokes in shadow input"
15808
+ }
15809
+ );
15810
+ return result2 === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result2 || "Error: Could not type into element";
15811
+ }
15027
15812
  const result = await executePageScript(
15028
15813
  wc,
15029
15814
  `
15030
15815
  (async function() {
15816
+ ${FILLABLE_CONTROL_HELPERS}
15031
15817
  const el = document.querySelector(${JSON.stringify(selector)});
15032
15818
  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));
15819
+ return vesselTypeFillableControlKeystrokes(el, ${JSON.stringify(value)});
15065
15820
  })()
15066
15821
  `,
15067
15822
  {
@@ -17427,8 +18182,25 @@ async function locateImplicitTextTarget(wc) {
17427
18182
  }
17428
18183
 
17429
18184
  function isFillable(el) {
18185
+ if (!(el instanceof HTMLElement)) return false;
18186
+ if (
18187
+ el.getAttribute("aria-disabled") === "true" ||
18188
+ (el instanceof HTMLInputElement && (el.disabled || el.readOnly)) ||
18189
+ (el instanceof HTMLTextAreaElement && (el.disabled || el.readOnly))
18190
+ ) {
18191
+ return false;
18192
+ }
18193
+ const role = normalize(el.getAttribute("role"));
18194
+ if (
18195
+ el.isContentEditable ||
18196
+ (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") ||
18197
+ role === "textbox" ||
18198
+ role === "searchbox" ||
18199
+ role === "combobox"
18200
+ ) {
18201
+ return true;
18202
+ }
17430
18203
  if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return false;
17431
- if (el.disabled || el.readOnly || el.getAttribute("aria-disabled") === "true") return false;
17432
18204
  const type = el instanceof HTMLTextAreaElement ? "text" : normalize(el.getAttribute("type") || el.type || "text");
17433
18205
  return ["", "search", "text", "email", "url", "tel", "number", "password"].includes(type);
17434
18206
  }
@@ -17445,7 +18217,7 @@ async function locateImplicitTextTarget(wc) {
17445
18217
  }
17446
18218
 
17447
18219
  const candidates = Array.from(
17448
- document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]), textarea')
18220
+ document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]), textarea, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="searchbox"], [role="combobox"]')
17449
18221
  ).filter((el) => isFillable(el) && isVisible(el));
17450
18222
 
17451
18223
  let best = null;
@@ -17697,7 +18469,8 @@ async function handleWebSearch(ctx, tabId, args) {
17697
18469
  if (!tab || !tabId) return "Error: No active tab";
17698
18470
  const query = String(args.query || "").trim();
17699
18471
  if (!query) return "Error: No web search query provided.";
17700
- const shortcut = buildDefaultEngineShortcut(query);
18472
+ const taskGoal = ctx.runtime.getState().taskTracker?.goal;
18473
+ const shortcut = buildFlightSearchShortcut(query, taskGoal) ?? buildDefaultEngineShortcut(query);
17701
18474
  if (!shortcut) {
17702
18475
  return "Error: No default search engine is configured. Navigate to a search engine or set a default search engine first.";
17703
18476
  }
@@ -19059,9 +19832,11 @@ async function handleHighlight(ctx, args) {
19059
19832
  }
19060
19833
  return highlightOnPage(wc, selector, highlightText, args.label, args.durationMs, highlightColor);
19061
19834
  }
19062
- function handleClearHighlights(ctx) {
19835
+ async function handleClearHighlights(ctx) {
19063
19836
  const wc = ctx.tabManager.getActiveTab()?.view.webContents;
19064
19837
  if (!wc) return "Error: No active tab";
19838
+ const url = normalizeUrl$1(wc.getURL());
19839
+ clearHighlightsForUrl(url);
19065
19840
  return clearHighlights(wc);
19066
19841
  }
19067
19842
  function trimText(value) {
@@ -21870,7 +22645,7 @@ function registerPrivateIpcHandlers(state2) {
21870
22645
  });
21871
22646
  ipc.handle(Channels.IS_PRIVATE_MODE, () => true);
21872
22647
  ipc.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
21873
- createPrivateWindow();
22648
+ return openPrivateWindowSafely();
21874
22649
  });
21875
22650
  ipc.handle(Channels.OPEN_NEW_WINDOW, async () => {
21876
22651
  const { createSecondaryWindow: createSecondaryWindow2 } = await Promise.resolve().then(() => window$1);
@@ -21914,70 +22689,120 @@ function registerPrivateIpcHandlers(state2) {
21914
22689
  function createPrivateWindow() {
21915
22690
  const privateSessionPartition = `private-${crypto$2.randomUUID()}`;
21916
22691
  const privateSession = electron.session.fromPartition(privateSessionPartition);
21917
- privateSession.setUserAgent(electron.session.defaultSession.getUserAgent());
21918
- const win = new electron.BaseWindow({
21919
- width: 1280,
21920
- height: 800,
21921
- minWidth: 800,
21922
- minHeight: 600,
21923
- frame: false,
21924
- show: false,
21925
- backgroundColor: "#1e1a2e",
21926
- title: "Vessel - Private Browsing"
21927
- });
21928
- const chromeView = new electron.WebContentsView({
21929
- webPreferences: {
21930
- preload: path$1.join(__dirname, "../preload/index.js"),
21931
- sandbox: true,
21932
- contextIsolation: true,
21933
- nodeIntegration: false
22692
+ let win = null;
22693
+ let tabManager = null;
22694
+ let state2 = null;
22695
+ try {
22696
+ privateSession.setUserAgent(electron.session.defaultSession.getUserAgent());
22697
+ win = new electron.BaseWindow({
22698
+ width: 1280,
22699
+ height: 800,
22700
+ minWidth: 800,
22701
+ minHeight: 600,
22702
+ frame: false,
22703
+ show: false,
22704
+ backgroundColor: "#1e1a2e",
22705
+ title: "Vessel - Private Browsing"
22706
+ });
22707
+ const chromeView = new electron.WebContentsView({
22708
+ webPreferences: {
22709
+ preload: path$1.join(__dirname, "../preload/index.js"),
22710
+ sandbox: true,
22711
+ contextIsolation: true,
22712
+ nodeIntegration: false
22713
+ }
22714
+ });
22715
+ chromeView.setBackgroundColor("#00000000");
22716
+ win.contentView.addChildView(chromeView);
22717
+ tabManager = new TabManager(
22718
+ win,
22719
+ (tabs, activeId) => {
22720
+ sendSafe(chromeView.webContents, Channels.TAB_STATE_UPDATE, tabs, activeId);
22721
+ if (state2) layoutPrivateViews(state2);
22722
+ },
22723
+ { isPrivate: true, sessionPartition: privateSessionPartition }
22724
+ );
22725
+ state2 = {
22726
+ window: win,
22727
+ chromeView,
22728
+ tabManager,
22729
+ session: privateSession,
22730
+ sessionPartition: privateSessionPartition
22731
+ };
22732
+ installAdBlockingForSession(privateSession, tabManager);
22733
+ installDownloadHandlerForSession(privateSession, chromeView);
22734
+ registerPrivateIpcHandlers(state2);
22735
+ win.on("resize", () => {
22736
+ if (state2) layoutPrivateViews(state2);
22737
+ });
22738
+ win.on("show", () => {
22739
+ if (state2) layoutPrivateViews(state2);
22740
+ });
22741
+ win.on("closed", () => {
22742
+ if (!state2) return;
22743
+ privateWindows.delete(state2);
22744
+ tabManager?.destroyAllTabs();
22745
+ void Promise.all([
22746
+ privateSession.clearStorageData(),
22747
+ privateSession.clearCache()
22748
+ ]).catch((error) => {
22749
+ logger$m.warn("Failed to clear private browsing session:", error);
22750
+ });
22751
+ });
22752
+ privateWindows.add(state2);
22753
+ chromeView.webContents.once("dom-ready", () => {
22754
+ try {
22755
+ tabManager?.createTab("about:blank");
22756
+ if (state2) layoutPrivateViews(state2);
22757
+ } catch (error) {
22758
+ logger$m.error("Failed to initialize private browsing tab:", error);
22759
+ try {
22760
+ win?.close();
22761
+ } catch (cleanupError) {
22762
+ logger$m.warn("Failed to close private window after tab init failure:", cleanupError);
22763
+ }
22764
+ }
22765
+ });
22766
+ loadPrivateRenderer(chromeView);
22767
+ win.show();
22768
+ logger$m.info("Private browsing window opened");
22769
+ return state2;
22770
+ } catch (error) {
22771
+ logger$m.error("Failed to create private browsing window:", error);
22772
+ if (state2) {
22773
+ privateWindows.delete(state2);
22774
+ }
22775
+ try {
22776
+ tabManager?.destroyAllTabs();
22777
+ } catch (cleanupError) {
22778
+ logger$m.warn("Failed to clean up private tabs after launch failure:", cleanupError);
22779
+ }
22780
+ try {
22781
+ win?.close();
22782
+ } catch (cleanupError) {
22783
+ logger$m.warn("Failed to close private window after launch failure:", cleanupError);
21934
22784
  }
21935
- });
21936
- chromeView.setBackgroundColor("#00000000");
21937
- win.contentView.addChildView(chromeView);
21938
- const tabManager = new TabManager(
21939
- win,
21940
- (tabs, activeId) => {
21941
- sendSafe(chromeView.webContents, Channels.TAB_STATE_UPDATE, tabs, activeId);
21942
- layoutPrivateViews(state2);
21943
- },
21944
- { isPrivate: true, sessionPartition: privateSessionPartition }
21945
- );
21946
- const state2 = {
21947
- window: win,
21948
- chromeView,
21949
- tabManager,
21950
- session: privateSession,
21951
- sessionPartition: privateSessionPartition
21952
- };
21953
- installAdBlockingForSession(privateSession, tabManager);
21954
- installDownloadHandlerForSession(privateSession, chromeView);
21955
- registerPrivateIpcHandlers(state2);
21956
- win.on("resize", () => layoutPrivateViews(state2));
21957
- win.on("show", () => layoutPrivateViews(state2));
21958
- win.on("closed", () => {
21959
- privateWindows.delete(state2);
21960
- tabManager.destroyAllTabs();
21961
22785
  void Promise.all([
21962
22786
  privateSession.clearStorageData(),
21963
22787
  privateSession.clearCache()
21964
- ]).catch((error) => {
21965
- logger$m.warn("Failed to clear private browsing session:", error);
22788
+ ]).catch((cleanupError) => {
22789
+ logger$m.warn("Failed to clear failed private browsing session:", cleanupError);
21966
22790
  });
21967
- });
21968
- privateWindows.add(state2);
21969
- chromeView.webContents.once("dom-ready", () => {
21970
- tabManager.createTab("about:blank");
21971
- layoutPrivateViews(state2);
21972
- });
21973
- loadPrivateRenderer(chromeView);
21974
- win.show();
21975
- logger$m.info("Private browsing window opened");
21976
- return state2;
22791
+ throw error;
22792
+ }
22793
+ }
22794
+ function openPrivateWindowSafely() {
22795
+ try {
22796
+ createPrivateWindow();
22797
+ return true;
22798
+ } catch {
22799
+ return false;
22800
+ }
21977
22801
  }
21978
22802
  const window$2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
21979
22803
  __proto__: null,
21980
- createPrivateWindow
22804
+ createPrivateWindow,
22805
+ openPrivateWindowSafely
21981
22806
  }, Symbol.toStringTag, { value: "Module" }));
21982
22807
  const secondaryWindows = /* @__PURE__ */ new Set();
21983
22808
  function layoutSecondaryViews(state2) {
@@ -22095,8 +22920,8 @@ function registerSecondaryIpcHandlers(state2) {
22095
22920
  );
22096
22921
  ipc.handle(Channels.OPEN_NEW_WINDOW, () => createSecondaryWindow());
22097
22922
  ipc.handle(Channels.OPEN_PRIVATE_WINDOW, async () => {
22098
- const { createPrivateWindow: createPrivateWindow2 } = await Promise.resolve().then(() => window$2);
22099
- createPrivateWindow2();
22923
+ const { openPrivateWindowSafely: openPrivateWindowSafely2 } = await Promise.resolve().then(() => window$2);
22924
+ return openPrivateWindowSafely2();
22100
22925
  });
22101
22926
  ipc.handle(Channels.IS_PRIVATE_MODE, () => false);
22102
22927
  ipc.handle(Channels.WINDOW_MINIMIZE, () => state2.window.minimize());
@@ -22184,7 +23009,7 @@ function registerTabHandlers(windowState, _sendToRendererViews) {
22184
23009
  const { tabManager, mainWindow } = windowState;
22185
23010
  electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, (event) => {
22186
23011
  assertTrustedIpcSender(event);
22187
- createPrivateWindow();
23012
+ return openPrivateWindowSafely();
22188
23013
  });
22189
23014
  electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, (event) => {
22190
23015
  assertTrustedIpcSender(event);
@@ -22402,8 +23227,11 @@ const SHARED_NAVIGATION_INSTRUCTIONS = [
22402
23227
  "Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.",
22403
23228
  "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
23229
  "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.",
23230
+ "For questions about a named venue, business, organization, school, or local place, use search results to find the official site, then open that site and answer from its page. Do not keep rewriting generic web_search queries or switch to site: searches when an official or clearly direct result is available.",
23231
+ "For flight price-shopping, include the route, date, and trip type in web_search(query); do not send vague or partial flight queries.",
23232
+ "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
23233
  "On retail and marketplace sites, prefer the site's visible search box, filters, and result pages over direct product URLs.",
22406
- "For broad discovery tasks, prefer direct sources and site-specific search over generic search engines."
23234
+ "For broad discovery tasks, prefer direct sources over generic search snippets. Use site-specific search only after opening the direct source fails to expose the needed information."
22407
23235
  ];
22408
23236
  const VESSEL_SOURCE_INSTRUCTIONS = [
22409
23237
  "When the user asks about Vessel, Vessel Browser, or Quanta Intellect, use these official sources directly instead of hunting around the open web first: Official page https://quantaintellect.com, GitHub repo https://github.com/unmodeled-tyler/vessel-browser, npm package https://www.npmjs.com/package/@quanta-intellect/vessel-browser.",
@@ -23360,8 +24188,16 @@ function registerHighlightHandlers(windowState, sendToRendererViews) {
23360
24188
  const info = getActiveTabInfo(tabManager);
23361
24189
  if (!info) return false;
23362
24190
  try {
24191
+ const url = normalizeUrl$1(info.wc.getURL());
24192
+ const text = await getHighlightTextAtIndex(info.wc, validatedIndex);
23363
24193
  const removed = await removeHighlightAtIndex(info.wc, validatedIndex);
23364
24194
  if (removed) {
24195
+ if (text) {
24196
+ const persisted = findHighlightByText(url, text);
24197
+ if (persisted) {
24198
+ removeHighlight(persisted.id);
24199
+ }
24200
+ }
23365
24201
  await emitHighlightCount();
23366
24202
  }
23367
24203
  return removed;
@@ -23375,6 +24211,8 @@ function registerHighlightHandlers(windowState, sendToRendererViews) {
23375
24211
  const info = getActiveTabInfo(tabManager);
23376
24212
  if (!info) return false;
23377
24213
  try {
24214
+ const url = normalizeUrl$1(info.wc.getURL());
24215
+ clearHighlightsForUrl(url);
23378
24216
  const cleared = await clearAllHighlightElements(info.wc);
23379
24217
  if (cleared) {
23380
24218
  await emitHighlightCount();