@quanta-intellect/vessel-browser 0.1.141 → 0.1.144

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(`
@@ -8751,7 +8764,24 @@ function recoverNarratedActionToolCalls(text, availableToolNames) {
8751
8764
  }
8752
8765
  return recovered;
8753
8766
  }
8754
- const DOLLAR_PRICE_RE = /\$\s?\d{2,4}(?:[,.]\d{2})?\b/;
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/;
8755
8785
  const FLIGHT_TASK_RE = /\b(?:flight|flights|airfare|air fare|plane ticket|airline|airport|google flights|pdx|sfo|san francisco|portland)\b/i;
8756
8786
  const SHOPPING_INTENT_RE = /\b(?:cheap|cheapest|price|prices|fare|fares|one[- ]?way|round[- ]?trip|depart|departure|arrive|arrival|from|to)\b/i;
8757
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;
@@ -8787,6 +8817,106 @@ function buildFlightPriceEvidenceRecoveryPrompt(userMessage, assistantText, late
8787
8817
  `Last unsupported answer: ${assistantText.replace(/\s+/g, " ").trim().slice(0, 500) || "(empty)"}`
8788
8818
  ].join("\n");
8789
8819
  }
8820
+ const SEARCH_HISTORY_LIMIT = 4;
8821
+ function normalizeSearchToolQuery(name, args) {
8822
+ if (name !== "search" && name !== "web_search") return null;
8823
+ const raw = typeof args.query === "string" ? args.query : typeof args.text === "string" ? args.text : typeof args.term === "string" ? args.term : "";
8824
+ const normalized = raw.replace(/\s+/g, " ").trim().toLowerCase();
8825
+ return normalized || null;
8826
+ }
8827
+ function buildLatestStateReminder(toolResultPreview) {
8828
+ const text = (toolResultPreview || "").trim();
8829
+ if (!text) return "";
8830
+ const existingReminder = text.match(
8831
+ /\bLatest browser state:\s*URL\s+.+?(?:Trust the latest tool result over the initial page context\.|$)/i
8832
+ )?.[0]?.trim();
8833
+ if (existingReminder) return existingReminder;
8834
+ const stateMatch = text.match(
8835
+ /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
8836
+ );
8837
+ if (stateMatch) {
8838
+ const url = stateMatch[1]?.trim();
8839
+ const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
8840
+ if (url) {
8841
+ return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
8842
+ }
8843
+ }
8844
+ const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8845
+ const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8846
+ if (structuredUrl) {
8847
+ return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8848
+ }
8849
+ const navigatedUrl = text.match(
8850
+ /\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i
8851
+ )?.[1]?.trim() ?? text.match(
8852
+ /\b(?:web\s+)?searched "[^"]+"[^\n]*?(?:->|→)\s+([^\s\n]+)/i
8853
+ )?.[1]?.trim();
8854
+ const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
8855
+ if (navigatedUrl) {
8856
+ return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8857
+ }
8858
+ return "";
8859
+ }
8860
+ function buildRepeatedSearchError(previousTool, previousQuery, latestToolResultPreview, mode) {
8861
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8862
+ const lines = [
8863
+ mode === "drifted" ? `Error: You already performed ${previousTool} successfully for this task.` : `Error: You already searched for "${previousQuery}" successfully with ${previousTool}.`,
8864
+ 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.`,
8865
+ `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.`,
8866
+ `Take the next action from the results you already have: click a result, inspect a specific item, or provide the final answer to the user. Do not call any search tool again as preparation, and do not call read_page as preparation for another search.`
8867
+ ];
8868
+ if (stateReminder) {
8869
+ lines.push(stateReminder);
8870
+ }
8871
+ return lines.join(" ");
8872
+ }
8873
+ class SearchLoopGuard {
8874
+ recentSuccessfulSearchQueries = [];
8875
+ recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
8876
+ lastSuccessfulWebSearchQuery = null;
8877
+ isContextResettingTool;
8878
+ constructor(isContextResettingTool) {
8879
+ this.isContextResettingTool = isContextResettingTool;
8880
+ }
8881
+ /**
8882
+ * Check whether a search/web_search call should be suppressed. Returns the
8883
+ * details needed to build the corrective error, or null if the call is OK.
8884
+ */
8885
+ check(toolName, query) {
8886
+ const isRepeatedSearchAcrossTools = query !== null && this.recentSuccessfulSearchQueries.includes(query);
8887
+ const isQueryDriftedWebSearch = toolName === "web_search" && this.lastSuccessfulWebSearchQuery !== null && query !== null && query !== this.lastSuccessfulWebSearchQuery;
8888
+ if (!isRepeatedSearchAcrossTools && !isQueryDriftedWebSearch) return null;
8889
+ const mode = isRepeatedSearchAcrossTools ? "repeated" : "drifted";
8890
+ const previousTool = isRepeatedSearchAcrossTools ? this.recentSuccessfulSearchToolByQuery.get(query ?? "") ?? (toolName === "web_search" ? "search" : "web_search") : "web_search";
8891
+ const previousQuery = isRepeatedSearchAcrossTools ? query ?? "" : this.lastSuccessfulWebSearchQuery ?? "";
8892
+ return { mode, previousTool, previousQuery };
8893
+ }
8894
+ /**
8895
+ * Record a successfully executed tool. Search queries are added to the
8896
+ * recent-history ring buffer, and real-progress tools clear the drift anchor
8897
+ * so a later distinct search is not flagged as drift.
8898
+ */
8899
+ recordSuccess(toolName, query, wasSuccessful) {
8900
+ if (wasSuccessful && this.isContextResettingTool(toolName)) {
8901
+ this.lastSuccessfulWebSearchQuery = null;
8902
+ }
8903
+ if (wasSuccessful && query) {
8904
+ if (!this.recentSuccessfulSearchQueries.includes(query)) {
8905
+ this.recentSuccessfulSearchQueries.push(query);
8906
+ this.recentSuccessfulSearchToolByQuery.set(query, toolName);
8907
+ if (this.recentSuccessfulSearchQueries.length > SEARCH_HISTORY_LIMIT) {
8908
+ const dropped = this.recentSuccessfulSearchQueries.shift();
8909
+ if (dropped) {
8910
+ this.recentSuccessfulSearchToolByQuery.delete(dropped);
8911
+ }
8912
+ }
8913
+ }
8914
+ }
8915
+ if (wasSuccessful && toolName === "web_search" && query) {
8916
+ this.lastSuccessfulWebSearchQuery = query;
8917
+ }
8918
+ }
8919
+ }
8790
8920
  const logger$v = createLogger("OpenAIProvider");
8791
8921
  function shouldDebugAgentLoop() {
8792
8922
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
@@ -8837,10 +8967,26 @@ function toOpenAIReasoningEffort(effort, providerId, model) {
8837
8967
  return void 0;
8838
8968
  }
8839
8969
  }
8970
+ function openRouterRoutingOptions(providerId) {
8971
+ if (providerId !== "openrouter") return {};
8972
+ return {
8973
+ provider: {
8974
+ require_parameters: true,
8975
+ sort: "latency"
8976
+ }
8977
+ };
8978
+ }
8979
+ function buildOpenRouterAttributionHeaders() {
8980
+ return {
8981
+ "HTTP-Referer": "https://github.com/unmodeled-tyler/vessel-browser",
8982
+ "X-OpenRouter-Title": "Vessel Browser",
8983
+ "X-OpenRouter-Categories": "personal-agent,general-chat"
8984
+ };
8985
+ }
8840
8986
  function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
8841
8987
  if (profile !== "compact") return null;
8842
8988
  const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
8843
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
8989
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8844
8990
  return {
8845
8991
  role: "user",
8846
8992
  content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
@@ -8850,13 +8996,13 @@ ${phaseReminder}` : "")
8850
8996
  };
8851
8997
  }
8852
8998
  function extractSingleGoalDomain(goal) {
8853
- const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
8999
+ const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.[a-z]{2,})\b/g);
8854
9000
  if (!matches || matches.length !== 1) return null;
8855
9001
  return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
8856
9002
  }
8857
9003
  function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
8858
9004
  const phaseReminder = buildPhaseReminder(userMessage, assistantText);
8859
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
9005
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8860
9006
  const goalDomain = extractSingleGoalDomain(userMessage);
8861
9007
  const latest = (latestToolResultPreview || "").toLowerCase();
8862
9008
  const assistant = assistantText.toLowerCase();
@@ -8953,30 +9099,18 @@ function buildPhaseReminder(userMessage, assistantText) {
8953
9099
  }
8954
9100
  return "";
8955
9101
  }
8956
- function buildLatestStateReminder(toolResultPreview) {
8957
- const text = toolResultPreview.trim();
8958
- if (!text) return "";
8959
- const stateMatch = text.match(
8960
- /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
8961
- );
8962
- if (stateMatch) {
8963
- const url = stateMatch[1]?.trim();
8964
- const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
8965
- if (url) {
8966
- return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
8967
- }
8968
- }
8969
- const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8970
- const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8971
- if (structuredUrl) {
8972
- return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8973
- }
8974
- const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
8975
- const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
8976
- if (navigatedUrl) {
8977
- return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8978
- }
8979
- return "";
9102
+ function isSearchContextResettingTool(name) {
9103
+ return ![
9104
+ "read_page",
9105
+ "current_tab",
9106
+ "list_tabs",
9107
+ "screenshot",
9108
+ "clear_overlays",
9109
+ "accept_cookies",
9110
+ "dismiss_popup",
9111
+ "web_search",
9112
+ "search"
9113
+ ].includes(name);
8980
9114
  }
8981
9115
  function shouldRecoverCompactStall(text, userMessage) {
8982
9116
  const trimmed = text.trim().toLowerCase();
@@ -9061,10 +9195,24 @@ function logAgentLoopDebug(payload) {
9061
9195
  }
9062
9196
  }
9063
9197
  function formatOpenAICompatErrorMessage(providerId, message) {
9198
+ if (providerId === "openrouter" && /(timed out after \d+(?:\.\d+)? seconds|request timed out|returned none after all retries|returned no content|empty response)/i.test(
9199
+ message
9200
+ )) {
9201
+ return [
9202
+ message,
9203
+ "OpenRouter reported an upstream model timeout/no-content failure.",
9204
+ "If this persists, retry or pin a specific low-latency tool-calling model instead of the free router."
9205
+ ].join(" ");
9206
+ }
9064
9207
  if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
9065
9208
  message
9066
9209
  )) {
9067
- return `${message} llama.cpp sets context size at server startup, not per request. Vessel's agent prompt plus tool schema is about 6.5k tokens before browsing history, so run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended).`;
9210
+ return [
9211
+ message,
9212
+ "llama.cpp sets context size at server startup, not per request.",
9213
+ `Vessel's agent prompt plus tool schema is about 6.5k tokens before browsing history, so run llama-server with`,
9214
+ `--ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended).`
9215
+ ].join(" ");
9068
9216
  }
9069
9217
  return message;
9070
9218
  }
@@ -9083,10 +9231,7 @@ class OpenAICompatProvider {
9083
9231
  apiKey: config.apiKey || "ollama",
9084
9232
  baseURL,
9085
9233
  ...isOpenRouter && {
9086
- defaultHeaders: {
9087
- "HTTP-Referer": "https://github.com/unmodeled/vessel-browser",
9088
- "X-Title": "Vessel"
9089
- }
9234
+ defaultHeaders: buildOpenRouterAttributionHeaders()
9090
9235
  }
9091
9236
  });
9092
9237
  this.providerId = config.id;
@@ -9113,6 +9258,7 @@ class OpenAICompatProvider {
9113
9258
  max_tokens: 4096,
9114
9259
  stream: true,
9115
9260
  messages,
9261
+ ...openRouterRoutingOptions(this.providerId),
9116
9262
  ...openAIPromptCacheOptions({
9117
9263
  providerId: this.providerId,
9118
9264
  model: this.model,
@@ -9171,9 +9317,12 @@ class OpenAICompatProvider {
9171
9317
  let iterationsUsed = 0;
9172
9318
  let compactRecoveryCount = 0;
9173
9319
  let flightPriceEvidenceRecoveryCount = 0;
9320
+ let highlightCompletionRecoveryCount = 0;
9174
9321
  let compactCorrectionCount = 0;
9175
9322
  const recentCompactToolSignatures = [];
9176
9323
  const recentToolNames = [];
9324
+ const successfulToolNames = [];
9325
+ const searchLoopGuard = new SearchLoopGuard(isSearchContextResettingTool);
9177
9326
  let clickReadLoopNudged = false;
9178
9327
  for (let i = 0; i < maxIterations; i++) {
9179
9328
  iterationsUsed = i + 1;
@@ -9195,6 +9344,7 @@ class OpenAICompatProvider {
9195
9344
  tools: openAITools,
9196
9345
  tool_choice: "auto",
9197
9346
  temperature: agentTemperatureForProfile(this.agentToolProfile),
9347
+ ...openRouterRoutingOptions(this.providerId),
9198
9348
  ...openAIPromptCacheOptions({
9199
9349
  providerId: this.providerId,
9200
9350
  model: this.model,
@@ -9342,6 +9492,19 @@ class OpenAICompatProvider {
9342
9492
  });
9343
9493
  continue;
9344
9494
  }
9495
+ if (highlightCompletionRecoveryCount < 1 && shouldRetryUnexecutedHighlightCompletion(
9496
+ userMessage,
9497
+ textAccum,
9498
+ successfulToolNames
9499
+ )) {
9500
+ highlightCompletionRecoveryCount += 1;
9501
+ if (textAccum.trim()) onChunk("<<erase_prev>>");
9502
+ messages.push({
9503
+ role: "user",
9504
+ content: `[System] ${buildHighlightToolCompletionPrompt()}`
9505
+ });
9506
+ continue;
9507
+ }
9345
9508
  break;
9346
9509
  }
9347
9510
  compactRecoveryCount = 0;
@@ -9394,6 +9557,25 @@ class OpenAICompatProvider {
9394
9557
  args = repairedArgs.args;
9395
9558
  args = coerceToolArgsForExecution(tc.name, args);
9396
9559
  const toolSignature = stableToolSignature(tc.name, args);
9560
+ const searchToolQuery = normalizeSearchToolQuery(tc.name, args);
9561
+ const searchLoopCheck = searchLoopGuard.check(tc.name, searchToolQuery);
9562
+ if (searchLoopCheck) {
9563
+ onChunk(`
9564
+ <<tool:${tc.name}:↻ duplicate suppressed>>
9565
+ `);
9566
+ messages.push({
9567
+ role: "tool",
9568
+ tool_call_id: tc.id,
9569
+ content: buildRepeatedSearchError(
9570
+ searchLoopCheck.previousTool,
9571
+ searchLoopCheck.previousQuery,
9572
+ latestToolMessage ? String(latestToolMessage.content || "") : null,
9573
+ searchLoopCheck.mode
9574
+ )
9575
+ });
9576
+ compactCorrectionCount += 1;
9577
+ continue;
9578
+ }
9397
9579
  if (this.agentToolProfile === "compact" && tc.name === "click" && isTargetlessClickArgs(args)) {
9398
9580
  onChunk(`
9399
9581
  <<tool:${tc.name}:⚠ missing target>>
@@ -9468,6 +9650,15 @@ class OpenAICompatProvider {
9468
9650
  recentCompactToolSignatures.shift();
9469
9651
  }
9470
9652
  }
9653
+ const toolSucceeded = !/^Error:/i.test(toolContent.trim());
9654
+ if (toolSucceeded) {
9655
+ successfulToolNames.push(tc.name);
9656
+ }
9657
+ searchLoopGuard.recordSuccess(
9658
+ tc.name,
9659
+ searchToolQuery,
9660
+ toolSucceeded
9661
+ );
9471
9662
  recentToolNames.push(tc.name);
9472
9663
  if (recentToolNames.length > 8) recentToolNames.shift();
9473
9664
  if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
@@ -9997,59 +10188,13 @@ function previewToolResult(text, maxLength = 800) {
9997
10188
  if (normalized.length <= maxLength) return normalized;
9998
10189
  return `${normalized.slice(0, maxLength)}...`;
9999
10190
  }
10000
- function normalizedSearchToolQuery(name, args) {
10001
- if (name !== "search" && name !== "web_search") return null;
10002
- const raw = typeof args.query === "string" ? args.query : typeof args.text === "string" ? args.text : typeof args.term === "string" ? args.term : "";
10003
- const normalized = raw.replace(/\s+/g, " ").trim().toLowerCase();
10004
- return normalized || null;
10005
- }
10006
10191
  function hasBlockingOverlaySignal(text) {
10007
10192
  if (!text) return false;
10008
10193
  if (/\bno blocking overlays detected\b/i.test(text)) return false;
10009
10194
  return /\bwarning:\s*blocking overlay detected\b/i.test(text) || /\bblocked-by-overlay\b/i.test(text) || /###\s*immediate blockers\b/i.test(text) || /\bblocking overlays\W+[1-9]\d*\b/i.test(text);
10010
10195
  }
10011
- function buildCodexLatestStateReminder(toolResultPreview) {
10012
- const text = (toolResultPreview || "").trim();
10013
- if (!text) return "";
10014
- const existingReminder = text.match(
10015
- /\bLatest browser state:\s*URL\s+.+?(?:Trust the latest tool result over the initial page context\.|$)/i
10016
- )?.[0]?.trim();
10017
- if (existingReminder) return existingReminder;
10018
- const stateMatch = text.match(
10019
- /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
10020
- );
10021
- if (stateMatch) {
10022
- const url = stateMatch[1]?.trim();
10023
- const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
10024
- if (url) {
10025
- return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
10026
- }
10027
- }
10028
- const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to)\s+([^\s\n]+)/i)?.[1]?.trim() ?? text.match(/\b(?:web\s+)?searched "[^"]+"[^\n]*?(?:->|→)\s+([^\s\n]+)/i)?.[1]?.trim();
10029
- const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
10030
- if (navigatedUrl) {
10031
- return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
10032
- }
10033
- return "";
10034
- }
10035
- function buildCodexRepeatedSearchError(previousTool, previousQuery, latestToolResultPreview, mode) {
10036
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10037
- const header = mode === "drifted" ? `Error: You already performed ${previousTool} successfully for this task.` : `Error: You already searched for "${previousQuery}" successfully with ${previousTool}.`;
10038
- const lines = [
10039
- header,
10040
- mode === "drifted" ? `Do not rewrite or broaden the query with another ${previousTool}. The latest results from your previous ${previousTool} are already in the conversation context.` : `Do not search the same query again with ${previousTool} (or its alias search/web_search). The latest results from your previous ${previousTool} are already in the conversation context.`,
10041
- // The key change: do NOT suggest read_page as a "recovery" action.
10042
- // The model was using read_page as a no-op to reset the strike counter
10043
- // and then issue another web_search. The prior results are sufficient.
10044
- `Take the next action from the results you already have: click a result link, call inspect_element on a specific item, highlight, or provide the final answer to the user. Do not call any search tool again, and do not call read_page as preparation for another search.`
10045
- ];
10046
- if (stateReminder) {
10047
- lines.push(stateReminder);
10048
- }
10049
- return lines.join(" ");
10050
- }
10051
10196
  function buildCodexUnsupportedClearOverlayError(latestToolResultPreview) {
10052
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10197
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10053
10198
  const lines = [
10054
10199
  `Error: No blocking overlay signal is present in the latest browser state.`,
10055
10200
  `Do not call clear_overlays unless read_page or the page context explicitly reports a blocking overlay.`,
@@ -10169,7 +10314,7 @@ function shouldRetryCodexToolLoop(text, hasToolHistory) {
10169
10314
  return false;
10170
10315
  }
10171
10316
  function buildCodexRecoveryInput(userMessage, assistantText, latestToolResultPreview) {
10172
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10317
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10173
10318
  const lines = [
10174
10319
  `[System] The task is still in progress: ${userMessage}`,
10175
10320
  `Do not ask the user what they want next unless the original request is genuinely ambiguous or blocked.`,
@@ -10202,7 +10347,7 @@ function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText,
10202
10347
  };
10203
10348
  }
10204
10349
  function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
10205
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10350
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10206
10351
  const lines = [
10207
10352
  `[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`,
10208
10353
  `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.`
@@ -10421,9 +10566,7 @@ class CodexProvider {
10421
10566
  let clickReadLoopNudged = false;
10422
10567
  let latestToolResultPreview = null;
10423
10568
  let failedClickCountSinceProgress = 0;
10424
- const recentSuccessfulSearchQueries = [];
10425
- const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
10426
- let lastSuccessfulWebSearchQuery = null;
10569
+ const searchLoopGuard = new SearchLoopGuard(isRealProgressTool);
10427
10570
  try {
10428
10571
  for (let i = 0; i < maxIterations; i++) {
10429
10572
  iterationsUsed = i + 1;
@@ -10513,30 +10656,29 @@ class CodexProvider {
10513
10656
  prepared.prepared.name,
10514
10657
  prepared.prepared.args
10515
10658
  );
10516
- const searchToolQuery = normalizedSearchToolQuery(
10659
+ const searchToolQuery = normalizeSearchToolQuery(
10517
10660
  prepared.prepared.name,
10518
10661
  prepared.prepared.args
10519
10662
  );
10520
- const isRepeatedSearchAcrossTools = searchToolQuery !== null && recentSuccessfulSearchQueries.includes(searchToolQuery);
10521
- const isQueryDriftedWebSearch = prepared.prepared.name === "web_search" && lastSuccessfulWebSearchQuery !== null && searchToolQuery !== null && searchToolQuery !== lastSuccessfulWebSearchQuery;
10663
+ const searchLoopCheck = searchLoopGuard.check(
10664
+ prepared.prepared.name,
10665
+ searchToolQuery
10666
+ );
10522
10667
  const isUnsupportedClearOverlay = prepared.prepared.name === "clear_overlays" && !hasBlockingOverlaySignal(
10523
10668
  `${systemPrompt}
10524
10669
  ${latestToolResultPreview || ""}`
10525
10670
  );
10526
- if (isRepeatedSearchAcrossTools || isQueryDriftedWebSearch) {
10671
+ if (searchLoopCheck) {
10527
10672
  onChunk(`
10528
10673
  <<tool:${prepared.prepared.name}:↻ duplicate suppressed>>
10529
10674
  `);
10530
- const previousTool = isRepeatedSearchAcrossTools ? recentSuccessfulSearchToolByQuery.get(searchToolQuery ?? "") ?? (prepared.prepared.name === "web_search" ? "search" : "web_search") : "web_search";
10531
- const previousQuery = isRepeatedSearchAcrossTools ? searchToolQuery ?? "" : lastSuccessfulWebSearchQuery ?? "";
10532
- const mode = isRepeatedSearchAcrossTools ? "repeated" : "drifted";
10533
10675
  const output2 = createCodexToolOutput(
10534
10676
  prepared.prepared.callId,
10535
- buildCodexRepeatedSearchError(
10536
- previousTool,
10537
- previousQuery,
10677
+ buildRepeatedSearchError(
10678
+ searchLoopCheck.previousTool,
10679
+ searchLoopCheck.previousQuery,
10538
10680
  latestToolResultPreview,
10539
- mode
10681
+ searchLoopCheck.mode
10540
10682
  )
10541
10683
  );
10542
10684
  currentInput.push(output2);
@@ -10590,30 +10732,15 @@ ${latestToolResultPreview || ""}`
10590
10732
  toolHistoryCount += 1;
10591
10733
  latestToolResultPreview = previewToolResult(output.output);
10592
10734
  const outputText = toolResultTextContent(output.output);
10593
- if (!looksLikeFailedToolOutput(outputText)) {
10594
- if (isRealProgressTool(prepared.prepared.name)) {
10595
- lastSuccessfulWebSearchQuery = null;
10596
- failedClickCountSinceProgress = 0;
10597
- }
10598
- }
10599
- if (searchToolQuery && !looksLikeFailedToolOutput(outputText) && !recentSuccessfulSearchQueries.includes(searchToolQuery)) {
10600
- recentSuccessfulSearchQueries.push(searchToolQuery);
10601
- recentSuccessfulSearchToolByQuery.set(
10602
- searchToolQuery,
10603
- prepared.prepared.name
10604
- );
10605
- if (recentSuccessfulSearchQueries.length > 4) {
10606
- const dropped = recentSuccessfulSearchQueries.shift();
10607
- if (dropped) {
10608
- recentSuccessfulSearchToolByQuery.delete(dropped);
10609
- }
10610
- }
10611
- }
10612
- if (prepared.prepared.name === "web_search" && !looksLikeFailedToolOutput(outputText)) {
10613
- if (searchToolQuery) {
10614
- lastSuccessfulWebSearchQuery = searchToolQuery;
10615
- }
10735
+ const toolSucceeded = !looksLikeFailedToolOutput(outputText);
10736
+ if (toolSucceeded && isRealProgressTool(prepared.prepared.name)) {
10737
+ failedClickCountSinceProgress = 0;
10616
10738
  }
10739
+ searchLoopGuard.recordSuccess(
10740
+ prepared.prepared.name,
10741
+ searchToolQuery,
10742
+ toolSucceeded
10743
+ );
10617
10744
  if (prepared.prepared.name === "click" && looksLikeFailedToolOutput(outputText)) {
10618
10745
  failedClickCountSinceProgress += 1;
10619
10746
  currentInput.push(
@@ -11529,13 +11656,15 @@ const TOOL_DEFINITIONS = [
11529
11656
  {
11530
11657
  name: "highlight",
11531
11658
  title: "Highlight Element",
11532
- description: "Visually highlight an element or text on the page for the user. Use to draw attention to specific content. Highlights persist until cleared.",
11659
+ 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.",
11533
11660
  inputSchema: {
11534
- index: zod.z.number().optional().describe("Element index from page content to highlight"),
11535
- selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
11536
11661
  text: normalizedOptionalStringSchema().describe(
11537
- "Text to find and highlight on the page (all occurrences)"
11662
+ "Exact visible text/title to find and highlight on the page. Preferred for story titles, result titles, links, headings, and passages."
11663
+ ),
11664
+ index: zod.z.number().optional().describe(
11665
+ "Element index from the latest page content listing. Use only when it identifies the exact item to highlight."
11538
11666
  ),
11667
+ selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
11539
11668
  label: zod.z.string().optional().describe("Annotation label to display near the highlight"),
11540
11669
  durationMs: zod.z.number().optional().describe(
11541
11670
  "Auto-clear after this many milliseconds (omit for permanent)"
@@ -11647,7 +11776,7 @@ const TOOL_DEFINITIONS = [
11647
11776
  {
11648
11777
  name: "web_search",
11649
11778
  title: "Web Search",
11650
- description: "Search the open web using the configured default search engine. Use this for broad discovery tasks instead of typing into the current page.",
11779
+ 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.",
11651
11780
  inputSchema: {
11652
11781
  query: zod.z.string().describe("Web search query text")
11653
11782
  },
@@ -12568,17 +12697,23 @@ function formatCartSnapshot(page) {
12568
12697
  function isVisibleToUser(el) {
12569
12698
  return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
12570
12699
  }
12700
+ function elementSearchText(el, extraFields = []) {
12701
+ const fields = [
12702
+ el.text,
12703
+ el.label,
12704
+ el.name,
12705
+ el.placeholder,
12706
+ el.description,
12707
+ el.href,
12708
+ ...extraFields.map((field) => el[field])
12709
+ ];
12710
+ return normalizeComparable(fields.filter(Boolean).join(" "));
12711
+ }
12712
+ function formatElementOptions(options, maxOptions) {
12713
+ return options?.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|") ?? "";
12714
+ }
12571
12715
  function purchaseActionPriority(el) {
12572
- const haystack = normalizeComparable(
12573
- [
12574
- el.text,
12575
- el.label,
12576
- el.name,
12577
- el.placeholder,
12578
- el.description,
12579
- el.href
12580
- ].filter(Boolean).join(" ")
12581
- );
12716
+ const haystack = elementSearchText(el);
12582
12717
  if (!haystack) return Number.POSITIVE_INFINITY;
12583
12718
  if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
12584
12719
  if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) {
@@ -12589,6 +12724,27 @@ function purchaseActionPriority(el) {
12589
12724
  }
12590
12725
  return Number.POSITIVE_INFINITY;
12591
12726
  }
12727
+ function dateOrShowtimeControlPriority(el) {
12728
+ const haystack = elementSearchText(el, ["role"]);
12729
+ if (!haystack) return Number.POSITIVE_INFINITY;
12730
+ if (/\b(today|tomorrow|mon(?:day)?|tue(?:s|sday)?|wed(?:nesday)?|thu(?:rs|rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/.test(
12731
+ haystack
12732
+ )) {
12733
+ return 0;
12734
+ }
12735
+ if (/\b(showtimes?|showings?|screenings?|movie times?|date|calendar)\b/.test(
12736
+ haystack
12737
+ )) {
12738
+ return 1;
12739
+ }
12740
+ if (/\b(ticketing|tickets?|formovietickets|seat selection)\b/.test(haystack)) {
12741
+ return 2;
12742
+ }
12743
+ return Number.POSITIVE_INFINITY;
12744
+ }
12745
+ function isDateOrShowtimeControl(el) {
12746
+ return Number.isFinite(dateOrShowtimeControlPriority(el));
12747
+ }
12592
12748
  function isPurchaseActionElement(el) {
12593
12749
  if (el.type !== "button" && el.type !== "link" && !(el.type === "input" && (el.inputType === "submit" || el.inputType === "button"))) {
12594
12750
  return false;
@@ -12678,20 +12834,34 @@ function formatDialogFocus(page) {
12678
12834
  }
12679
12835
  function formatInteractiveElements(elements) {
12680
12836
  if (elements.length === 0) return "None";
12837
+ const DIALOG_PRIORITY_BONUS = 40;
12838
+ const HIDDEN_VISIBILITY_PENALTY = 100;
12839
+ const OFFSCREEN_PENALTY = 50;
12840
+ const NAVIGATION_CONTEXT_PENALTY = 30;
12841
+ const OBSCURED_PENALTY = 20;
12842
+ const LINK_TYPE_PENALTY = 5;
12843
+ const PURCHASE_BASE_WEIGHT = 25;
12844
+ const PURCHASE_PRIORITY_MULTIPLIER = 5;
12845
+ const DATE_BASE_WEIGHT = 18;
12846
+ const DATE_PRIORITY_MULTIPLIER = 4;
12681
12847
  const sorted = [...elements].sort((a, b) => {
12682
12848
  const scoreEl = (el) => {
12683
12849
  let s = 0;
12684
- if (el.context === "dialog") s -= 40;
12850
+ if (el.context === "dialog") s -= DIALOG_PRIORITY_BONUS;
12685
12851
  const purchasePriority = purchaseActionPriority(el);
12686
12852
  if (Number.isFinite(purchasePriority)) {
12687
- s -= 25 - purchasePriority * 5;
12853
+ s -= PURCHASE_BASE_WEIGHT - purchasePriority * PURCHASE_PRIORITY_MULTIPLIER;
12854
+ }
12855
+ const datePriority = dateOrShowtimeControlPriority(el);
12856
+ if (Number.isFinite(datePriority)) {
12857
+ s -= DATE_BASE_WEIGHT - datePriority * DATE_PRIORITY_MULTIPLIER;
12688
12858
  }
12689
- if (el.visible === false) s += 100;
12690
- if (el.inViewport === false) s += 50;
12859
+ if (el.visible === false) s += HIDDEN_VISIBILITY_PENALTY;
12860
+ if (el.inViewport === false) s += OFFSCREEN_PENALTY;
12691
12861
  if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
12692
- s += 30;
12693
- if (el.obscured) s += 20;
12694
- if (el.type === "link") s += 5;
12862
+ s += NAVIGATION_CONTEXT_PENALTY;
12863
+ if (el.obscured) s += OBSCURED_PENALTY;
12864
+ if (el.type === "link") s += LINK_TYPE_PENALTY;
12695
12865
  return s;
12696
12866
  };
12697
12867
  return scoreEl(a) - scoreEl(b);
@@ -12722,9 +12892,8 @@ function formatInteractiveElements(elements) {
12722
12892
  if (summary) parts.push(`${summary.label}="${summary.value}"`);
12723
12893
  appendFieldAffordances(parts, el);
12724
12894
  if (el.options?.length) {
12725
- parts.push(
12726
- `options=${el.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12727
- );
12895
+ const maxOptions = isDateOrShowtimeControl(el) ? 10 : 5;
12896
+ parts.push(`options=${formatElementOptions(el.options, maxOptions)}`);
12728
12897
  }
12729
12898
  } else if (el.type === "textarea") {
12730
12899
  parts.push(`[${el.label || "Text Area"}]`);
@@ -12785,8 +12954,9 @@ function formatForms(forms) {
12785
12954
  if (summary) fieldParts.push(`${summary.label}="${summary.value}"`);
12786
12955
  appendFieldAffordances(fieldParts, field);
12787
12956
  if (field.options?.length) {
12957
+ const maxOptions = isDateOrShowtimeControl(field) ? 10 : 5;
12788
12958
  fieldParts.push(
12789
- `options=${field.options.slice(0, 5).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12959
+ `options=${formatElementOptions(field.options, maxOptions)}`
12790
12960
  );
12791
12961
  }
12792
12962
  } else if (field.type === "textarea") {
@@ -13051,7 +13221,66 @@ function chooseAgentReadMode(page) {
13051
13221
  return "visible_only";
13052
13222
  }
13053
13223
  }
13224
+ const SITE_RESULT_FILTERS = [
13225
+ {
13226
+ hostname: "news.ycombinator.com",
13227
+ listingPaths: [
13228
+ "/",
13229
+ "/news",
13230
+ "/newest",
13231
+ "/front",
13232
+ "/ask",
13233
+ "/show",
13234
+ "/jobs",
13235
+ "/best",
13236
+ "/active",
13237
+ "/classic",
13238
+ "/noobstories"
13239
+ ],
13240
+ utilityPathnames: ["/hide", "/user"],
13241
+ utilityTextPatterns: [
13242
+ /^(hide|past|favorite|unfavorite|flag|unflag|discuss|reply|parent|more)$/,
13243
+ /^\d+\s+(?:comments?|points?)$/
13244
+ ]
13245
+ }
13246
+ ];
13247
+ function matchesSiteFilter(url, filter, baseHostname) {
13248
+ try {
13249
+ const parsed = new URL(url, baseHostname ? `https://${baseHostname}` : void 0);
13250
+ return parsed.hostname === filter.hostname;
13251
+ } catch {
13252
+ return false;
13253
+ }
13254
+ }
13255
+ function isSiteListingPage(url) {
13256
+ for (const filter of SITE_RESULT_FILTERS) {
13257
+ if (!matchesSiteFilter(url, filter, "")) continue;
13258
+ try {
13259
+ const pathname = new URL(url).pathname.replace(/\/+$/, "") || "/";
13260
+ if (filter.listingPaths?.includes(pathname)) return true;
13261
+ } catch {
13262
+ }
13263
+ }
13264
+ return false;
13265
+ }
13266
+ function isSiteUtilityLink(element) {
13267
+ if (!element.href) return false;
13268
+ for (const filter of SITE_RESULT_FILTERS) {
13269
+ if (!matchesSiteFilter(element.href, filter, "")) continue;
13270
+ const text = normalizeComparable(element.text || "");
13271
+ for (const pattern of filter.utilityTextPatterns ?? []) {
13272
+ if (pattern.test(text)) return true;
13273
+ }
13274
+ try {
13275
+ const pathname = new URL(element.href).pathname.replace(/\/+$/, "") || "/";
13276
+ if (filter.utilityPathnames?.includes(pathname)) return true;
13277
+ } catch {
13278
+ }
13279
+ }
13280
+ return false;
13281
+ }
13054
13282
  function isSearchOrListingPage(page) {
13283
+ if (isSiteListingPage(page.url)) return true;
13055
13284
  const haystack = normalizeComparable(
13056
13285
  [
13057
13286
  page.url,
@@ -13103,7 +13332,7 @@ function getResultCandidates(page) {
13103
13332
  const pageHost = normalizeUrlForMatch(page.url);
13104
13333
  const searchOrListingPage = isSearchOrListingPage(page);
13105
13334
  const scored = page.interactiveElements.filter(
13106
- (element) => element.type === "link" && element.text?.trim() && element.href
13335
+ (element) => element.type === "link" && element.text?.trim() && element.href && !isSiteUtilityLink(element)
13107
13336
  ).map((element) => {
13108
13337
  const text = element.text?.trim() || "";
13109
13338
  const comparableText = normalizeComparable(text);
@@ -13563,6 +13792,7 @@ function detectPageType(page) {
13563
13792
  if (hasResults && hasSearchInput && listingLike) return "SEARCH_RESULTS";
13564
13793
  if (hasCart) return "SHOPPING";
13565
13794
  if (formCount > 0 && !hasPasswordField) return "FORM";
13795
+ if (isSiteListingPage(page.url)) return "PAGINATED_LIST";
13566
13796
  if (hasPagination && listingLike) return "PAGINATED_LIST";
13567
13797
  if (hasSearchInput && !listingLike) return "SEARCH_READY";
13568
13798
  if (page.content.length > 3e3 && page.interactiveElements.length < 10)
@@ -15111,7 +15341,7 @@ function vesselIsEditableElement(el) {
15111
15341
  if (!(el instanceof HTMLElement)) return false;
15112
15342
  var role = (el.getAttribute("role") || "").toLowerCase();
15113
15343
  return el.isContentEditable ||
15114
- el.getAttribute("contenteditable") === "true" ||
15344
+ (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") ||
15115
15345
  role === "textbox" ||
15116
15346
  role === "searchbox";
15117
15347
  }
@@ -15120,7 +15350,7 @@ function vesselResolveFillableControl(el) {
15120
15350
  if (!el) return null;
15121
15351
  if (vesselIsNativeField(el) || vesselIsEditableElement(el)) return el;
15122
15352
  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']");
15353
+ var nested = el.querySelector("input:not([type='hidden']):not([type='submit']):not([type='button']), textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox'], [role='searchbox']");
15124
15354
  return nested instanceof HTMLElement ? nested : null;
15125
15355
  }
15126
15356
 
@@ -15138,7 +15368,7 @@ function vesselFindVisibleFillableControl(original) {
15138
15368
 
15139
15369
  var scopes = Array.from(document.querySelectorAll("dialog[open], [role='dialog'], [role='alertdialog'], [aria-modal='true'], [role='listbox'], [role='combobox'][aria-expanded='true']"));
15140
15370
  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']";
15371
+ var selector = "input:not([type='hidden']):not([type='submit']):not([type='button']), textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox'], [role='searchbox']";
15142
15372
  for (var i = 0; i < scopes.length; i += 1) {
15143
15373
  var scope = scopes[i];
15144
15374
  if (!scope || !(scope instanceof Element)) continue;
@@ -15436,7 +15666,7 @@ async function resolveFieldSelector(wc, field) {
15436
15666
  if (!(el instanceof HTMLElement)) return false;
15437
15667
  var role = (el.getAttribute("role") || "").toLowerCase();
15438
15668
  return el.isContentEditable ||
15439
- el.getAttribute("contenteditable") === "true" ||
15669
+ (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") ||
15440
15670
  role === "textbox" ||
15441
15671
  role === "searchbox" ||
15442
15672
  role === "combobox";
@@ -15476,7 +15706,7 @@ async function resolveFieldSelector(wc, field) {
15476
15706
  return score;
15477
15707
  }
15478
15708
 
15479
- const candidates = Array.from(document.querySelectorAll("input, textarea, select, [contenteditable='true'], [role='textbox'], [role='searchbox'], [role='combobox']"));
15709
+ const candidates = Array.from(document.querySelectorAll("input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox'], [role='searchbox'], [role='combobox']"));
15480
15710
  let best = null;
15481
15711
  let bestScore = -1;
15482
15712
  for (const el of candidates) {
@@ -17969,7 +18199,7 @@ async function locateImplicitTextTarget(wc) {
17969
18199
  const role = normalize(el.getAttribute("role"));
17970
18200
  if (
17971
18201
  el.isContentEditable ||
17972
- el.getAttribute("contenteditable") === "true" ||
18202
+ (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") ||
17973
18203
  role === "textbox" ||
17974
18204
  role === "searchbox" ||
17975
18205
  role === "combobox"
@@ -17993,7 +18223,7 @@ async function locateImplicitTextTarget(wc) {
17993
18223
  }
17994
18224
 
17995
18225
  const candidates = Array.from(
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"]')
18226
+ 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"]')
17997
18227
  ).filter((el) => isFillable(el) && isVisible(el));
17998
18228
 
17999
18229
  let best = null;
@@ -19608,9 +19838,11 @@ async function handleHighlight(ctx, args) {
19608
19838
  }
19609
19839
  return highlightOnPage(wc, selector, highlightText, args.label, args.durationMs, highlightColor);
19610
19840
  }
19611
- function handleClearHighlights(ctx) {
19841
+ async function handleClearHighlights(ctx) {
19612
19842
  const wc = ctx.tabManager.getActiveTab()?.view.webContents;
19613
19843
  if (!wc) return "Error: No active tab";
19844
+ const url = normalizeUrl$1(wc.getURL());
19845
+ clearHighlightsForUrl(url);
19614
19846
  return clearHighlights(wc);
19615
19847
  }
19616
19848
  function trimText(value) {
@@ -22419,7 +22651,7 @@ function registerPrivateIpcHandlers(state2) {
22419
22651
  });
22420
22652
  ipc.handle(Channels.IS_PRIVATE_MODE, () => true);
22421
22653
  ipc.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
22422
- createPrivateWindow();
22654
+ return openPrivateWindowSafely();
22423
22655
  });
22424
22656
  ipc.handle(Channels.OPEN_NEW_WINDOW, async () => {
22425
22657
  const { createSecondaryWindow: createSecondaryWindow2 } = await Promise.resolve().then(() => window$1);
@@ -22463,70 +22695,120 @@ function registerPrivateIpcHandlers(state2) {
22463
22695
  function createPrivateWindow() {
22464
22696
  const privateSessionPartition = `private-${crypto$2.randomUUID()}`;
22465
22697
  const privateSession = electron.session.fromPartition(privateSessionPartition);
22466
- privateSession.setUserAgent(electron.session.defaultSession.getUserAgent());
22467
- const win = new electron.BaseWindow({
22468
- width: 1280,
22469
- height: 800,
22470
- minWidth: 800,
22471
- minHeight: 600,
22472
- frame: false,
22473
- show: false,
22474
- backgroundColor: "#1e1a2e",
22475
- title: "Vessel - Private Browsing"
22476
- });
22477
- const chromeView = new electron.WebContentsView({
22478
- webPreferences: {
22479
- preload: path$1.join(__dirname, "../preload/index.js"),
22480
- sandbox: true,
22481
- contextIsolation: true,
22482
- nodeIntegration: false
22698
+ let win = null;
22699
+ let tabManager = null;
22700
+ let state2 = null;
22701
+ try {
22702
+ privateSession.setUserAgent(electron.session.defaultSession.getUserAgent());
22703
+ win = new electron.BaseWindow({
22704
+ width: 1280,
22705
+ height: 800,
22706
+ minWidth: 800,
22707
+ minHeight: 600,
22708
+ frame: false,
22709
+ show: false,
22710
+ backgroundColor: "#1e1a2e",
22711
+ title: "Vessel - Private Browsing"
22712
+ });
22713
+ const chromeView = new electron.WebContentsView({
22714
+ webPreferences: {
22715
+ preload: path$1.join(__dirname, "../preload/index.js"),
22716
+ sandbox: true,
22717
+ contextIsolation: true,
22718
+ nodeIntegration: false
22719
+ }
22720
+ });
22721
+ chromeView.setBackgroundColor("#00000000");
22722
+ win.contentView.addChildView(chromeView);
22723
+ tabManager = new TabManager(
22724
+ win,
22725
+ (tabs, activeId) => {
22726
+ sendSafe(chromeView.webContents, Channels.TAB_STATE_UPDATE, tabs, activeId);
22727
+ if (state2) layoutPrivateViews(state2);
22728
+ },
22729
+ { isPrivate: true, sessionPartition: privateSessionPartition }
22730
+ );
22731
+ state2 = {
22732
+ window: win,
22733
+ chromeView,
22734
+ tabManager,
22735
+ session: privateSession,
22736
+ sessionPartition: privateSessionPartition
22737
+ };
22738
+ installAdBlockingForSession(privateSession, tabManager);
22739
+ installDownloadHandlerForSession(privateSession, chromeView);
22740
+ registerPrivateIpcHandlers(state2);
22741
+ win.on("resize", () => {
22742
+ if (state2) layoutPrivateViews(state2);
22743
+ });
22744
+ win.on("show", () => {
22745
+ if (state2) layoutPrivateViews(state2);
22746
+ });
22747
+ win.on("closed", () => {
22748
+ if (!state2) return;
22749
+ privateWindows.delete(state2);
22750
+ tabManager?.destroyAllTabs();
22751
+ void Promise.all([
22752
+ privateSession.clearStorageData(),
22753
+ privateSession.clearCache()
22754
+ ]).catch((error) => {
22755
+ logger$m.warn("Failed to clear private browsing session:", error);
22756
+ });
22757
+ });
22758
+ privateWindows.add(state2);
22759
+ chromeView.webContents.once("dom-ready", () => {
22760
+ try {
22761
+ tabManager?.createTab("about:blank");
22762
+ if (state2) layoutPrivateViews(state2);
22763
+ } catch (error) {
22764
+ logger$m.error("Failed to initialize private browsing tab:", error);
22765
+ try {
22766
+ win?.close();
22767
+ } catch (cleanupError) {
22768
+ logger$m.warn("Failed to close private window after tab init failure:", cleanupError);
22769
+ }
22770
+ }
22771
+ });
22772
+ loadPrivateRenderer(chromeView);
22773
+ win.show();
22774
+ logger$m.info("Private browsing window opened");
22775
+ return state2;
22776
+ } catch (error) {
22777
+ logger$m.error("Failed to create private browsing window:", error);
22778
+ if (state2) {
22779
+ privateWindows.delete(state2);
22780
+ }
22781
+ try {
22782
+ tabManager?.destroyAllTabs();
22783
+ } catch (cleanupError) {
22784
+ logger$m.warn("Failed to clean up private tabs after launch failure:", cleanupError);
22785
+ }
22786
+ try {
22787
+ win?.close();
22788
+ } catch (cleanupError) {
22789
+ logger$m.warn("Failed to close private window after launch failure:", cleanupError);
22483
22790
  }
22484
- });
22485
- chromeView.setBackgroundColor("#00000000");
22486
- win.contentView.addChildView(chromeView);
22487
- const tabManager = new TabManager(
22488
- win,
22489
- (tabs, activeId) => {
22490
- sendSafe(chromeView.webContents, Channels.TAB_STATE_UPDATE, tabs, activeId);
22491
- layoutPrivateViews(state2);
22492
- },
22493
- { isPrivate: true, sessionPartition: privateSessionPartition }
22494
- );
22495
- const state2 = {
22496
- window: win,
22497
- chromeView,
22498
- tabManager,
22499
- session: privateSession,
22500
- sessionPartition: privateSessionPartition
22501
- };
22502
- installAdBlockingForSession(privateSession, tabManager);
22503
- installDownloadHandlerForSession(privateSession, chromeView);
22504
- registerPrivateIpcHandlers(state2);
22505
- win.on("resize", () => layoutPrivateViews(state2));
22506
- win.on("show", () => layoutPrivateViews(state2));
22507
- win.on("closed", () => {
22508
- privateWindows.delete(state2);
22509
- tabManager.destroyAllTabs();
22510
22791
  void Promise.all([
22511
22792
  privateSession.clearStorageData(),
22512
22793
  privateSession.clearCache()
22513
- ]).catch((error) => {
22514
- logger$m.warn("Failed to clear private browsing session:", error);
22794
+ ]).catch((cleanupError) => {
22795
+ logger$m.warn("Failed to clear failed private browsing session:", cleanupError);
22515
22796
  });
22516
- });
22517
- privateWindows.add(state2);
22518
- chromeView.webContents.once("dom-ready", () => {
22519
- tabManager.createTab("about:blank");
22520
- layoutPrivateViews(state2);
22521
- });
22522
- loadPrivateRenderer(chromeView);
22523
- win.show();
22524
- logger$m.info("Private browsing window opened");
22525
- return state2;
22797
+ throw error;
22798
+ }
22799
+ }
22800
+ function openPrivateWindowSafely() {
22801
+ try {
22802
+ createPrivateWindow();
22803
+ return true;
22804
+ } catch {
22805
+ return false;
22806
+ }
22526
22807
  }
22527
22808
  const window$2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
22528
22809
  __proto__: null,
22529
- createPrivateWindow
22810
+ createPrivateWindow,
22811
+ openPrivateWindowSafely
22530
22812
  }, Symbol.toStringTag, { value: "Module" }));
22531
22813
  const secondaryWindows = /* @__PURE__ */ new Set();
22532
22814
  function layoutSecondaryViews(state2) {
@@ -22644,8 +22926,8 @@ function registerSecondaryIpcHandlers(state2) {
22644
22926
  );
22645
22927
  ipc.handle(Channels.OPEN_NEW_WINDOW, () => createSecondaryWindow());
22646
22928
  ipc.handle(Channels.OPEN_PRIVATE_WINDOW, async () => {
22647
- const { createPrivateWindow: createPrivateWindow2 } = await Promise.resolve().then(() => window$2);
22648
- createPrivateWindow2();
22929
+ const { openPrivateWindowSafely: openPrivateWindowSafely2 } = await Promise.resolve().then(() => window$2);
22930
+ return openPrivateWindowSafely2();
22649
22931
  });
22650
22932
  ipc.handle(Channels.IS_PRIVATE_MODE, () => false);
22651
22933
  ipc.handle(Channels.WINDOW_MINIMIZE, () => state2.window.minimize());
@@ -22733,7 +23015,7 @@ function registerTabHandlers(windowState, _sendToRendererViews) {
22733
23015
  const { tabManager, mainWindow } = windowState;
22734
23016
  electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, (event) => {
22735
23017
  assertTrustedIpcSender(event);
22736
- createPrivateWindow();
23018
+ return openPrivateWindowSafely();
22737
23019
  });
22738
23020
  electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, (event) => {
22739
23021
  assertTrustedIpcSender(event);
@@ -22951,10 +23233,11 @@ const SHARED_NAVIGATION_INSTRUCTIONS = [
22951
23233
  "Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.",
22952
23234
  "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.",
22953
23235
  "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.",
23236
+ "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.",
22954
23237
  "For flight price-shopping, include the route, date, and trip type in web_search(query); do not send vague or partial flight queries.",
22955
23238
  "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.",
22956
23239
  "On retail and marketplace sites, prefer the site's visible search box, filters, and result pages over direct product URLs.",
22957
- "For broad discovery tasks, prefer direct sources and site-specific search over generic search engines."
23240
+ "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."
22958
23241
  ];
22959
23242
  const VESSEL_SOURCE_INSTRUCTIONS = [
22960
23243
  "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.",
@@ -23911,8 +24194,16 @@ function registerHighlightHandlers(windowState, sendToRendererViews) {
23911
24194
  const info = getActiveTabInfo(tabManager);
23912
24195
  if (!info) return false;
23913
24196
  try {
24197
+ const url = normalizeUrl$1(info.wc.getURL());
24198
+ const text = await getHighlightTextAtIndex(info.wc, validatedIndex);
23914
24199
  const removed = await removeHighlightAtIndex(info.wc, validatedIndex);
23915
24200
  if (removed) {
24201
+ if (text) {
24202
+ const persisted = findHighlightByText(url, text);
24203
+ if (persisted) {
24204
+ removeHighlight(persisted.id);
24205
+ }
24206
+ }
23916
24207
  await emitHighlightCount();
23917
24208
  }
23918
24209
  return removed;
@@ -23926,6 +24217,8 @@ function registerHighlightHandlers(windowState, sendToRendererViews) {
23926
24217
  const info = getActiveTabInfo(tabManager);
23927
24218
  if (!info) return false;
23928
24219
  try {
24220
+ const url = normalizeUrl$1(info.wc.getURL());
24221
+ clearHighlightsForUrl(url);
23929
24222
  const cleared = await clearAllHighlightElements(info.wc);
23930
24223
  if (cleared) {
23931
24224
  await emitHighlightCount();