@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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
"
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21918
|
-
|
|
21919
|
-
|
|
21920
|
-
|
|
21921
|
-
|
|
21922
|
-
|
|
21923
|
-
|
|
21924
|
-
|
|
21925
|
-
|
|
21926
|
-
|
|
21927
|
-
|
|
21928
|
-
|
|
21929
|
-
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
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((
|
|
21965
|
-
logger$m.warn("Failed to clear private browsing session:",
|
|
22788
|
+
]).catch((cleanupError) => {
|
|
22789
|
+
logger$m.warn("Failed to clear failed private browsing session:", cleanupError);
|
|
21966
22790
|
});
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
|
|
21970
|
-
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
|
|
21976
|
-
|
|
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 {
|
|
22099
|
-
|
|
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
|
-
|
|
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
|
|
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();
|